;
@@ -65,11 +66,13 @@ function createKibanaRequestMock({
routeAuthRequired,
validation = {},
kibanaRouteState = { xsrfRequired: true },
+ auth = { isAuthenticated: true },
}: RequestFixtureOptions
= {}) {
const queryString = stringify(query, { sort: false });
return KibanaRequest.from
(
createRawRequestMock({
+ auth,
headers,
params,
query,
@@ -113,6 +116,9 @@ function createRawRequestMock(customization: DeepPartial = {}) {
{},
{
app: { xsrfRequired: true } as any,
+ auth: {
+ isAuthenticated: true,
+ },
headers: {},
path: '/',
route: { settings: {} },
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index cffdffab0d0cf..f898ed0ea1a99 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -26,8 +26,7 @@ import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';
-
-import { IRouter, KibanaRouteState, isSafeMethod } from './router';
+import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
@@ -148,7 +147,7 @@ export class HttpServer {
this.log.debug(`registering route handler for [${route.path}]`);
// Hapi does not allow payload validation to be specified for 'head' or 'get' requests
const validate = isSafeMethod(route.method) ? undefined : { payload: true };
- const { authRequired = true, tags, body = {} } = route.options;
+ const { authRequired, tags, body = {} } = route.options;
const { accepts: allow, maxBytes, output, parse } = body;
const kibanaRouteState: KibanaRouteState = {
@@ -160,8 +159,7 @@ export class HttpServer {
method: route.method,
path: route.path,
options: {
- // Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }`
- auth: authRequired === true ? undefined : false,
+ auth: this.getAuthOption(authRequired),
app: kibanaRouteState,
tags: tags ? Array.from(tags) : undefined,
// TODO: This 'validate' section can be removed once the legacy platform is completely removed.
@@ -196,6 +194,22 @@ export class HttpServer {
this.server = undefined;
}
+ private getAuthOption(
+ authRequired: RouteConfigOptions['authRequired'] = true
+ ): undefined | false | { mode: 'required' | 'optional' } {
+ if (this.authRegistered === false) return undefined;
+
+ if (authRequired === true) {
+ return { mode: 'required' };
+ }
+ if (authRequired === 'optional') {
+ return { mode: 'optional' };
+ }
+ if (authRequired === false) {
+ return false;
+ }
+ }
+
private setupBasePathRewrite(config: HttpConfig, basePathService: BasePath) {
if (config.basePath === undefined || !config.rewriteBasePath) {
return;
diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts
index 30032ff5da796..442bc93190d86 100644
--- a/src/core/server/http/http_service.mock.ts
+++ b/src/core/server/http/http_service.mock.ts
@@ -115,6 +115,8 @@ const createOnPostAuthToolkitMock = (): jest.Mocked => ({
const createAuthToolkitMock = (): jest.Mocked => ({
authenticated: jest.fn(),
+ notHandled: jest.fn(),
+ redirected: jest.fn(),
});
const createOnPreResponseToolkitMock = (): jest.Mocked => ({
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index 8f4c02680f8a3..a75eb04fa0120 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -67,9 +67,12 @@ export {
AuthenticationHandler,
AuthHeaders,
AuthResultParams,
+ AuthRedirected,
+ AuthRedirectedParams,
AuthToolkit,
AuthResult,
Authenticated,
+ AuthNotHandled,
AuthResultType,
} from './lifecycle/auth';
export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth';
diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts
index 425d8cac1893e..7b1630a7de0be 100644
--- a/src/core/server/http/integration_tests/core_services.test.ts
+++ b/src/core/server/http/integration_tests/core_services.test.ts
@@ -50,7 +50,7 @@ describe('http service', () => {
await root.shutdown();
});
describe('#isAuthenticated()', () => {
- it('returns true if has been authorized', async () => {
+ it('returns true if has been authenticated', async () => {
const { http } = await root.setup();
const { registerAuth, createRouter, auth } = http;
@@ -65,11 +65,11 @@ describe('http service', () => {
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true });
});
- it('returns false if has not been authorized', async () => {
+ it('returns false if has not been authenticated', async () => {
const { http } = await root.setup();
const { registerAuth, createRouter, auth } = http;
- await registerAuth((req, res, toolkit) => toolkit.authenticated());
+ registerAuth((req, res, toolkit) => toolkit.authenticated());
const router = createRouter('');
router.get(
@@ -81,7 +81,7 @@ describe('http service', () => {
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
});
- it('returns false if no authorization mechanism has been registered', async () => {
+ it('returns false if no authentication mechanism has been registered', async () => {
const { http } = await root.setup();
const { createRouter, auth } = http;
@@ -94,6 +94,37 @@ describe('http service', () => {
await root.start();
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
});
+
+ it('returns true if authenticated on a route with "optional" auth', async () => {
+ const { http } = await root.setup();
+ const { createRouter, auth, registerAuth } = http;
+
+ registerAuth((req, res, toolkit) => toolkit.authenticated());
+ const router = createRouter('');
+ router.get(
+ { path: '/is-auth', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } })
+ );
+
+ await root.start();
+ await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true });
+ });
+
+ it('returns false if not authenticated on a route with "optional" auth', async () => {
+ const { http } = await root.setup();
+ const { createRouter, auth, registerAuth } = http;
+
+ registerAuth((req, res, toolkit) => toolkit.notHandled());
+
+ const router = createRouter('');
+ router.get(
+ { path: '/is-auth', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } })
+ );
+
+ await root.start();
+ await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
+ });
});
describe('#get()', () => {
it('returns authenticated status and allow associate auth state with request', async () => {
diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts
index 6dc7ece1359df..0f0d54e88daca 100644
--- a/src/core/server/http/integration_tests/lifecycle.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle.test.ts
@@ -57,7 +57,7 @@ interface StorageData {
}
describe('OnPreAuth', () => {
- it('supports registering request inceptors', async () => {
+ it('supports registering a request interceptor', async () => {
const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@@ -415,6 +415,23 @@ describe('Auth', () => {
.expect(200, { content: 'ok' });
});
+ it('blocks access to a resource if credentials are not provided', async () => {
+ const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.get({ path: '/', validate: false }, (context, req, res) =>
+ res.ok({ body: { content: 'ok' } })
+ );
+ registerAuth((req, res, t) => t.notHandled());
+ await server.start();
+
+ const result = await supertest(innerServer.listener)
+ .get('/')
+ .expect(401);
+
+ expect(result.body.message).toBe('Unauthorized');
+ });
+
it('enables auth for a route by default if registerAuth has been called', async () => {
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@@ -492,11 +509,9 @@ describe('Auth', () => {
router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
const redirectTo = '/redirect-url';
- registerAuth((req, res) =>
- res.redirected({
- headers: {
- location: redirectTo,
- },
+ registerAuth((req, res, t) =>
+ t.redirected({
+ location: redirectTo,
})
);
await server.start();
@@ -507,6 +522,19 @@ describe('Auth', () => {
expect(response.header.location).toBe(redirectTo);
});
+ it('throws if redirection url is not provided', async () => {
+ const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
+ registerAuth((req, res, t) => t.redirected({} as any));
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(500);
+ });
+
it(`doesn't expose internal error details`, async () => {
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@@ -865,7 +893,7 @@ describe('Auth', () => {
]
`);
});
- // eslint-disable-next-line
+
it(`doesn't share request object between interceptors`, async () => {
const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
index bc1bbc881315a..85270174fbc04 100644
--- a/src/core/server/http/integration_tests/request.test.ts
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -45,6 +45,89 @@ afterEach(async () => {
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
describe('KibanaRequest', () => {
+ describe('auth', () => {
+ describe('isAuthenticated', () => {
+ it('returns false if no auth interceptor was registered', async () => {
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+ router.get(
+ { path: '/', validate: false, options: { authRequired: true } },
+ (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ isAuthenticated: false,
+ });
+ });
+ it('returns false if not authenticated on a route with authRequired: "optional"', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+ registerAuth((req, res, toolkit) => toolkit.notHandled());
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ isAuthenticated: false,
+ });
+ });
+ it('returns false if redirected on a route with authRequired: "optional"', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+ registerAuth((req, res, toolkit) => toolkit.redirected({ location: '/any' }));
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ isAuthenticated: false,
+ });
+ });
+ it('returns true if authenticated on a route with authRequired: "optional"', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+ registerAuth((req, res, toolkit) => toolkit.authenticated());
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ isAuthenticated: true,
+ });
+ });
+ it('returns true if authenticated', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+ registerAuth((req, res, toolkit) => toolkit.authenticated());
+ router.get(
+ { path: '/', validate: false, options: { authRequired: true } },
+ (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ isAuthenticated: true,
+ });
+ });
+ });
+ });
describe('events', () => {
describe('aborted$', () => {
it('emits once and completes when request aborted', async done => {
diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts
index a1523781010d4..ee5b0c50acafb 100644
--- a/src/core/server/http/integration_tests/router.test.ts
+++ b/src/core/server/http/integration_tests/router.test.ts
@@ -46,6 +46,286 @@ afterEach(async () => {
await server.stop();
});
+describe('Options', () => {
+ describe('authRequired', () => {
+ describe('optional', () => {
+ it('User has access to a route if auth mechanism not registered', async () => {
+ const { server: innerServer, createRouter, auth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) =>
+ res.ok({
+ body: {
+ httpAuthIsAuthenticated: auth.isAuthenticated(req),
+ requestIsAuthenticated: req.auth.isAuthenticated,
+ },
+ })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ httpAuthIsAuthenticated: false,
+ requestIsAuthenticated: false,
+ });
+ });
+
+ it('Authenticated user has access to a route', async () => {
+ const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ registerAuth((req, res, toolkit) => {
+ return toolkit.authenticated();
+ });
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) =>
+ res.ok({
+ body: {
+ httpAuthIsAuthenticated: auth.isAuthenticated(req),
+ requestIsAuthenticated: req.auth.isAuthenticated,
+ },
+ })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ httpAuthIsAuthenticated: true,
+ requestIsAuthenticated: true,
+ });
+ });
+
+ it('User with no credentials can access a route', async () => {
+ const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ registerAuth((req, res, toolkit) => toolkit.notHandled());
+
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) =>
+ res.ok({
+ body: {
+ httpAuthIsAuthenticated: auth.isAuthenticated(req),
+ requestIsAuthenticated: req.auth.isAuthenticated,
+ },
+ })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ httpAuthIsAuthenticated: false,
+ requestIsAuthenticated: false,
+ });
+ });
+
+ it('User with invalid credentials cannot access a route', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ registerAuth((req, res, toolkit) => res.unauthorized());
+
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) => res.ok({ body: 'ok' })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(401);
+ });
+
+ it('does not redirect user and allows access to a resource', async () => {
+ const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ registerAuth((req, res, toolkit) =>
+ toolkit.redirected({
+ location: '/redirect-to',
+ })
+ );
+
+ router.get(
+ { path: '/', validate: false, options: { authRequired: 'optional' } },
+ (context, req, res) =>
+ res.ok({
+ body: {
+ httpAuthIsAuthenticated: auth.isAuthenticated(req),
+ requestIsAuthenticated: req.auth.isAuthenticated,
+ },
+ })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ httpAuthIsAuthenticated: false,
+ requestIsAuthenticated: false,
+ });
+ });
+ });
+
+ describe('true', () => {
+ it('User has access to a route if auth interceptor is not registered', async () => {
+ const { server: innerServer, createRouter, auth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.get(
+ { path: '/', validate: false, options: { authRequired: true } },
+ (context, req, res) =>
+ res.ok({
+ body: {
+ httpAuthIsAuthenticated: auth.isAuthenticated(req),
+ requestIsAuthenticated: req.auth.isAuthenticated,
+ },
+ })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ httpAuthIsAuthenticated: false,
+ requestIsAuthenticated: false,
+ });
+ });
+
+ it('Authenticated user has access to a route', async () => {
+ const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ registerAuth((req, res, toolkit) => {
+ return toolkit.authenticated();
+ });
+ router.get(
+ { path: '/', validate: false, options: { authRequired: true } },
+ (context, req, res) =>
+ res.ok({
+ body: {
+ httpAuthIsAuthenticated: auth.isAuthenticated(req),
+ requestIsAuthenticated: req.auth.isAuthenticated,
+ },
+ })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ httpAuthIsAuthenticated: true,
+ requestIsAuthenticated: true,
+ });
+ });
+
+ it('User with no credentials cannot access a route', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ registerAuth((req, res, toolkit) => toolkit.notHandled());
+ router.get(
+ { path: '/', validate: false, options: { authRequired: true } },
+ (context, req, res) => res.ok({ body: 'ok' })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(401);
+ });
+
+ it('User with invalid credentials cannot access a route', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ registerAuth((req, res, toolkit) => res.unauthorized());
+
+ router.get(
+ { path: '/', validate: false, options: { authRequired: true } },
+ (context, req, res) => res.ok({ body: 'ok' })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(401);
+ });
+
+ it('allows redirecting an user', async () => {
+ const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
+ const router = createRouter('/');
+ const redirectUrl = '/redirect-to';
+
+ registerAuth((req, res, toolkit) =>
+ toolkit.redirected({
+ location: redirectUrl,
+ })
+ );
+
+ router.get(
+ { path: '/', validate: false, options: { authRequired: true } },
+ (context, req, res) => res.ok({ body: 'ok' })
+ );
+ await server.start();
+
+ const result = await supertest(innerServer.listener)
+ .get('/')
+ .expect(302);
+
+ expect(result.header.location).toBe(redirectUrl);
+ });
+ });
+
+ describe('false', () => {
+ it('does not try to authenticate a user', async () => {
+ const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ const authHook = jest.fn();
+ registerAuth(authHook);
+ router.get(
+ { path: '/', validate: false, options: { authRequired: false } },
+ (context, req, res) =>
+ res.ok({
+ body: {
+ httpAuthIsAuthenticated: auth.isAuthenticated(req),
+ requestIsAuthenticated: req.auth.isAuthenticated,
+ },
+ })
+ );
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, {
+ httpAuthIsAuthenticated: false,
+ requestIsAuthenticated: false,
+ });
+
+ expect(authHook).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+});
+
describe('Handler', () => {
it("Doesn't expose error details if handler throws", async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts
index 036ab0211c2ff..2eaf7e0f6fbfe 100644
--- a/src/core/server/http/lifecycle/auth.ts
+++ b/src/core/server/http/lifecycle/auth.ts
@@ -25,11 +25,14 @@ import {
lifecycleResponseFactory,
LifecycleResponseFactory,
isKibanaResponse,
+ ResponseHeaders,
} from '../router';
/** @public */
export enum AuthResultType {
authenticated = 'authenticated',
+ notHandled = 'notHandled',
+ redirected = 'redirected',
}
/** @public */
@@ -38,10 +41,20 @@ export interface Authenticated extends AuthResultParams {
}
/** @public */
-export type AuthResult = Authenticated;
+export interface AuthNotHandled {
+ type: AuthResultType.notHandled;
+}
+
+/** @public */
+export interface AuthRedirected extends AuthRedirectedParams {
+ type: AuthResultType.redirected;
+}
+
+/** @public */
+export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected;
const authResult = {
- authenticated(data: Partial = {}): AuthResult {
+ authenticated(data: AuthResultParams = {}): AuthResult {
return {
type: AuthResultType.authenticated,
state: data.state,
@@ -49,8 +62,25 @@ const authResult = {
responseHeaders: data.responseHeaders,
};
},
+ notHandled(): AuthResult {
+ return {
+ type: AuthResultType.notHandled,
+ };
+ },
+ redirected(headers: { location: string } & ResponseHeaders): AuthResult {
+ return {
+ type: AuthResultType.redirected,
+ headers,
+ };
+ },
isAuthenticated(result: AuthResult): result is Authenticated {
- return result && result.type === AuthResultType.authenticated;
+ return result?.type === AuthResultType.authenticated;
+ },
+ isNotHandled(result: AuthResult): result is AuthNotHandled {
+ return result?.type === AuthResultType.notHandled;
+ },
+ isRedirected(result: AuthResult): result is AuthRedirected {
+ return result?.type === AuthResultType.redirected;
},
};
@@ -62,7 +92,7 @@ const authResult = {
export type AuthHeaders = Record;
/**
- * Result of an incoming request authentication.
+ * Result of successful authentication.
* @public
*/
export interface AuthResultParams {
@@ -82,6 +112,18 @@ export interface AuthResultParams {
responseHeaders?: AuthHeaders;
}
+/**
+ * Result of auth redirection.
+ * @public
+ */
+export interface AuthRedirectedParams {
+ /**
+ * Headers to attach for auth redirect.
+ * Must include "location" header
+ */
+ headers: { location: string } & ResponseHeaders;
+}
+
/**
* @public
* A tool set defining an outcome of Auth interceptor for incoming request.
@@ -89,10 +131,23 @@ export interface AuthResultParams {
export interface AuthToolkit {
/** Authentication is successful with given credentials, allow request to pass through */
authenticated: (data?: AuthResultParams) => AuthResult;
+ /**
+ * User has no credentials.
+ * Allows user to access a resource when authRequired: 'optional'
+ * Rejects a request when authRequired: true
+ * */
+ notHandled: () => AuthResult;
+ /**
+ * Redirects user to another location to complete authentication when authRequired: true
+ * Allows user to access a resource without redirection when authRequired: 'optional'
+ * */
+ redirected: (headers: { location: string } & ResponseHeaders) => AuthResult;
}
const toolkit: AuthToolkit = {
authenticated: authResult.authenticated,
+ notHandled: authResult.notHandled,
+ redirected: authResult.redirected,
};
/**
@@ -109,30 +164,51 @@ export type AuthenticationHandler = (
export function adoptToHapiAuthFormat(
fn: AuthenticationHandler,
log: Logger,
- onSuccess: (req: Request, data: AuthResultParams) => void = () => undefined
+ onAuth: (request: Request, data: AuthResultParams) => void = () => undefined
) {
return async function interceptAuth(
request: Request,
responseToolkit: ResponseToolkit
): Promise {
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
+ const kibanaRequest = KibanaRequest.from(request, undefined, false);
+
try {
- const result = await fn(
- KibanaRequest.from(request, undefined, false),
- lifecycleResponseFactory,
- toolkit
- );
+ const result = await fn(kibanaRequest, lifecycleResponseFactory, toolkit);
+
if (isKibanaResponse(result)) {
return hapiResponseAdapter.handle(result);
}
+
if (authResult.isAuthenticated(result)) {
- onSuccess(request, {
+ onAuth(request, {
state: result.state,
requestHeaders: result.requestHeaders,
responseHeaders: result.responseHeaders,
});
return responseToolkit.authenticated({ credentials: result.state || {} });
}
+
+ if (authResult.isRedirected(result)) {
+ // we cannot redirect a user when resources with optional auth requested
+ if (kibanaRequest.route.options.authRequired === 'optional') {
+ return responseToolkit.continue;
+ }
+
+ return hapiResponseAdapter.handle(
+ lifecycleResponseFactory.redirected({
+ // hapi doesn't accept string[] as a valid header
+ headers: result.headers as any,
+ })
+ );
+ }
+
+ if (authResult.isNotHandled(result)) {
+ if (kibanaRequest.route.options.authRequired === 'optional') {
+ return responseToolkit.continue;
+ }
+ return hapiResponseAdapter.handle(lifecycleResponseFactory.unauthorized());
+ }
throw new Error(
`Unexpected result from Authenticate. Expected AuthResult or KibanaResponse, but given: ${result}.`
);
diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts
index 032027c234485..fb999dc60e39c 100644
--- a/src/core/server/http/router/request.test.ts
+++ b/src/core/server/http/router/request.test.ts
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { RouteOptions } from 'hapi';
import { KibanaRequest } from './request';
import { httpServerMock } from '../http_server.mocks';
import { schema } from '@kbn/config-schema';
@@ -117,6 +118,106 @@ describe('KibanaRequest', () => {
});
});
+ describe('route.options.authRequired property', () => {
+ it('handles required auth: undefined', () => {
+ const auth: RouteOptions['auth'] = undefined;
+ const request = httpServerMock.createRawRequest({
+ route: {
+ settings: {
+ auth,
+ },
+ },
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+
+ expect(kibanaRequest.route.options.authRequired).toBe(true);
+ });
+ it('handles required auth: false', () => {
+ const auth: RouteOptions['auth'] = false;
+ const request = httpServerMock.createRawRequest({
+ route: {
+ settings: {
+ auth,
+ },
+ },
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+
+ expect(kibanaRequest.route.options.authRequired).toBe(false);
+ });
+ it('handles required auth: { mode: "required" }', () => {
+ const auth: RouteOptions['auth'] = { mode: 'required' };
+ const request = httpServerMock.createRawRequest({
+ route: {
+ settings: {
+ auth,
+ },
+ },
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+
+ expect(kibanaRequest.route.options.authRequired).toBe(true);
+ });
+
+ it('handles required auth: { mode: "optional" }', () => {
+ const auth: RouteOptions['auth'] = { mode: 'optional' };
+ const request = httpServerMock.createRawRequest({
+ route: {
+ settings: {
+ auth,
+ },
+ },
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+
+ expect(kibanaRequest.route.options.authRequired).toBe('optional');
+ });
+
+ it('handles required auth: { mode: "try" } as "optional"', () => {
+ const auth: RouteOptions['auth'] = { mode: 'try' };
+ const request = httpServerMock.createRawRequest({
+ route: {
+ settings: {
+ auth,
+ },
+ },
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+
+ expect(kibanaRequest.route.options.authRequired).toBe('optional');
+ });
+
+ it('throws on auth: strategy name', () => {
+ const auth: RouteOptions['auth'] = 'session';
+ const request = httpServerMock.createRawRequest({
+ route: {
+ settings: {
+ auth,
+ },
+ },
+ });
+
+ expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot(
+ `"unexpected authentication options: \\"session\\" for route: /"`
+ );
+ });
+
+ it('throws on auth: { mode: unexpected mode }', () => {
+ const auth: RouteOptions['auth'] = { mode: undefined };
+ const request = httpServerMock.createRawRequest({
+ route: {
+ settings: {
+ auth,
+ },
+ },
+ });
+
+ expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot(
+ `"unexpected authentication options: {} for route: /"`
+ );
+ });
+ });
+
describe('RouteSchema type inferring', () => {
it('should work with config-schema', () => {
const body = Buffer.from('body!');
diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts
index bb2db6367f701..f266677c1a172 100644
--- a/src/core/server/http/router/request.ts
+++ b/src/core/server/http/router/request.ts
@@ -143,6 +143,10 @@ export class KibanaRequest<
public readonly socket: IKibanaSocket;
/** Request events {@link KibanaRequestEvents} */
public readonly events: KibanaRequestEvents;
+ public readonly auth: {
+ /* true if the request has been successfully authenticated, otherwise false. */
+ isAuthenticated: boolean;
+ };
/** @internal */
protected readonly [requestSymbol]: Request;
@@ -172,6 +176,11 @@ export class KibanaRequest<
this.route = deepFreeze(this.getRouteInfo(request));
this.socket = new KibanaSocket(request.raw.req.socket);
this.events = this.getEvents(request);
+
+ this.auth = {
+ // missing in fakeRequests, so we cast to false
+ isAuthenticated: Boolean(request.auth?.isAuthenticated),
+ };
}
private getEvents(request: Request): KibanaRequestEvents {
@@ -189,7 +198,7 @@ export class KibanaRequest<
const { parse, maxBytes, allow, output } = request.route.settings.payload || {};
const options = ({
- authRequired: request.route.settings.auth !== false,
+ authRequired: this.getAuthRequired(request),
// some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true,
tags: request.route.settings.tags || [],
@@ -209,6 +218,31 @@ export class KibanaRequest<
options,
};
}
+
+ private getAuthRequired(request: Request): boolean | 'optional' {
+ const authOptions = request.route.settings.auth;
+ if (typeof authOptions === 'object') {
+ // 'try' is used in the legacy platform
+ if (authOptions.mode === 'optional' || authOptions.mode === 'try') {
+ return 'optional';
+ }
+ if (authOptions.mode === 'required') {
+ return true;
+ }
+ }
+
+ // legacy platform routes
+ if (authOptions === undefined) {
+ return true;
+ }
+
+ if (authOptions === false) return false;
+ throw new Error(
+ `unexpected authentication options: ${JSON.stringify(authOptions)} for route: ${
+ this.url.href
+ }`
+ );
+ }
}
/**
diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts
index d1458ef4ad063..bb0a8616e7222 100644
--- a/src/core/server/http/router/route.ts
+++ b/src/core/server/http/router/route.ts
@@ -116,13 +116,15 @@ export interface RouteConfigOptionsBody {
*/
export interface RouteConfigOptions {
/**
- * A flag shows that authentication for a route:
- * `enabled` when true
- * `disabled` when false
+ * Defines authentication mode for a route:
+ * - true. A user has to have valid credentials to access a resource
+ * - false. A user can access a resource without any credentials.
+ * - 'optional'. A user can access a resource if has valid credentials or no credentials at all.
+ * Can be useful when we grant access to a resource but want to identify a user if possible.
*
- * Enabled by default.
+ * Defaults to `true` if an auth mechanism is registered.
*/
- authRequired?: boolean;
+ authRequired?: boolean | 'optional';
/**
* Defines xsrf protection requirements for a route:
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 8e481171116fa..80eabe778ece3 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -100,9 +100,12 @@ export {
AuthResultParams,
AuthStatus,
AuthToolkit,
+ AuthRedirected,
+ AuthRedirectedParams,
AuthResult,
AuthResultType,
Authenticated,
+ AuthNotHandled,
BasePath,
IBasePath,
CustomHttpResponseOptions,
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts b/src/core/server/metrics/collectors/mocks.ts
similarity index 72%
rename from src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts
rename to src/core/server/metrics/collectors/mocks.ts
index d2d11c14a3e5f..d1eb15637779a 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts
+++ b/src/core/server/metrics/collectors/mocks.ts
@@ -17,4 +17,19 @@
* under the License.
*/
-export function calculateObjectHash(obj: object): string;
+import { MetricsCollector } from './types';
+
+const createMock = () => {
+ const mocked: jest.Mocked> = {
+ collect: jest.fn(),
+ reset: jest.fn(),
+ };
+
+ mocked.collect.mockResolvedValue({});
+
+ return mocked;
+};
+
+export const collectorMock = {
+ create: createMock,
+};
diff --git a/src/core/server/metrics/collectors/os.ts b/src/core/server/metrics/collectors/os.ts
index d3d9bb0be86fa..59bef9d8ddd2b 100644
--- a/src/core/server/metrics/collectors/os.ts
+++ b/src/core/server/metrics/collectors/os.ts
@@ -57,4 +57,6 @@ export class OsMetricsCollector implements MetricsCollector {
return metrics;
}
+
+ public reset() {}
}
diff --git a/src/core/server/metrics/collectors/process.ts b/src/core/server/metrics/collectors/process.ts
index aa68abaf74e41..a3b59a7cc8b7c 100644
--- a/src/core/server/metrics/collectors/process.ts
+++ b/src/core/server/metrics/collectors/process.ts
@@ -40,6 +40,8 @@ export class ProcessMetricsCollector implements MetricsCollector => {
diff --git a/src/core/server/metrics/collectors/server.ts b/src/core/server/metrics/collectors/server.ts
index e46ac2f653df6..84204d0466ff3 100644
--- a/src/core/server/metrics/collectors/server.ts
+++ b/src/core/server/metrics/collectors/server.ts
@@ -26,12 +26,12 @@ interface ServerResponseTime {
}
export class ServerMetricsCollector implements MetricsCollector {
- private readonly requests: OpsServerMetrics['requests'] = {
+ private requests: OpsServerMetrics['requests'] = {
disconnects: 0,
total: 0,
statusCodes: {},
};
- private readonly responseTimes: ServerResponseTime = {
+ private responseTimes: ServerResponseTime = {
count: 0,
total: 0,
max: 0,
@@ -77,4 +77,17 @@ export class ServerMetricsCollector implements MetricsCollector {
+ /** collect the data currently gathered by the collector */
collect(): Promise;
+ /** reset the internal state of the collector */
+ reset(): void;
}
/**
diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts
index 6baf95894b9b4..dd5c256cf1600 100644
--- a/src/core/server/metrics/integration_tests/server_collector.test.ts
+++ b/src/core/server/metrics/integration_tests/server_collector.test.ts
@@ -200,4 +200,80 @@ describe('ServerMetricsCollector', () => {
metrics = await collector.collect();
expect(metrics.concurrent_connections).toEqual(0);
});
+
+ describe('#reset', () => {
+ it('reset the requests state', async () => {
+ router.get({ path: '/', validate: false }, async (ctx, req, res) => {
+ return res.ok({ body: '' });
+ });
+ await server.start();
+
+ await sendGet('/');
+ await sendGet('/');
+ await sendGet('/not-found');
+
+ let metrics = await collector.collect();
+
+ expect(metrics.requests).toEqual({
+ total: 3,
+ disconnects: 0,
+ statusCodes: {
+ '200': 2,
+ '404': 1,
+ },
+ });
+
+ collector.reset();
+ metrics = await collector.collect();
+
+ expect(metrics.requests).toEqual({
+ total: 0,
+ disconnects: 0,
+ statusCodes: {},
+ });
+
+ await sendGet('/');
+ await sendGet('/not-found');
+
+ metrics = await collector.collect();
+
+ expect(metrics.requests).toEqual({
+ total: 2,
+ disconnects: 0,
+ statusCodes: {
+ '200': 1,
+ '404': 1,
+ },
+ });
+ });
+
+ it('resets the response times', async () => {
+ router.get({ path: '/no-delay', validate: false }, async (ctx, req, res) => {
+ return res.ok({ body: '' });
+ });
+ router.get({ path: '/500-ms', validate: false }, async (ctx, req, res) => {
+ await delay(500);
+ return res.ok({ body: '' });
+ });
+
+ await server.start();
+
+ await Promise.all([sendGet('/no-delay'), sendGet('/500-ms')]);
+ let metrics = await collector.collect();
+
+ expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(250);
+ expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500);
+
+ collector.reset();
+ metrics = await collector.collect();
+ expect(metrics.response_times.avg_in_millis).toBe(0);
+ expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(0);
+
+ await Promise.all([sendGet('/500-ms'), sendGet('/500-ms')]);
+ metrics = await collector.collect();
+
+ expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(500);
+ expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500);
+ });
+ });
});
diff --git a/src/core/server/metrics/metrics_service.test.mocks.ts b/src/core/server/metrics/metrics_service.test.mocks.ts
index 8e91775283042..fe46e5693bf45 100644
--- a/src/core/server/metrics/metrics_service.test.mocks.ts
+++ b/src/core/server/metrics/metrics_service.test.mocks.ts
@@ -17,9 +17,10 @@
* under the License.
*/
-export const mockOpsCollector = {
- collect: jest.fn(),
-};
+import { collectorMock } from './collectors/mocks';
+
+export const mockOpsCollector = collectorMock.create();
+
jest.doMock('./ops_metrics_collector', () => ({
OpsMetricsCollector: jest.fn().mockImplementation(() => mockOpsCollector),
}));
diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts
index 10d6761adbe7d..f6334cc5d3c0f 100644
--- a/src/core/server/metrics/metrics_service.test.ts
+++ b/src/core/server/metrics/metrics_service.test.ts
@@ -57,37 +57,50 @@ describe('MetricsService', () => {
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), testInterval);
});
- it('emits the metrics at start', async () => {
+ it('collects the metrics at every interval', async () => {
mockOpsCollector.collect.mockResolvedValue(dummyMetrics);
- const { getOpsMetrics$ } = await metricsService.setup({
- http: httpMock,
- });
-
+ await metricsService.setup({ http: httpMock });
await metricsService.start();
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1);
- expect(
- await getOpsMetrics$()
- .pipe(take(1))
- .toPromise()
- ).toEqual(dummyMetrics);
+
+ jest.advanceTimersByTime(testInterval);
+ expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2);
+
+ jest.advanceTimersByTime(testInterval);
+ expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3);
});
- it('collects the metrics at every interval', async () => {
+ it('resets the collector after each collection', async () => {
mockOpsCollector.collect.mockResolvedValue(dummyMetrics);
- await metricsService.setup({ http: httpMock });
-
+ const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock });
await metricsService.start();
+ // `advanceTimersByTime` only ensure the interval handler is executed
+ // however the `reset` call is executed after the async call to `collect`
+ // meaning that we are going to miss the call if we don't wait for the
+ // actual observable emission that is performed after
+ const waitForNextEmission = () =>
+ getOpsMetrics$()
+ .pipe(take(1))
+ .toPromise();
+
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1);
+ expect(mockOpsCollector.reset).toHaveBeenCalledTimes(1);
+ let nextEmission = waitForNextEmission();
jest.advanceTimersByTime(testInterval);
+ await nextEmission;
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2);
+ expect(mockOpsCollector.reset).toHaveBeenCalledTimes(2);
+ nextEmission = waitForNextEmission();
jest.advanceTimersByTime(testInterval);
+ await nextEmission;
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3);
+ expect(mockOpsCollector.reset).toHaveBeenCalledTimes(3);
});
it('throws when called before setup', async () => {
diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts
index 1aed89a4aad60..0ea9d00792600 100644
--- a/src/core/server/metrics/metrics_service.ts
+++ b/src/core/server/metrics/metrics_service.ts
@@ -17,8 +17,8 @@
* under the License.
*/
-import { ReplaySubject } from 'rxjs';
-import { first, shareReplay } from 'rxjs/operators';
+import { Subject } from 'rxjs';
+import { first } from 'rxjs/operators';
import { CoreService } from '../../types';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
@@ -37,7 +37,7 @@ export class MetricsService
private readonly logger: Logger;
private metricsCollector?: OpsMetricsCollector;
private collectInterval?: NodeJS.Timeout;
- private metrics$ = new ReplaySubject(1);
+ private metrics$ = new Subject();
constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger.get('metrics');
@@ -46,7 +46,7 @@ export class MetricsService
public async setup({ http }: MetricsServiceSetupDeps): Promise {
this.metricsCollector = new OpsMetricsCollector(http.server);
- const metricsObservable = this.metrics$.pipe(shareReplay(1));
+ const metricsObservable = this.metrics$.asObservable();
return {
getOpsMetrics$: () => metricsObservable,
@@ -74,6 +74,7 @@ export class MetricsService
private async refreshMetrics() {
this.logger.debug('Refreshing metrics');
const metrics = await this.metricsCollector!.collect();
+ this.metricsCollector!.reset();
this.metrics$.next(metrics);
}
diff --git a/src/core/server/metrics/ops_metrics_collector.test.mocks.ts b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts
index 8265796d57970..cf51f8a753729 100644
--- a/src/core/server/metrics/ops_metrics_collector.test.mocks.ts
+++ b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts
@@ -17,23 +17,19 @@
* under the License.
*/
-export const mockOsCollector = {
- collect: jest.fn(),
-};
+import { collectorMock } from './collectors/mocks';
+
+export const mockOsCollector = collectorMock.create();
jest.doMock('./collectors/os', () => ({
OsMetricsCollector: jest.fn().mockImplementation(() => mockOsCollector),
}));
-export const mockProcessCollector = {
- collect: jest.fn(),
-};
+export const mockProcessCollector = collectorMock.create();
jest.doMock('./collectors/process', () => ({
ProcessMetricsCollector: jest.fn().mockImplementation(() => mockProcessCollector),
}));
-export const mockServerCollector = {
- collect: jest.fn(),
-};
+export const mockServerCollector = collectorMock.create();
jest.doMock('./collectors/server', () => ({
ServerMetricsCollector: jest.fn().mockImplementation(() => mockServerCollector),
}));
diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts
index 04302a195fb6c..559588db60a42 100644
--- a/src/core/server/metrics/ops_metrics_collector.test.ts
+++ b/src/core/server/metrics/ops_metrics_collector.test.ts
@@ -35,25 +35,43 @@ describe('OpsMetricsCollector', () => {
mockOsCollector.collect.mockResolvedValue('osMetrics');
});
- it('gathers metrics from the underlying collectors', async () => {
- mockOsCollector.collect.mockResolvedValue('osMetrics');
- mockProcessCollector.collect.mockResolvedValue('processMetrics');
- mockServerCollector.collect.mockResolvedValue({
- requests: 'serverRequestsMetrics',
- response_times: 'serverTimingMetrics',
+ describe('#collect', () => {
+ it('gathers metrics from the underlying collectors', async () => {
+ mockOsCollector.collect.mockResolvedValue('osMetrics');
+ mockProcessCollector.collect.mockResolvedValue('processMetrics');
+ mockServerCollector.collect.mockResolvedValue({
+ requests: 'serverRequestsMetrics',
+ response_times: 'serverTimingMetrics',
+ });
+
+ const metrics = await collector.collect();
+
+ expect(mockOsCollector.collect).toHaveBeenCalledTimes(1);
+ expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1);
+ expect(mockServerCollector.collect).toHaveBeenCalledTimes(1);
+
+ expect(metrics).toEqual({
+ process: 'processMetrics',
+ os: 'osMetrics',
+ requests: 'serverRequestsMetrics',
+ response_times: 'serverTimingMetrics',
+ });
});
+ });
+
+ describe('#reset', () => {
+ it('call reset on the underlying collectors', () => {
+ collector.reset();
- const metrics = await collector.collect();
+ expect(mockOsCollector.reset).toHaveBeenCalledTimes(1);
+ expect(mockProcessCollector.reset).toHaveBeenCalledTimes(1);
+ expect(mockServerCollector.reset).toHaveBeenCalledTimes(1);
- expect(mockOsCollector.collect).toHaveBeenCalledTimes(1);
- expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1);
- expect(mockServerCollector.collect).toHaveBeenCalledTimes(1);
+ collector.reset();
- expect(metrics).toEqual({
- process: 'processMetrics',
- os: 'osMetrics',
- requests: 'serverRequestsMetrics',
- response_times: 'serverTimingMetrics',
+ expect(mockOsCollector.reset).toHaveBeenCalledTimes(2);
+ expect(mockProcessCollector.reset).toHaveBeenCalledTimes(2);
+ expect(mockServerCollector.reset).toHaveBeenCalledTimes(2);
});
});
});
diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts
index 04344f21f57f7..525515dba1457 100644
--- a/src/core/server/metrics/ops_metrics_collector.ts
+++ b/src/core/server/metrics/ops_metrics_collector.ts
@@ -49,4 +49,10 @@ export class OpsMetricsCollector implements MetricsCollector {
...server,
};
}
+
+ public reset() {
+ this.processCollector.reset();
+ this.osCollector.reset();
+ this.serverCollector.reset();
+ }
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 30695df33345a..f7afe7a6a290a 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -419,7 +419,26 @@ export type AuthenticationHandler = (request: KibanaRequest, response: Lifecycle
export type AuthHeaders = Record;
// @public (undocumented)
-export type AuthResult = Authenticated;
+export interface AuthNotHandled {
+ // (undocumented)
+ type: AuthResultType.notHandled;
+}
+
+// @public (undocumented)
+export interface AuthRedirected extends AuthRedirectedParams {
+ // (undocumented)
+ type: AuthResultType.redirected;
+}
+
+// @public
+export interface AuthRedirectedParams {
+ headers: {
+ location: string;
+ } & ResponseHeaders;
+}
+
+// @public (undocumented)
+export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected;
// @public
export interface AuthResultParams {
@@ -431,7 +450,11 @@ export interface AuthResultParams {
// @public (undocumented)
export enum AuthResultType {
// (undocumented)
- authenticated = "authenticated"
+ authenticated = "authenticated",
+ // (undocumented)
+ notHandled = "notHandled",
+ // (undocumented)
+ redirected = "redirected"
}
// @public
@@ -444,6 +467,10 @@ export enum AuthStatus {
// @public
export interface AuthToolkit {
authenticated: (data?: AuthResultParams) => AuthResult;
+ notHandled: () => AuthResult;
+ redirected: (headers: {
+ location: string;
+ } & ResponseHeaders) => AuthResult;
}
// @public
@@ -970,6 +997,10 @@ export class KibanaRequest {
// @public
export interface RouteConfigOptions {
- authRequired?: boolean;
+ authRequired?: boolean | 'optional';
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
tags?: readonly string[];
xsrfRequired?: Method extends 'get' ? never : boolean;
diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index 35ac4e27f9c8b..8ed64f004c9be 100644
--- a/src/dev/storybook/aliases.ts
+++ b/src/dev/storybook/aliases.ts
@@ -25,4 +25,5 @@ export const storybookAliases = {
embeddable: 'src/plugins/embeddable/scripts/storybook.js',
infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js',
siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js',
+ ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js',
};
diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts
index 24dd1c4944bfb..bb954cb887ef3 100644
--- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts
+++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts
@@ -38,7 +38,7 @@ import {
} from '../../../../../../plugins/data/public';
import { buildTabularInspectorData } from './build_tabular_inspector_data';
-import { calculateObjectHash } from '../../../../visualizations/public';
+import { calculateObjectHash } from '../../../../../../plugins/kibana_utils/common';
import { tabifyAggResponse } from '../../../../../core_plugins/data/public';
import { PersistedState } from '../../../../../../plugins/visualizations/public';
import { Adapters } from '../../../../../../plugins/inspector/public';
diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts
index 1bdff06b3a59f..dae6c9abb625e 100644
--- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts
+++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts
@@ -22,8 +22,9 @@ import { i18n } from '@kbn/i18n';
import { createInputControlVisController } from './vis_controller';
import { getControlsTab } from './components/editor/controls_tab';
import { OptionsTab } from './components/editor/options_tab';
-import { Status, defaultFeedbackMessage } from '../../visualizations/public';
+import { Status } from '../../visualizations/public';
import { InputControlVisDependencies } from './plugin';
+import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common';
export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) {
const InputControlVisController = createInputControlVisController(deps);
diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/legacy/core_plugins/input_control_vis/public/plugin.ts
index e9ffad8b35f21..e85ccd94f9e6a 100644
--- a/src/legacy/core_plugins/input_control_vis/public/plugin.ts
+++ b/src/legacy/core_plugins/input_control_vis/public/plugin.ts
@@ -59,7 +59,7 @@ export class InputControlVisPlugin implements Plugin, void> {
};
expressions.registerFunction(createInputControlVisFn);
- visualizations.types.createBaseVisualization(
+ visualizations.createBaseVisualization(
createInputControlVisTypeDefinition(visualizationDependencies)
);
}
diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts
index 91b5c7f13dc95..7fa5183a4f54b 100644
--- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts
+++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts
@@ -50,7 +50,6 @@ export function setServices(newServices: any) {
// EXPORT legacy static dependencies, should be migrated when available in a new version;
export { angular };
export { wrapInI18nContext } from 'ui/i18n';
-export { buildVislibDimensions } from '../../../visualizations/public';
export { getRequestInspectorStats, getResponseInspectorStats } from '../../../data/public';
// @ts-ignore
export { intervalOptions } from 'ui/agg_types';
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js
index fb4158a6e3e03..81c10798936f5 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js
@@ -45,7 +45,6 @@ import { getPainlessError } from './get_painless_error';
import { discoverResponseHandler } from './response_handler';
import {
angular,
- buildVislibDimensions,
getRequestInspectorStats,
getResponseInspectorStats,
getServices,
@@ -76,6 +75,7 @@ const {
import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs';
import {
esFilters,
+ fieldFormats,
indexPatterns as indexPatternsUtils,
} from '../../../../../../../plugins/data/public';
import { getIndexPatternId } from '../helpers/get_index_pattern_id';
@@ -812,21 +812,45 @@ function discoverController(
$fetchObservable.next();
};
+ function getDimensions(aggs, timeRange) {
+ const [metric, agg] = aggs;
+ agg.params.timeRange = timeRange;
+ const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null;
+ agg.buckets.setBounds(bounds);
+
+ const { esUnit, esValue } = agg.buckets.getInterval();
+ return {
+ x: {
+ accessor: 0,
+ label: agg.makeLabel(),
+ format: fieldFormats.serialize(agg),
+ params: {
+ date: true,
+ interval: moment.duration(esValue, esUnit),
+ intervalESValue: esValue,
+ intervalESUnit: esUnit,
+ format: agg.buckets.getScaledDateFormat(),
+ bounds: agg.buckets.getBounds(),
+ },
+ },
+ y: {
+ accessor: 1,
+ format: fieldFormats.serialize(metric),
+ label: metric.makeLabel(),
+ },
+ };
+ }
+
function onResults(resp) {
logInspectorResponse(resp);
if ($scope.opts.timefield) {
const tabifiedData = tabifyAggResponse($scope.vis.aggs, resp);
$scope.searchSource.rawResponse = resp;
- Promise.resolve(
- buildVislibDimensions($scope.vis, {
- timefilter,
- timeRange: $scope.timeRange,
- searchSource: $scope.searchSource,
- })
- ).then(resp => {
- $scope.histogramData = discoverResponseHandler(tabifiedData, resp);
- });
+ $scope.histogramData = discoverResponseHandler(
+ tabifiedData,
+ getDimensions($scope.vis.aggs.aggs, $scope.timeRange)
+ );
}
$scope.hits = resp.hits.total;
@@ -993,7 +1017,7 @@ function discoverController(
},
};
- $scope.vis = new visualizations.Vis(
+ $scope.vis = visualizations.createVis(
$scope.searchSource.getField('index'),
visSavedObject.visState
);
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts
index 8dbf3cd79ccb1..7ea1863693e0d 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts
@@ -29,7 +29,7 @@ import { getServices } from '../../../../kibana_services';
function getMapsAppBaseUrl() {
const mapsAppVisAlias = getServices()
- .visualizations.types.getAliases()
+ .visualizations.getAliases()
.find(({ name }) => {
return name === 'maps';
});
@@ -38,7 +38,7 @@ function getMapsAppBaseUrl() {
export function isMapsAppRegistered() {
return getServices()
- .visualizations.types.getAliases()
+ .visualizations.getAliases()
.some(({ name }) => {
return name === 'maps';
});
diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts
index 604575a6e6220..8e73a09480c41 100644
--- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts
+++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { npStart } from 'ui/new_platform';
import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public';
import { createSavedDashboardLoader } from '../dashboard';
-import { TypesService, createSavedVisLoader } from '../../../visualizations/public';
+import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy';
import { createSavedSearchesLoader } from '../../../../../plugins/discover/public';
/**
@@ -58,10 +58,7 @@ const services = {
savedObjectManagementRegistry.register({
id: 'savedVisualizations',
- service: createSavedVisLoader({
- ...services,
- ...{ visualizationTypes: new TypesService().start() },
- }),
+ service: visualizations.savedVisualizationsLoader,
title: 'visualizations',
});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts
index cd7c8278adcc7..5a8460fcb51ba 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts
+++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts
@@ -19,7 +19,8 @@
import { getIndices } from './get_indices';
import { IndexPatternCreationConfig } from './../../../../../../../management/public';
-import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public/search';
export const successfulResponse = {
hits: {
diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts
index b8ee7cd378750..66a7bd6f33373 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts
@@ -24,17 +24,8 @@
* directly where they are needed.
*/
-export { State } from 'ui/state_management/state';
-// @ts-ignore
-export { GlobalStateProvider } from 'ui/state_management/global_state';
-// @ts-ignore
-export { StateManagementConfigProvider } from 'ui/state_management/config_provider';
-
export { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
// @ts-ignore
-export { EventsProvider } from 'ui/events';
-export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router';
-// @ts-ignore
export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url';
export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';
export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url';
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts
index b15d89275eba7..8ef63ec5778e2 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts
@@ -23,13 +23,11 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular';
import { AppMountContext } from 'kibana/public';
import {
configureAppAngularModule,
- GlobalStateProvider,
KbnUrlProvider,
RedirectWhenMissingProvider,
IPrivate,
PrivateProvider,
PromiseServiceCreator,
- StateManagementConfigProvider,
} from '../legacy_imports';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public';
import {
@@ -87,35 +85,20 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav
createLocalI18nModule();
createLocalPrivateModule();
createLocalPromiseModule();
- createLocalConfigModule(core);
createLocalKbnUrlModule();
- createLocalStateModule();
createLocalTopNavModule(navigation);
const visualizeAngularModule: IModule = angular.module(moduleName, [
...thirdPartyAngularDependencies,
- 'app/visualize/Config',
'app/visualize/I18n',
'app/visualize/Private',
'app/visualize/TopNav',
- 'app/visualize/State',
+ 'app/visualize/KbnUrl',
+ 'app/visualize/Promise',
]);
return visualizeAngularModule;
}
-function createLocalStateModule() {
- angular
- .module('app/visualize/State', [
- 'app/visualize/Private',
- 'app/visualize/Config',
- 'app/visualize/KbnUrl',
- 'app/visualize/Promise',
- ])
- .service('globalState', function(Private: IPrivate) {
- return Private(GlobalStateProvider);
- });
-}
-
function createLocalKbnUrlModule() {
angular
.module('app/visualize/KbnUrl', ['app/visualize/Private', 'ngRoute'])
@@ -123,19 +106,6 @@ function createLocalKbnUrlModule() {
.service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider));
}
-function createLocalConfigModule(core: AppMountContext['core']) {
- angular
- .module('app/visualize/Config', ['app/visualize/Private'])
- .provider('stateManagementConfig', StateManagementConfigProvider)
- .provider('config', () => {
- return {
- $get: () => ({
- get: core.uiSettings.get.bind(core.uiSettings),
- }),
- };
- });
-}
-
function createLocalPromiseModule() {
angular.module('app/visualize/Promise', []).service('Promise', PromiseServiceCreator);
}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html
index 9dbb05ea95b48..28baf21925cbe 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html
@@ -31,8 +31,8 @@
refresh-interval="refreshInterval.value"
on-refresh-change="onRefreshChange"
show-save-query="showSaveQuery"
- on-saved="onQuerySaved"
- on-saved-query-updated="onSavedQueryUpdated"
+ on-saved="updateSavedQuery"
+ on-saved-query-updated="updateSavedQuery"
on-clear-saved-query="onClearSavedQuery"
>
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js
index 2d2552b5e2f30..e1a20e3381331 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js
@@ -20,6 +20,7 @@
import angular from 'angular';
import _ from 'lodash';
import { Subscription } from 'rxjs';
+import { map } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import React from 'react';
@@ -29,13 +30,17 @@ import { VisualizeConstants } from '../visualize_constants';
import { getEditBreadcrumbs } from '../breadcrumbs';
import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util';
-import { FilterStateManager } from '../../../../../data/public';
import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public';
import { kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public';
import {
SavedObjectSaveModal,
showSaveModal,
} from '../../../../../../../plugins/saved_objects/public';
+import {
+ esFilters,
+ connectToQueryState,
+ syncQueryStateWithUrl,
+} from '../../../../../../../plugins/data/public';
import { initVisEditorDirective } from './visualization_editor';
import { initVisualizationDirective } from './visualization';
@@ -65,28 +70,21 @@ export function initEditorDirective(app, deps) {
function VisualizeAppController(
$scope,
- $element,
$route,
$window,
$injector,
$timeout,
kbnUrl,
redirectWhenMissing,
- Promise,
- globalState,
- config
+ kbnUrlStateStorage,
+ history
) {
const {
indexPatterns,
localStorage,
visualizeCapabilities,
share,
- data: {
- query: {
- filterManager,
- timefilter: { timefilter },
- },
- },
+ data: { query: queryService },
toastNotifications,
chrome,
getBasePath,
@@ -97,6 +95,17 @@ function VisualizeAppController(
setActiveUrl,
} = getServices();
+ const {
+ filterManager,
+ timefilter: { timefilter },
+ } = queryService;
+
+ // starts syncing `_g` portion of url with query services
+ const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl(
+ queryService,
+ kbnUrlStateStorage
+ );
+
// Retrieve the resolved SavedVis instance.
const savedVis = $route.current.locals.savedVis;
const _applyVis = () => {
@@ -284,26 +293,24 @@ function VisualizeAppController(
linked: !!savedVis.savedSearchId,
};
- const useHash = config.get('state:storeInSessionStorage');
const { stateContainer, stopStateSync } = useVisualizeAppState({
- useHash,
stateDefaults,
+ kbnUrlStateStorage,
});
- const filterStateManager = new FilterStateManager(
- globalState,
- () => {
- // Temporary AppState replacement
- return {
- set filters(_filters) {
- stateContainer.transitions.set('filters', _filters);
- },
- get filters() {
- return stateContainer.getState().filters;
- },
- };
+ // sync initial app filters from state to filterManager
+ filterManager.setAppFilters(_.cloneDeep(stateContainer.getState().filters));
+ // setup syncing of app filters between appState and filterManager
+ const stopSyncingAppFilters = connectToQueryState(
+ queryService,
+ {
+ set: ({ filters }) => stateContainer.transitions.set('filters', filters),
+ get: () => ({ filters: stateContainer.getState().filters }),
+ state$: stateContainer.state$.pipe(map(state => ({ filters: state.filters }))),
},
- filterManager
+ {
+ filters: esFilters.FilterStateStore.APP_STATE,
+ }
);
// The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the
@@ -335,6 +342,24 @@ function VisualizeAppController(
}
);
+ const updateSavedQueryFromUrl = savedQueryId => {
+ if (!savedQueryId) {
+ delete $scope.savedQuery;
+
+ return;
+ }
+
+ if ($scope.savedQuery && $scope.savedQuery.id === savedQueryId) {
+ return;
+ }
+
+ savedQueryService.getSavedQuery(savedQueryId).then(savedQuery => {
+ $scope.$evalAsync(() => {
+ $scope.updateSavedQuery(savedQuery);
+ });
+ });
+ };
+
function init() {
if (vis.indexPattern) {
$scope.indexPattern = vis.indexPattern;
@@ -388,7 +413,6 @@ function VisualizeAppController(
};
$scope.timeRange = timefilter.getTime();
- $scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode');
const unsubscribeStateUpdates = stateContainer.subscribe(state => {
const newQuery = migrateLegacyQuery(state.query);
@@ -396,6 +420,7 @@ function VisualizeAppController(
stateContainer.transitions.set('query', newQuery);
}
persistOnChange(state);
+ updateSavedQueryFromUrl(state.savedQuery);
// if the browser history was changed manually we need to reflect changes in the editor
if (!_.isEqual(vis.getState(), state.vis)) {
@@ -413,6 +438,9 @@ function VisualizeAppController(
$scope.$broadcast('render');
};
+ // update the query if savedQuery is stored
+ updateSavedQueryFromUrl(initialState.savedQuery);
+
const subscriptions = new Subscription();
subscriptions.add(
@@ -438,7 +466,7 @@ function VisualizeAppController(
// update the searchSource when query updates
$scope.fetch = function() {
- const { query, filters, linked } = stateContainer.getState();
+ const { query, linked, filters } = stateContainer.getState();
$scope.query = query;
$scope.linked = linked;
savedVis.searchSource.setField('query', query);
@@ -451,7 +479,6 @@ function VisualizeAppController(
subscribeWithScope($scope, filterManager.getUpdates$(), {
next: () => {
$scope.filters = filterManager.getFilters();
- $scope.globalFilters = filterManager.getGlobalFilters();
},
})
);
@@ -466,13 +493,14 @@ function VisualizeAppController(
$scope._handler.destroy();
}
savedVis.destroy();
- filterStateManager.destroy();
subscriptions.unsubscribe();
$scope.vis.off('apply', _applyVis);
unsubscribePersisted();
unsubscribeStateUpdates();
stopStateSync();
+ stopSyncingQueryServiceStateWithUrl();
+ stopSyncingAppFilters();
});
$timeout(() => {
@@ -501,23 +529,14 @@ function VisualizeAppController(
});
};
- $scope.onQuerySaved = savedQuery => {
- $scope.savedQuery = savedQuery;
- };
-
- $scope.onSavedQueryUpdated = savedQuery => {
- $scope.savedQuery = { ...savedQuery };
- };
-
$scope.onClearSavedQuery = () => {
delete $scope.savedQuery;
stateContainer.transitions.removeSavedQuery(defaultQuery);
filterManager.setFilters(filterManager.getGlobalFilters());
- $scope.fetch();
};
const updateStateFromSavedQuery = savedQuery => {
- stateContainer.transitions.set('query', savedQuery.attributes.query);
+ stateContainer.transitions.updateFromSavedQuery(savedQuery);
const savedQueryFilters = savedQuery.attributes.filters || [];
const globalFilters = filterManager.getGlobalFilters();
@@ -532,25 +551,12 @@ function VisualizeAppController(
timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval);
}
}
-
- $scope.fetch();
};
- // update the query if savedQuery is stored
- if (stateContainer.getState().savedQuery) {
- savedQueryService.getSavedQuery(stateContainer.getState().savedQuery).then(savedQuery => {
- $scope.$evalAsync(() => {
- $scope.savedQuery = savedQuery;
- });
- });
- }
-
- $scope.$watch('savedQuery', newSavedQuery => {
- if (!newSavedQuery) return;
- stateContainer.transitions.set('savedQuery', newSavedQuery.id);
-
- updateStateFromSavedQuery(newSavedQuery);
- });
+ $scope.updateSavedQuery = savedQuery => {
+ $scope.savedQuery = savedQuery;
+ updateStateFromSavedQuery(savedQuery);
+ };
$scope.$watch('linked', linked => {
if (linked && !savedVis.savedSearchId) {
@@ -626,7 +632,10 @@ function VisualizeAppController(
savedVis.vis.title = savedVis.title;
savedVis.vis.description = savedVis.description;
} else {
- kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id });
+ history.replace({
+ ...history.location,
+ pathname: `${VisualizeConstants.EDIT_PATH}/${savedVis.id}`,
+ });
}
}
});
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts
index d8de81193d857..d3fae3d457b63 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts
@@ -17,21 +17,20 @@
* under the License.
*/
-import { createHashHistory } from 'history';
import { isFunction, omit } from 'lodash';
import { migrateAppState } from './migrate_app_state';
import {
- createKbnUrlStateStorage,
createStateContainer,
syncState,
+ IKbnUrlStateStorage,
} from '../../../../../../../../plugins/kibana_utils/public';
import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types';
const STATE_STORAGE_KEY = '_a';
interface Arguments {
- useHash: boolean;
+ kbnUrlStateStorage: IKbnUrlStateStorage;
stateDefaults: VisualizeAppState;
}
@@ -41,12 +40,7 @@ function toObject(state: PureVisState): PureVisState {
});
}
-export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) {
- const history = createHashHistory();
- const kbnUrlStateStorage = createKbnUrlStateStorage({
- useHash,
- history,
- });
+export function useVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) {
const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY);
const initialState = migrateAppState({
...stateDefaults,
@@ -88,6 +82,11 @@ export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) {
linked: false,
}),
updateVisState: state => newVisState => ({ ...state, vis: toObject(newVisState) }),
+ updateFromSavedQuery: state => savedQuery => ({
+ ...state,
+ savedQuery: savedQuery.id,
+ query: savedQuery.attributes.query,
+ }),
}
);
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/global_state_sync.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/global_state_sync.ts
deleted file mode 100644
index f29fb72a9fbc5..0000000000000
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/global_state_sync.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { State } from '../legacy_imports';
-import { DataPublicPluginStart as DataStart } from '../../../../../../plugins/data/public';
-
-/**
- * Helper function to sync the global state with the various state providers
- * when a local angular application mounts. There are three different ways
- * global state can be passed into the application:
- * * parameter in the URL hash - e.g. shared link
- * * in-memory state in the data plugin exports (timefilter and filterManager) - e.g. default values
- *
- * This function looks up the three sources (earlier in the list means it takes precedence),
- * puts it into the globalState object and syncs it with the url.
- *
- * Currently the legacy chrome takes care of restoring the global state when navigating from
- * one app to another - to migrate away from that it will become necessary to also write the current
- * state to local storage
- */
-export function syncOnMount(
- globalState: State,
- {
- query: {
- filterManager,
- timefilter: { timefilter },
- },
- }: DataStart
-) {
- // pull in global state information from the URL
- globalState.fetch();
- // remember whether there were info in the URL
- const hasGlobalURLState = Boolean(Object.keys(globalState.toObject()).length);
-
- // sync kibana platform state with the angular global state
- if (!globalState.time) {
- globalState.time = timefilter.getTime();
- }
- if (!globalState.refreshInterval) {
- globalState.refreshInterval = timefilter.getRefreshInterval();
- }
- if (!globalState.filters && filterManager.getGlobalFilters().length > 0) {
- globalState.filters = filterManager.getGlobalFilters();
- }
- // only inject cross app global state if there is none in the url itself (that takes precedence)
- if (hasGlobalURLState) {
- // set flag the global state is set from the URL
- globalState.$inheritedGlobalState = true;
- }
- globalState.save();
-}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js
index 24055b9a2d9ed..b9409445166bc 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js
@@ -19,6 +19,9 @@
import { find } from 'lodash';
import { i18n } from '@kbn/i18n';
+import { createHashHistory } from 'history';
+
+import { createKbnUrlStateStorage } from '../../../../../../plugins/kibana_utils/public';
import editorTemplate from './editor/editor.html';
import visualizeListingTemplate from './listing/visualize_listing.html';
@@ -26,11 +29,7 @@ import visualizeListingTemplate from './listing/visualize_listing.html';
import { initVisualizeAppDirective } from './visualize_app';
import { VisualizeConstants } from './visualize_constants';
import { VisualizeListingController } from './listing/visualize_listing';
-import {
- ensureDefaultIndexPattern,
- registerTimefilterWithGlobalStateFactory,
-} from '../legacy_imports';
-import { syncOnMount } from './global_state_sync';
+import { ensureDefaultIndexPattern } from '../legacy_imports';
import {
getLandingBreadcrumbs,
@@ -42,17 +41,13 @@ import {
export function initVisualizeApp(app, deps) {
initVisualizeAppDirective(app, deps);
- app.run(globalState => {
- syncOnMount(globalState, deps.data);
- });
-
- app.run((globalState, $rootScope) => {
- registerTimefilterWithGlobalStateFactory(
- deps.data.query.timefilter.timefilter,
- globalState,
- $rootScope
- );
- });
+ app.factory('history', () => createHashHistory());
+ app.factory('kbnUrlStateStorage', history =>
+ createKbnUrlStateStorage({
+ history,
+ useHash: deps.uiSettings.get('state:storeInSessionStorage'),
+ })
+ );
app.config(function($routeProvider) {
const defaults = {
@@ -107,7 +102,7 @@ export function initVisualizeApp(app, deps) {
resolve: {
savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) {
const { core, data, savedVisualizations, visualizations } = deps;
- const visTypes = visualizations.types.all();
+ const visTypes = visualizations.all();
const visType = find(visTypes, { name: $route.current.params.type });
const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection;
const hasIndex =
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js
index c0cc499b598f0..5a479a491395a 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js
@@ -26,23 +26,21 @@ import { i18n } from '@kbn/i18n';
import { getServices } from '../../kibana_services';
import { wrapInI18nContext } from '../../legacy_imports';
+import { syncQueryStateWithUrl } from '../../../../../../../plugins/data/public';
+
export function initListingDirective(app) {
app.directive('visualizeListingTable', reactDirective =>
reactDirective(wrapInI18nContext(VisualizeListingTable))
);
}
-export function VisualizeListingController($injector, $scope, createNewVis) {
+export function VisualizeListingController($injector, $scope, createNewVis, kbnUrlStateStorage) {
const {
addBasePath,
chrome,
savedObjectsClient,
savedVisualizations,
- data: {
- query: {
- timefilter: { timefilter },
- },
- },
+ data: { query },
toastNotifications,
uiSettings,
visualizations,
@@ -50,6 +48,16 @@ export function VisualizeListingController($injector, $scope, createNewVis) {
} = getServices();
const kbnUrl = $injector.get('kbnUrl');
+ // syncs `_g` portion of url with query services
+ const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl(
+ query,
+ kbnUrlStateStorage
+ );
+
+ const {
+ timefilter: { timefilter },
+ } = query;
+
timefilter.disableAutoRefreshSelector();
timefilter.disableTimeRangeSelector();
@@ -124,5 +132,7 @@ export function VisualizeListingController($injector, $scope, createNewVis) {
if (this.closeNewVisModal) {
this.closeNewVisModal();
}
+
+ stopSyncingQueryServiceStateWithUrl();
});
}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts
index 8ca603eb11459..55fccd75361a0 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts
@@ -17,7 +17,13 @@
* under the License.
*/
-import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public';
+import {
+ TimeRange,
+ Query,
+ Filter,
+ DataPublicPluginStart,
+ SavedQuery,
+} from 'src/plugins/data/public';
import { IEmbeddableStart } from 'src/plugins/embeddable/public';
import { PersistedState } from 'src/plugins/visualizations/public';
import { LegacyCoreStart } from 'kibana/public';
@@ -48,6 +54,7 @@ export interface VisualizeAppStateTransitions {
state: VisualizeAppState
) => (query: Query, filters: Filter[]) => VisualizeAppState;
updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState;
+ updateFromSavedQuery: (state: VisualizeAppState) => (savedQuery: SavedQuery) => VisualizeAppState;
}
export interface EditorRenderProps {
diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js
index f11aab9b9db88..6bdb5d00e67d8 100644
--- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js
+++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js
@@ -111,9 +111,7 @@ describe('RegionMapsVisualizationTests', function() {
if (!visRegComplete) {
visRegComplete = true;
- visualizationsSetup.types.createBaseVisualization(
- createRegionMapTypeDefinition(dependencies)
- );
+ visualizationsSetup.createBaseVisualization(createRegionMapTypeDefinition(dependencies));
}
RegionMapsVisualization = createRegionMapVisualization(dependencies);
@@ -160,7 +158,7 @@ describe('RegionMapsVisualizationTests', function() {
imageComparator = new ImageComparator();
- vis = new visualizationsStart.Vis(indexPattern, {
+ vis = visualizationsStart.createVis(indexPattern, {
type: 'region_map',
});
diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts
index aaf0a8a308aea..98fb5604c3d65 100644
--- a/src/legacy/core_plugins/region_map/public/plugin.ts
+++ b/src/legacy/core_plugins/region_map/public/plugin.ts
@@ -70,7 +70,7 @@ export class RegionMapPlugin implements Plugin, void> {
expressions.registerFunction(createRegionMapFn);
- visualizations.types.createBaseVisualization(
+ visualizations.createBaseVisualization(
createRegionMapTypeDefinition(visualizationDependencies)
);
}
diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js
index 27e9459c7e06c..6a08405b5b6a5 100644
--- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js
+++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js
@@ -88,9 +88,7 @@ describe('CoordinateMapsVisualizationTest', function() {
if (!visRegComplete) {
visRegComplete = true;
- visualizationsSetup.types.createBaseVisualization(
- createTileMapTypeDefinition(dependencies)
- );
+ visualizationsSetup.createBaseVisualization(createTileMapTypeDefinition(dependencies));
}
CoordinateMapsVisualization = createTileMapVisualization(dependencies);
@@ -126,7 +124,7 @@ describe('CoordinateMapsVisualizationTest', function() {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
- vis = new visualizationsStart.Vis(indexPattern, {
+ vis = visualizationsStart.createVis(indexPattern, {
type: 'tile_map',
});
vis.params = {
diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts
index 52acaf51b39b1..a12c2753cc525 100644
--- a/src/legacy/core_plugins/tile_map/public/plugin.ts
+++ b/src/legacy/core_plugins/tile_map/public/plugin.ts
@@ -64,9 +64,7 @@ export class TileMapPlugin implements Plugin, void> {
expressions.registerFunction(() => createTileMapFn(visualizationDependencies));
- visualizations.types.createBaseVisualization(
- createTileMapTypeDefinition(visualizationDependencies)
- );
+ visualizations.createBaseVisualization(createTileMapTypeDefinition(visualizationDependencies));
}
public start(core: CoreStart) {
diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js
index e4a48c09db832..a9d678cfea79c 100644
--- a/src/legacy/core_plugins/timelion/public/app.js
+++ b/src/legacy/core_plugins/timelion/public/app.js
@@ -42,7 +42,7 @@ import '../../data/public/legacy';
import './services/saved_sheet_register';
import rootTemplate from 'plugins/timelion/index.html';
-import { createSavedVisLoader, TypesService } from '../../visualizations/public';
+import { start as visualizations } from '../../visualizations/public/np_ready/public/legacy';
import { loadKbnTopNavDirectives } from '../../../../plugins/kibana_legacy/public';
loadKbnTopNavDirectives(npStart.plugins.navigation.ui);
@@ -127,13 +127,7 @@ app.controller('timelion', function(
timefilter.enableAutoRefreshSelector();
timefilter.enableTimeRangeSelector();
- const savedVisualizations = createSavedVisLoader({
- savedObjectsClient: npStart.core.savedObjects.client,
- indexPatterns: npStart.plugins.data.indexPatterns,
- chrome: npStart.core.chrome,
- overlays: npStart.core.overlays,
- visualizationTypes: new TypesService().start(),
- });
+ const savedVisualizations = visualizations.savedVisualizationsLoader;
const timezone = Private(timezoneProvider)();
const defaultExpression = '.es(*)';
diff --git a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts
index f131664756202..71d6c1c69ef2d 100644
--- a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts
@@ -39,7 +39,7 @@ export class MarkdownPlugin implements Plugin {
}
public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) {
- visualizations.types.createReactVisualization(markdownVisDefinition);
+ visualizations.createReactVisualization(markdownVisDefinition);
expressions.registerFunction(createMarkdownVisFn);
}
diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts
index 67b5d018f4638..5dbd59f3f1709 100644
--- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts
+++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts
@@ -40,7 +40,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => {
let vis: Vis;
beforeAll(() => {
- visualizationsSetup.types.createReactVisualization(createMetricVisTypeDefinition());
+ visualizationsSetup.createReactVisualization(createMetricVisTypeDefinition());
(npStart.plugins.data.fieldFormats.getType as jest.Mock).mockImplementation(() => {
return fieldFormats.UrlFormat;
});
@@ -59,7 +59,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => {
// TODO: remove when Vis is converted to typescript. Only importing Vis as type
// @ts-ignore
- vis = new visualizationsStart.Vis(stubIndexPattern, {
+ vis = visualizationsStart.createVis(stubIndexPattern, {
type: 'metric',
aggs: [{ id: '1', type: 'top_hits', schema: 'metric', params: { field: 'ip' } }],
});
@@ -80,7 +80,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => {
};
const el = document.createElement('div');
- const metricVisType = visualizationsStart.types.get('metric');
+ const metricVisType = visualizationsStart.get('metric');
const Controller = metricVisType.visualization;
const controller = new Controller(el, vis);
const render = (esResponse: any) => {
diff --git a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts
index 082fab47e573c..28b435cbc7980 100644
--- a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts
@@ -45,7 +45,7 @@ export class MetricVisPlugin implements Plugin {
{ expressions, visualizations, charts }: MetricVisPluginSetupDependencies
) {
expressions.registerFunction(createMetricVisFn);
- visualizations.types.createReactVisualization(createMetricVisTypeDefinition());
+ visualizations.createReactVisualization(createMetricVisTypeDefinition());
}
public start(core: CoreStart) {
diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js
index 9fe7920588cd2..91581923b05cb 100644
--- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js
+++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js
@@ -47,10 +47,10 @@ describe('Table Vis - AggTable Directive', function() {
const tabifiedData = {};
const init = () => {
- const vis1 = new visualizationsStart.Vis(indexPattern, 'table');
+ const vis1 = visualizationsStart.createVis(indexPattern, 'table');
tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly);
- const vis2 = new visualizationsStart.Vis(indexPattern, {
+ const vis2 = visualizationsStart.createVis(indexPattern, {
type: 'table',
params: {
showMetricsAtAllLevels: true,
@@ -69,7 +69,7 @@ describe('Table Vis - AggTable Directive', function() {
metricsAtAllLevels: true,
});
- const vis3 = new visualizationsStart.Vis(indexPattern, {
+ const vis3 = visualizationsStart.createVis(indexPattern, {
type: 'table',
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
@@ -110,7 +110,7 @@ describe('Table Vis - AggTable Directive', function() {
beforeEach(initLocalAngular);
ngMock.inject(function() {
- visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition);
+ visualizationsSetup.createBaseVisualization(tableVisTypeDefinition);
});
beforeEach(ngMock.module('kibana/table_vis'));
diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js
index 79d4d7c40d355..4d62551dcf396 100644
--- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js
+++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js
@@ -35,10 +35,10 @@ describe('Table Vis - AggTableGroup Directive', function() {
const tabifiedData = {};
const init = () => {
- const vis1 = new visualizationsStart.Vis(indexPattern, 'table');
+ const vis1 = visualizationsStart.createVis(indexPattern, 'table');
tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly);
- const vis2 = new visualizationsStart.Vis(indexPattern, {
+ const vis2 = visualizationsStart.createVis(indexPattern, {
type: 'pie',
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
diff --git a/src/legacy/core_plugins/vis_type_table/public/plugin.ts b/src/legacy/core_plugins/vis_type_table/public/plugin.ts
index 17c50b0567b67..519a56da23ac9 100644
--- a/src/legacy/core_plugins/vis_type_table/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_table/public/plugin.ts
@@ -44,7 +44,7 @@ export class TableVisPlugin implements Plugin, void> {
) {
expressions.registerFunction(createTableVisFn);
- visualizations.types.createBaseVisualization(tableVisTypeDefinition);
+ visualizations.createBaseVisualization(tableVisTypeDefinition);
}
public start(core: CoreStart) {
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js
index 55ecf98f994d2..3091b3340cd6d 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js
+++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js
@@ -76,7 +76,7 @@ describe('TagCloudVisualizationTest', function() {
beforeEach(async function() {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
- vis = new visualizationsStart.Vis(indexPattern, {
+ vis = visualizationsStart.createVis(indexPattern, {
type: 'tagcloud',
params: {
bucket: { accessor: 0, format: {} },
diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts
index 9e5940eca1598..8244cba38edc3 100644
--- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts
@@ -53,7 +53,7 @@ export class TagCloudPlugin implements Plugin {
colors: charts.colors,
};
expressions.registerFunction(createTagCloudFn);
- visualizations.types.createBaseVisualization(
+ visualizations.createBaseVisualization(
createTagCloudVisTypeDefinition(visualizationDependencies)
);
}
diff --git a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts b/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts
index 69a2ad3c1351a..9d69c312b48f4 100644
--- a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts
@@ -66,7 +66,7 @@ export class TimelionVisPlugin implements Plugin {
};
expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies));
- visualizations.types.createReactVisualization(getTimelionVisDefinition(dependencies));
+ visualizations.createReactVisualization(getTimelionVisDefinition(dependencies));
}
public start(core: CoreStart, plugins: PluginsStart) {
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts
index 135cc1e181432..30c62d778933b 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts
@@ -25,7 +25,7 @@ import { metricsRequestHandler } from './request_handler';
import { EditorController } from './editor_controller';
// @ts-ignore
import { PANEL_TYPES } from '../../../../plugins/vis_type_timeseries/common/panel_types';
-import { defaultFeedbackMessage } from '../../visualizations/public';
+import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common';
export const metricsVisDefinition = {
name: 'metrics',
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts
index 38a9c68487854..441b1f05ea78c 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts
@@ -57,7 +57,7 @@ export class MetricsPlugin implements Plugin, void> {
) {
expressions.registerFunction(createMetricsFn);
setUISettings(core.uiSettings);
- visualizations.types.createReactVisualization(metricsVisDefinition);
+ visualizations.createReactVisualization(metricsVisDefinition);
}
public start(core: CoreStart, { data }: MetricsPluginStartDependencies) {
diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js
index 378590af29d3a..5befc09b24544 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js
+++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js
@@ -93,7 +93,7 @@ describe('VegaVisualizations', () => {
if (!visRegComplete) {
visRegComplete = true;
- visualizationsSetup.types.createBaseVisualization(
+ visualizationsSetup.createBaseVisualization(
createVegaTypeDefinition(vegaVisualizationDependencies)
);
}
@@ -108,7 +108,7 @@ describe('VegaVisualizations', () => {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
- vis = new visualizationsStart.Vis(indexPattern, { type: 'vega' });
+ vis = visualizationsStart.createVis(indexPattern, { type: 'vega' });
});
afterEach(function() {
diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts
index b354433330caf..3b01d9ceca5a6 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts
@@ -84,9 +84,7 @@ export class VegaPlugin implements Plugin, void> {
expressions.registerFunction(() => createVegaFn(visualizationDependencies));
- visualizations.types.createBaseVisualization(
- createVegaTypeDefinition(visualizationDependencies)
- );
+ visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies));
}
public start(core: CoreStart, { data }: VegaPluginStartDependencies) {
diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts
index a84948f725e0a..78f9c170ab62d 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts
+++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts
@@ -19,10 +19,11 @@
import { i18n } from '@kbn/i18n';
// @ts-ignore
-import { Status, defaultFeedbackMessage } from '../../visualizations/public';
+import { Status } from '../../visualizations/public';
import { DefaultEditorSize } from '../../vis_default_editor/public';
import { VegaVisualizationDependencies } from './plugin';
import { VegaVisEditor } from './components';
+import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common';
import { createVegaRequestHandler } from './vega_request_handler';
// @ts-ignore
diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts
index 8a7196a61ecec..a71892cc47b05 100644
--- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts
@@ -97,14 +97,14 @@ export class VisTypeVislibPlugin implements Plugin, void> {
// Register legacy vislib types that have been converted
convertedFns.forEach(expressions.registerFunction);
convertedTypes.forEach(vis =>
- visualizations.types.createBaseVisualization(vis(visualizationDependencies))
+ visualizations.createBaseVisualization(vis(visualizationDependencies))
);
}
// Register non-converted types
vislibFns.forEach(expressions.registerFunction);
vislibTypes.forEach(vis =>
- visualizations.types.createBaseVisualization(vis(visualizationDependencies))
+ visualizations.createBaseVisualization(vis(visualizationDependencies))
);
}
diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js
index 9c9c5a84f046c..43e3b987f1962 100644
--- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js
+++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js
@@ -133,7 +133,7 @@ describe('No global chart settings', function() {
responseHandler = vislibSlicesResponseHandler;
let id1 = 1;
- stubVis1 = new visualizationsStart.Vis(indexPattern, {
+ stubVis1 = visualizationsStart.createVis(indexPattern, {
type: 'pie',
aggs: rowAgg,
});
@@ -222,7 +222,7 @@ describe('Vislib PieChart Class Test Suite', function() {
responseHandler = vislibSlicesResponseHandler;
let id = 1;
- stubVis = new visualizationsStart.Vis(indexPattern, {
+ stubVis = visualizationsStart.createVis(indexPattern, {
type: 'pie',
aggs: dataAgg,
});
diff --git a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts
index 59bb64b337256..35abb04fd8732 100644
--- a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts
+++ b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts
@@ -72,7 +72,7 @@ export class VisTypeXyPlugin implements Plugin, void> {
visFunctions.forEach((fn: any) => expressions.registerFunction(fn));
visTypeDefinitions.forEach((vis: any) =>
- visualizations.types.createBaseVisualization(vis(visualizationDependencies))
+ visualizations.createBaseVisualization(vis(visualizationDependencies))
);
}
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts
index 7688a7769cf79..b59eb2277411c 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts
@@ -43,20 +43,11 @@ export { Vis, VisParams, VisState } from './vis';
import { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable';
export type VisualizeEmbeddableFactoryContract = PublicContract;
export type VisualizeEmbeddableContract = PublicContract;
+export { TypesService } from './vis_types/types_service';
+export { Status } from './legacy/update_status'; // should remove
+export { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from './embeddable';
+export { SchemaConfig } from './legacy/build_pipeline';
export function plugin(initializerContext: PluginInitializerContext) {
return new VisualizationsPlugin(initializerContext);
}
-
-/** @public static code */
-export { TypesService } from './vis_types/types_service';
-export { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from './embeddable';
-
-export { Status } from './legacy/update_status';
-export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/build_pipeline';
-
-// @ts-ignore
-export { updateOldState } from './legacy/vis_update_state';
-export { calculateObjectHash } from './legacy/calculate_object_hash';
-export { createSavedVisLoader } from './saved_visualizations/saved_visualizations';
-export { defaultFeedbackMessage } from './misc/default_feedback_message';
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js
index 8c75ba24051b0..deb345a77cdb6 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js
@@ -43,12 +43,12 @@ describe('Vis Class', function() {
beforeEach(
ngMock.inject(function(Private) {
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
- visTypes = visualizations.types;
+ visTypes = visualizations;
})
);
beforeEach(function() {
- vis = new visualizations.Vis(indexPattern, stateFixture);
+ vis = visualizations.createVis(indexPattern, stateFixture);
});
const verifyVis = function(vis) {
@@ -84,7 +84,7 @@ describe('Vis Class', function() {
describe('setState()', function() {
it('should set the state to defaults', function() {
- const vis = new visualizations.Vis(indexPattern);
+ const vis = visualizations.createVis(indexPattern);
expect(vis).to.have.property('type');
expect(vis.type).to.eql(visTypes.get('histogram'));
expect(vis).to.have.property('aggs');
@@ -100,7 +100,7 @@ describe('Vis Class', function() {
expect(vis.isHierarchical()).to.be(true);
});
it('should return false for non-hierarchical vis (like histogram)', function() {
- const vis = new visualizations.Vis(indexPattern);
+ const vis = visualizations.createVis(indexPattern);
expect(vis.isHierarchical()).to.be(false);
});
});
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts
index d9af5122eadec..92a9ce8366f4f 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts
@@ -18,7 +18,7 @@
*/
import { PersistedState } from '../../../../../../../plugins/visualizations/public';
-import { calculateObjectHash } from './calculate_object_hash';
+import { calculateObjectHash } from '../../../../../../../plugins/kibana_utils/common';
import { Vis } from '../vis';
enum Status {
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts
index 8d7407b6191d6..9e8eac08c33ea 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts
@@ -28,23 +28,19 @@ import { usageCollectionPluginMock } from '../../../../../../plugins/usage_colle
import { uiActionsPluginMock } from '../../../../../../plugins/ui_actions/public/mocks';
const createSetupContract = (): VisualizationsSetup => ({
- types: {
- createBaseVisualization: jest.fn(),
- createReactVisualization: jest.fn(),
- registerAlias: jest.fn(),
- hideTypes: jest.fn(),
- },
+ createBaseVisualization: jest.fn(),
+ createReactVisualization: jest.fn(),
+ registerAlias: jest.fn(),
+ hideTypes: jest.fn(),
});
const createStartContract = (): VisualizationsStart => ({
- types: {
- get: jest.fn(),
- all: jest.fn(),
- getAliases: jest.fn(),
- },
+ get: jest.fn(),
+ all: jest.fn(),
+ getAliases: jest.fn(),
savedVisualizationsLoader: {} as any,
showNewVisModal: jest.fn(),
- Vis: jest.fn(),
+ createVis: jest.fn(),
});
const createInstance = async () => {
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts
index 10797a1a04df4..b8db611f30815 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts
@@ -48,27 +48,27 @@ import { visualization as visualizationRenderer } from './expressions/visualizat
import {
DataPublicPluginSetup,
DataPublicPluginStart,
+ IIndexPattern,
} from '../../../../../../plugins/data/public';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public';
import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations';
-import { VisImpl, VisImplConstructor } from './vis_impl';
+import { VisImpl } from './vis_impl';
import { showNewVisModal } from './wizard';
import { UiActionsStart } from '../../../../../../plugins/ui_actions/public';
import { DataStart as LegacyDataStart } from '../../../../data/public';
+import { VisState } from './types';
/**
* Interface for this plugin's returned setup/start contracts.
*
* @public
*/
-export interface VisualizationsSetup {
- types: TypesSetup;
-}
-export interface VisualizationsStart {
- types: TypesStart;
+export type VisualizationsSetup = TypesSetup;
+
+export interface VisualizationsStart extends TypesStart {
savedVisualizationsLoader: SavedVisualizationsLoader;
- Vis: VisImplConstructor;
+ createVis: (indexPattern: IIndexPattern, visState?: VisState) => VisImpl;
showNewVisModal: typeof showNewVisModal;
}
@@ -122,7 +122,7 @@ export class VisualizationsPlugin
embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory);
return {
- types: this.types.setup(),
+ ...this.types.setup(),
};
}
@@ -152,9 +152,15 @@ export class VisualizationsPlugin
setSavedVisualizationsLoader(savedVisualizationsLoader);
return {
- types,
+ ...types,
showNewVisModal,
- Vis: VisImpl,
+ /**
+ * creates new instance of Vis
+ * @param {IIndexPattern} indexPattern - index pattern to use
+ * @param {VisState} visState - visualization configuration
+ */
+ createVis: (indexPattern: IIndexPattern, visState?: VisState) =>
+ new VisImpl(indexPattern, visState),
savedVisualizationsLoader,
};
}
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts
index 2458ed5008ddd..e381a01edef8b 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts
@@ -29,7 +29,8 @@ import {
SavedObject,
SavedObjectKibanaServices,
} from '../../../../../../../plugins/saved_objects/public';
-import { updateOldState } from '../../../index';
+// @ts-ignore
+import { updateOldState } from '../legacy/vis_update_state';
import { extractReferences, injectReferences } from './saved_visualization_references';
import { IIndexPattern } from '../../../../../../../plugins/data/public';
import { VisSavedObject } from '../types';
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts
index 0cae83afb7861..6bcaa9a3e1dac 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts
@@ -67,15 +67,32 @@ export class TypesService {
this.types[visDefinition.name] = visDefinition;
};
return {
+ /**
+ * registers a visualization type
+ * @param {VisType} config - visualization type definition
+ */
createBaseVisualization: (config: any) => {
const vis = new BaseVisType(config);
registerVisualization(() => vis);
},
+ /**
+ * registers a visualization which uses react for rendering
+ * @param {VisType} config - visualization type definition
+ */
createReactVisualization: (config: any) => {
const vis = new ReactVisType(config);
registerVisualization(() => vis);
},
+ /**
+ * registers a visualization alias
+ * alias is a visualization type without implementation, it just redirects somewhere in kibana
+ * @param {VisTypeAlias} config - visualization alias definition
+ */
registerAlias: visTypeAliasRegistry.add,
+ /**
+ * allows to hide specific visualization types from create visualization dialog
+ * @param {string[]} typeNames - list of type ids to hide
+ */
hideTypes: (typeNames: string[]) => {
typeNames.forEach((name: string) => {
if (this.types[name]) {
@@ -90,12 +107,22 @@ export class TypesService {
public start() {
return {
+ /**
+ * returns specific visualization or undefined if not found
+ * @param {string} visualization - id of visualization to return
+ */
get: (visualization: string) => {
return this.types[visualization];
},
+ /**
+ * returns all registered visualization types
+ */
all: () => {
return [...Object.values(this.types)];
},
+ /**
+ * returns all registered aliases
+ */
getAliases: visTypeAliasRegistry.get,
};
}
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx
index a79c6ad98edf6..6b37845f03db1 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx
@@ -29,6 +29,11 @@ export interface ShowNewVisModalParams {
onClose?: () => void;
}
+/**
+ * shows modal dialog that allows you to create new visualization
+ * @param {string[]} editorParams
+ * @param {function} onClose - function that will be called when dialog is closed
+ */
export function showNewVisModal({ editorParams = [], onClose }: ShowNewVisModalParams = {}) {
const container = document.createElement('div');
let isClosed = false;
diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js
index ee88ad95eeff0..43461c4c689be 100644
--- a/src/legacy/ui/public/field_editor/field_editor.js
+++ b/src/legacy/ui/public/field_editor/field_editor.js
@@ -66,7 +66,7 @@ import { ScriptingHelpFlyout } from './components/scripting_help';
import { FieldFormatEditor } from './components/field_format_editor';
import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants';
-import { copyField, getDefaultFormat, executeScript, isScriptValid } from './lib';
+import { copyField, executeScript, isScriptValid } from './lib';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -76,6 +76,25 @@ import 'brace/mode/groovy';
const getFieldFormats = () => npStart.plugins.data.fieldFormats;
+const getFieldTypeFormatsList = (field, defaultFieldFormat) => {
+ const fieldFormats = getFieldFormats();
+ const formatsByType = fieldFormats.getByFieldType(field.type).map(({ id, title }) => ({
+ id,
+ title,
+ }));
+
+ return [
+ {
+ id: '',
+ defaultFieldFormat,
+ title: i18n.translate('common.ui.fieldEditor.defaultFormatDropDown', {
+ defaultMessage: '- Default -',
+ }),
+ },
+ ...formatsByType,
+ ];
+};
+
export class FieldEditor extends PureComponent {
static propTypes = {
indexPattern: PropTypes.object.isRequired,
@@ -137,11 +156,7 @@ export class FieldEditor extends PureComponent {
field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0];
const fieldFormats = getFieldFormats();
-
- const fieldTypeFormats = [
- getDefaultFormat(fieldFormats.getDefaultType(field.type, field.esTypes)),
- ...fieldFormats.getByFieldType(field.type),
- ];
+ const DefaultFieldFormat = fieldFormats.getDefaultType(field.type, field.esTypes);
this.setState({
isReady: true,
@@ -150,14 +165,14 @@ export class FieldEditor extends PureComponent {
errors: [],
scriptingLangs,
fieldTypes,
- fieldTypeFormats,
+ fieldTypeFormats: getFieldTypeFormatsList(field, DefaultFieldFormat),
fieldFormatId: get(indexPattern, ['fieldFormatMap', field.name, 'type', 'id']),
fieldFormatParams: field.format.params(),
});
}
onFieldChange = (fieldName, value) => {
- const field = this.state.field;
+ const { field } = this.state;
field[fieldName] = value;
this.forceUpdate();
};
@@ -169,18 +184,11 @@ export class FieldEditor extends PureComponent {
const DefaultFieldFormat = fieldFormats.getDefaultType(type);
field.type = type;
-
- const fieldTypeFormats = [
- getDefaultFormat(DefaultFieldFormat),
- ...getFieldFormats().getByFieldType(field.type),
- ];
-
- const FieldFormat = fieldTypeFormats[0];
- field.format = new FieldFormat(null, getConfig);
+ field.format = new DefaultFieldFormat(null, getConfig);
this.setState({
- fieldTypeFormats,
- fieldFormatId: FieldFormat.id,
+ fieldTypeFormats: getFieldTypeFormatsList(field, DefaultFieldFormat),
+ fieldFormatId: DefaultFieldFormat.id,
fieldFormatParams: field.format.params(),
});
};
@@ -197,12 +205,13 @@ export class FieldEditor extends PureComponent {
};
onFormatChange = (formatId, params) => {
- const { getConfig } = this.props.helpers;
+ const fieldFormats = getFieldFormats();
const { field, fieldTypeFormats } = this.state;
- const FieldFormat =
- fieldTypeFormats.find(format => format.id === formatId) || fieldTypeFormats[0];
+ const FieldFormat = fieldFormats.getType(
+ formatId || fieldTypeFormats[0]?.defaultFieldFormat.id
+ );
- field.format = new FieldFormat(params, getConfig);
+ field.format = new FieldFormat(params, this.props.helpers.getConfig);
this.setState({
fieldFormatId: FieldFormat.id,
@@ -416,7 +425,8 @@ export class FieldEditor extends PureComponent {
renderFormat() {
const { field, fieldTypeFormats, fieldFormatId, fieldFormatParams } = this.state;
const { fieldFormatEditors } = this.props.helpers;
- const defaultFormat = fieldTypeFormats[0] && fieldTypeFormats[0].resolvedTitle;
+ const defaultFormat = fieldTypeFormats[0]?.defaultFieldFormat.title;
+
const label = defaultFormat ? (