Skip to content

[Discuss] New Management Sections API #43499

@lukeelmers

Description

@lukeelmers

Background

Management is one of the four primary "domains" covered by @elastic/kibana-app-arch (along with Data, Embeddables, and Visualizations). There are two main purposes for this service:

  1. Own the management "framework" -- the UI that displays the management sidebar nav, the landing page, and handles rendering each of the sections
  2. Expose a registry for other plugins to add their own registry sections to the UI and add nested links to them in the sidebar.

Purpose

The purpose of this discussion is to consider item 2 above -- the service for registering sections to the nav & loading them up. Below is a proposed design for how this would look in the new platform. The reason for having this discussion now is because some plugins will be blocked on fully moving to the new platform by the management plugin.

Design Goals

  • Remove another usage of IndexedArray & uiRegistry (required for migration)
  • Remove dependency on ui/routes (required for migration)
  • Remove management section uiExport (required for migration)
  • Simple API that is designed in keeping with new platform principles
    • This includes being rendering-framework-agnostic... You should be able to build your management section UI however you'd like
  • Clear separation of app/UI code and service code, even if both live within the same plugin
  • Flexibility to potentially support alternate layouts in the future (see mockups in reference section below)

Reference

Overview of the legacy service

Example usage of how this looks today:

// myplugin/index
new Kibana.Plugin({
  uiExports: {
    managementSections: ['myplugin/management'],
  }
});
 
// myplugin/public/management
import { management } from 'ui/management';
 
// completely new section
const newSection = management.register('mypluginsection', {
  name: 'mypluginsection',
  order: 10,
  display: 'My Plugin',
  icon: 'iconName',
});
newSection.register('mypluginlink', {
  name: 'mypluginlink',
  order: 10,
  display: 'My sublink',
  url: `#/management/myplugin`,
});
 
// new link in existing section
const kibanaSection = management.getSection('kibana');
kibanaSection.register('mypluginlink', {
  name: 'mypluginlink',
  order: 10,
  display: 'My sublink',
  url: `#/management/myplugin`,
});
 
// use ui/routes to render component
import routes from 'ui/routes';
 
const renderReact = (elem) => {
  render(<MyApp />, elem);
};
 
routes.when('management/myplugin', {
  controller($scope, $http, kbnUrl) {
    $scope.$on('$destroy', () => {
      const elem = document.getElementById('usersReactRoot');
      if (elem) unmountComponentAtNode(elem);
    });
    $scope.$$postDigest(() => {
      const elem = document.getElementById('usersReactRoot');
      const changeUrl = (url) => {
        kbnUrl.change(url);
        $scope.$apply();
      };
      renderReact(elem, $http, changeUrl);
    });
  },
});

Current public contracts owned by the legacy service:

// ui/management/index
interface API {
  PAGE_TITLE_COMPONENT: string; // actually related to advanced settings?
  PAGE_SUBTITLE_COMPONENT: string; // actually related to advanced settings?
  PAGE_FOOTER_COMPONENT: string; // actually related to advanced settings?
  SidebarNav: React.SFC<any>;
  registerSettingsComponent: (
    id: string,
    component: string | React.SFC<any>,
    allowOverride: boolean
  ) => void;
  management: new ManagementSection();
  MANAGEMENT_BREADCRUMB: {
    text: string;
    href: string;
  };
}

// ui/management/section
class ManagementSection {
  get visibleItems,
  addListener: (fn: function) => void,
  register: (id: string, options: Options) => ManagementSection | Error,
  deregister: (id: string) => void,
  hasItem: (id: string) => boolean,
  getSection: (id: string) => ManagementSection,
  hide: () => void,
  show: () => void,
  disable: () => void,
  enable: () => void,
}
 
interface Options {
  order: number | null;
  display: string | null; // defaults to id
  url: string | null; // defaults to ''
  visible: boolean | null; // defaults to true
  disabled: boolean | null; // defaults to false
  tooltip: string | null; // defaults to ''
  icon: string | null; // defaults to ''
}

Proposed API

This API is influenced heavily by the design of Core's application service mounting. The intent is to make the experience consistent with that service; the Management section is basically one big app with a bunch of registered "subapps".

Example usage:

// my_plugin/public/plugin.ts

export class MyPlugin {
  setup(core, { management }) {
    management.sections.register({
      id: 'my-section',
      title: 'My Main Section', // display name
      description: 'hello', // not used in current UI, but possibly in future
      order: 10,
      euiIconType: 'iconName',
    });
    management.sections.registerLink('my-section', {
      id: 'my-link',
      title: 'My Link', // display name
      description: 'hello', // not used in current UI, but possibly in future
      order: 20,
      async mount(context, params) {
        const { renderApp } = await import('./my-section');
        return renderApp(context, params);
      }
    });
  }
}
 
// my_plugin/public/my-section.tsx

export function renderApp(context, { basePath, element }) {
  ReactDOM.render(
    // `basePath` would be `/app/management/my-section/my-link`
    <MyApp basename={basePath} />,
    element
  );
 
  // return value must be a function that unmounts (just like Core Application Service)
  return () => ReactDOM.unmountComponentAtNode(element);
}

Detailed design:

interface ManagementSetup {
  sections: SectionsService;
}
 
interface SectionsService {
  register: Register;
  registerLink: RegisterLink;
  getAvailable: () => Section[]; // filtered based on capabilities
  get: (id: string) => Section | undefined;
  getLink: (id: string) => Link | undefined;
}
 
type Register = ({
  id: string;
  title: string;
  description: string; // not used in current UI, but possibly in future
  order?: number;
  euiIconType?: string; // takes precedence over `icon` property.
  icon?: string; // URL to image file; fallback if no `euiIconType`
}) => Section | Error;
 
type RegisterLink = (sectionId: string, {
  id: string;
  title: string;
  description: string; // not used in current UI, but possibly in future
  order?: number;
  mount: ManagementSectionMount;
}) => Link | Error;
 
type Unmount = () => Promise<void> | void;
 
type ManagementSectionMount = (
  context: AppMountContext, // provided by core.ApplicationService
  params: MountParams,
) => Unmount | Promise<Unmount>;
 
interface MountParams {
  basePath: string; // base path for setting up your router
  element: HTMLElement; // element the section should render into
}
 
interface Section {
  id: string;
  title: string;
  description: string;
  baseBath: string;
  links: Link[];
  order?: number;
  euiIconType?: string;
  icon?: string;
  destroy: () => Promise<void> | void; // de-registers and destroys all links
}
 
interface Link {
  id: string;
  title: string;
  description: string;
  basePath: string;
  sectionId: string;
  order?: number;
  destroy: () => Promise<void> | void; // de-registers & calls unmount()
}

Caveats, Questions, & Discussion Points:

  • This removes the ability to infinitely nest sections within each other by making a distinction between a section header and a nav link.
    • So far we didn't seem to be using this feature anyway, but would like feedback on any use cases for it.
  • The hide/show/disable/enable options were dropped with the assumption that we will be working with uiCapabilities to determine this instead... so people shouldn't need to manage it manually as they can look up a pre-filtered list of sections.
  • This was updated to add flexibility for custom (non-EUI) icons as outlined in Allow custom icons for management sections #32661. Much like the Core Application Service, you either choose an EUI icon, or provide a URL to an icon.
  • Do individual links within a section need the ability to specify icons? Currently only the section headers have icons.
  • Added field for a description, though this isn't currently used in the UI.

Next Steps

We will keep this issue open for feedback for one week (until August 23, 2019), at which point we will seek any final comments before closing the discussion in order to make plans for future implementation.


Copying a few folks here who either have knowledge of the legacy management framework, or who are actively managing plugins that register management sections:

@bmcconaghy @cjcenizal @jen-huang @mattkime @kobelb @legrego @alvarezmelissa87 @peteharverson @mattapperson @stacey-gammon

Metadata

Metadata

Assignees

Labels

Feature:Kibana ManagementFeature label for Data Views, Advanced Setting, Saved Object management pagesdiscuss

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions