Skip to content

HTML page rendering service #41964

Description

@mshustov

Rendering a view populates an HTML page with build artifacts and additional meta-data to bootstrap client-side core services and plugins.
Currently, Kibana supports 2 types of view rendering in ui_mixin

  • Render the Kibana app with default config. No user-specific settings are loaded.
  • Render Kibana app. The page is populated with user settings.

Also, there is an implicit concept of lightweight applications. Those applications do not have client code of the Kibana app but should have the same headers (CSP, CORS, etc.) as normal Kibana HTML page

HttpResource service (#44620) or another rendering service should provide API covering all 3 use-cases.

Known limitations
Concepts of uiExports and optimizer from Legacy platform don't exist in the New platform. Thus a Legacy platform code cannot be accessed, loaded and bootstrapped within the New Platform core.

Blocked by #18843
Related:
#41981

Activity

  1. added
    Team:CorePlatform Core services: plugins, logging, config, saved objects, http, ES client, i18n, etc t//
    on Jul 25, 2019
  2. elasticmachine commented on Jul 25, 2019

    @elasticmachine
    Contributor

    Pinging @elastic/kibana-platform

  3. changed the title [-]View rendering in the New platform[/-] [+]HTML page rendering service[/+] on Sep 6, 2019
  4. mshustov commented on Sep 10, 2019

    @mshustov
    ContributorAuthor

    Server-side:

    In the new platform, we have one js bundle - core, which bootstrap and load all the plugins. It means that we can serve only one page for different URLs and rely on client-side rendering to mount the required app depending on the current URL. It's different from the Legacy platform rendering, where the server needs to know app id to include a correct link to the application bundle.

    There are a few blockers on the server-side. We should either migrate them to the New platform or bridge Legacy platform API for a while (available via Hapi server object):

    Steps:

    1. To introduce Rendering service in the new platform. Which is responsible to populate a Kibana page template with passed data. I didn't find any use-cases when a plugin defines a custom template, seems that we can skip this functionality for a while. The Service should be coupled to HTTP service (important for integration with Legacy platform)
    interface KibanaRenderParams {
     strictCsp: boolean;
     bootstrapScriptUrl: string;
     ...
    }
    interface RenderingServiceSetup {
      renderKibanaPage(params: KibanaRenderParams): string;
    1. Provide an ability to respond from HttpService with rendered page.
    const page = rendering.renderKibanaPage();
    return response.ok(page);
    1. Switch legacy ui_render_mixin to the New platform Rendering service.

    Client-side

    The current implementation of Application Service limits standalone apps URL format. The URL format is defined as /app/:appId, where appId is application Id to mount. Thus, any other URL format (for example, global resource /login) cannot be associated with New platform plugin.
    There are several options to discuss:

    • Plugins declare other global(?) URLs as a part of their Application definition when registers in Application service. As before, an application is responsible to implement internal routing to render a page.
      Pros:
      One point for all the mounting logic.
      Easier to integrate with related services - capabilities, chrome, etc.
    core.application.register({
      id: 'security',
      globalURL: ['/login', '/logout'],
      async mount(context, params) {
        // mount is called for `/app/security`, `/login` and `/logout`
        return renderApp(context, params);
       },
    });
    // or register as a separate app
    core.application.register({
      id: 'security-login',
      globalURL: '/login',
      async mount(context, params) {
        // mount is called for  `/login` only
        return renderApp(context, params);
      },
    });
    • Create a separate StandalonePage Service. Which is similar to the Application Service, but allows a plugin to register any URL.
      Pros:
      More focused Application and StandalonePage services.
    core.standalonePage.register({
      url: '/login',
      async mount(context, params) {
        // mount is called for `/login` only
        return renderApp(context, params);
       },
    });
    • If an application doesn't want to be listed in ChromeUI it should configure it somehow. In the current implementation, all the apps are listed by default. Should we split registration in the application service from registration in the ChromeUI?
    core.chrome.addLink({ title, icon: ... })
    core.application.register({ id, mount... });
  5. mshustov commented on Nov 18, 2019

    @mshustov
    ContributorAuthor

    Until the new API is ready we need to recommend a workaround.
    Options:

    • To keep rendering in the legacy platform. Plugins can migrate code to the new platform. When then need to render HTML, they request a legacy route or redirect a request to the legacy route.
    • NP provides temporary API to render HTML. The platform can use Legacy infrastructure under the hood. Plugins don't depend on the implementation.
  6. joshdover commented on Nov 18, 2019

    @joshdover
    Contributor

    I proposed something similar to the second option here: #49102 (comment)

    I think that's a practical way forward, but we should be careful about which options we expose in the NP API so we don't have too much churn from breaking it in the future.

  7. self-assigned this
    on Nov 19, 2019
  8. mshustov commented on Jan 16, 2020

    @mshustov
    ContributorAuthor

    consider adding a rendering helper #54470 (comment)

  9. mshustov commented on Mar 12, 2020

    @mshustov
    ContributorAuthor

    The current implementation has a couple of drawbacks:

    return response.ok({
      body: await context.core.rendering.render({ includeUserSettings: true }),
      headers: { 'content-security-policy': csp.header },
    });

    We would like to improve the situation and avoid error-prone manual configuration. It seems that this logic is more appropriate for HttpResource service #44620

    • for basic use-cases when not much logic involved and we need to respond with the static "core" HTML page it can provide
    interface HttpResourceSetup {
      registerStaticApp: ({ path: string }) => void
    }
    httpResource.registerStaticApp({ path: '/my-url'})
    • for use-cases when a plugin needs to perform some computation in a route handler we could adopt HttpResource service to provide Kibana-router compatible API:
    interface HttpResourceSetup {
      registerApp: (RouteConfig<'get'>) => KibanaResponse
    }
    httpResource.registerApp({ path: '/my-url', validate: false }, async (context, request, response) => {
      // response already pre-configured with necessary CSP, Content-Type, Cache-control headers
      // plugins can override headers except for CSP header
      return response.ok({
        body: `<!DOCTYPE html> ...`
        headers: 
      });

    Are we okay with this proposal?
    Can we close the current issue in favor of #44620?
    Should I outline RFC for HttpResource service?
    @elastic/kibana-platform

  10. joshdover commented on Mar 15, 2020

    @joshdover
    Contributor

    I wonder if we couldn't improve the ergonomics of a regular app that needs a custom route (not prefixed with /app/):

    • Make the server-side API easier to find by making it part of the application namespace of CoreSetup: core.application.registerAppRoute('/login')
    • If an application is registered in the UI with a custom appRoute and there is no corresponding registration on the backend, we could throw an error to guide developers.
    • for use-cases when a plugin needs to perform some computation in a route handle we could adopt HttpResource service to provide Kibana-router compatible API:
    interface HttpResourceSetup {
      registerApp: (RouteConfig<'get'>) => KibanaResponse
    }
    httpResource.registerApp({ path: '/my-url', validate: false }, async (context, request, response) => {
      // response already pre-configured with necessary CSP, Content-Type, Cache-control headers
      // plugins can override headers except for CSP header
      return response.ok({
        body: `<!DOCTYPE html> ...`
        headers: 
      });

    This API seems to do very little. It feels like it should just be part of the ResponseFactory:

    res.html({ html: ``, headers: {} });
  11. mshustov commented on Mar 16, 2020

    @mshustov
    ContributorAuthor

    This API seems to do very little. It feels like it should just be part of the ResponseFactory:

    I thought about it, but I decided it wasn't enough for a few (maybe minor) reasons:

    • html will have an inconsistent interface with ResponseFactory. for example, you might need to send an HTML page with an error status code.
    • response type is not limited by html, but can be any type of dynamic content
      window.location.replace(
      '${serverBasePath}/api/security/oidc/callback?authenticationResponseURI=' + encodeURIComponent(window.location.href)
      );
      `,
      'text/javascript',

    I wonder if we couldn't improve the ergonomics of a regular app that needs a custom route (not prefixed with /app/):

    Why not for both prefixed and un-prefixed apps? As I understand, a user can land on any page.

  12. mshustov commented on Mar 16, 2020

    @mshustov
    ContributorAuthor

    Let's move discussion to #50654

  13. mshustov commented on Mar 19, 2020

    @mshustov
    ContributorAuthor

    will be done in #50654

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Feature:New PlatformTeam:CorePlatform Core services: plugins, logging, config, saved objects, http, ES client, i18n, etc t//blockerv8.0.0

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions