Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Runtime fields editor] Expose editor for consuming apps #82116

Conversation

sebelga
Copy link
Contributor

@sebelga sebelga commented Oct 30, 2020

This PR adds the <RuntimeFieldEditor /> and <RuntimeFieldEditorFlyout /> component with their tests.

I've also added a README.md with an explanation on how to integrate the editor in the consuming apps.

Screenshot 2020-11-03 at 11 30 37

Screenshot 2020-10-30 at 12 56 48

Consuming the editor in a flyout

flyoutEditor.current = core.overlays.openFlyout(
  toMountPoint(
    <KibanaReactContextProvider>
      <RuntimeFieldEditorFlyoutContent
        onSave={saveRuntimeField}
        onCancel={() => flyoutEditor.current?.close()}
        docLinks={docLinksStart}
        defaultValue={/* optional field to edit */}
      />
    </KibanaReactContextProvider>
  )
)

How to test

  1. Replace the content of the x-pack/plugins/index_management/public/application/sections/home/home.tsx file with the code below.
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */

import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import {
  EuiButtonEmpty,
  EuiFlexGroup,
  EuiFlexItem,
  EuiPageBody,
  EuiPageContent,
  EuiSpacer,
  EuiTab,
  EuiTabs,
  EuiTitle,
  EuiFlyout,
  EuiButton,
} from '@elastic/eui';
import { OverlayRef } from 'src/core/public';
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
import {
  RuntimeFieldEditorFlyoutContent,
  RuntimeField,
} from '../../../../../runtime_fields/public';
import { documentationService } from '../../services/documentation';
import { createKibanaReactContext } from '../../../shared_imports';
import { DataStreamList } from './data_stream_list';
import { IndexList } from './index_list';
import { TemplateList } from './template_list';
import { ComponentTemplateList } from '../../components/component_templates';
import { breadcrumbService } from '../../services/breadcrumbs';
import { useAppContext } from '../../app_context';

export enum Section {
  Indices = 'indices',
  DataStreams = 'data_streams',
  IndexTemplates = 'templates',
  ComponentTemplates = 'component_templates',
}

export const homeSections = [
  Section.Indices,
  Section.DataStreams,
  Section.IndexTemplates,
  Section.ComponentTemplates,
];

interface MatchParams {
  section: Section;
}

const defaultRuntimeField: RuntimeField = { name: 'myField', type: 'date', script: 'test=123' };

export const IndexManagementHome: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
  match: {
    params: { section },
  },
  history,
}) => {
  const tabs = [
    {
      id: Section.Indices,
      name: <FormattedMessage id="xpack.idxMgmt.home.indicesTabTitle" defaultMessage="Indices" />,
    },
    {
      id: Section.DataStreams,
      name: (
        <FormattedMessage
          id="xpack.idxMgmt.home.dataStreamsTabTitle"
          defaultMessage="Data Streams"
        />
      ),
    },
    {
      id: Section.IndexTemplates,
      name: (
        <FormattedMessage
          id="xpack.idxMgmt.home.indexTemplatesTabTitle"
          defaultMessage="Index Templates"
        />
      ),
    },
    {
      id: Section.ComponentTemplates,
      name: (
        <FormattedMessage
          id="xpack.idxMgmt.home.componentTemplatesTabTitle"
          defaultMessage="Component Templates"
        />
      ),
    },
  ];

  const onSectionChange = (newSection: Section) => {
    history.push(`/${newSection}`);
  };

  const flyoutEditor = useRef<OverlayRef | null>(null);
  const [isRuntimeFieldEditorVisible, setIsRuntimeFieldEditorVisible] = useState(false);
  const {
    core: {
      overlays: { openFlyout },
    },
    uiSettings,
  } = useAppContext();

  const onSaveRuntimeField = useCallback((field: RuntimeField) => {
    setIsRuntimeFieldEditorVisible(false);
    console.log('Updated field', field);
    flyoutEditor.current?.close();
  }, []);

  const openRuntimeFieldEditor = useCallback(() => {
    const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings });
    flyoutEditor.current = openFlyout(
      toMountPoint(
        <KibanaReactContextProvider>
          <RuntimeFieldEditorFlyoutContent
            onSave={onSaveRuntimeField}
            onCancel={() => flyoutEditor.current?.close()}
            docLinks={{
              ELASTIC_WEBSITE_URL: 'https://elastic.co/',
              DOC_LINK_VERSION: 'master',
              links: {} as any,
            }}
            defaultValue={defaultRuntimeField}
          />
        </KibanaReactContextProvider>
      )
    );
  }, [openFlyout, onSaveRuntimeField, uiSettings]);

  useEffect(() => {
    breadcrumbService.setBreadcrumbs('home');
  }, []);

  return (
    <EuiPageBody>
      <EuiPageContent>
        <EuiTitle size="l">
          <EuiFlexGroup alignItems="center">
            <EuiFlexItem grow={true}>
              <h1 data-test-subj="appTitle">
                <FormattedMessage
                  id="xpack.idxMgmt.home.appTitle"
                  defaultMessage="Index Management"
                />
              </h1>
            </EuiFlexItem>
            <EuiFlexItem grow={false}>
              <EuiButtonEmpty
                href={documentationService.getIdxMgmtDocumentationLink()}
                target="_blank"
                iconType="help"
                data-test-subj="documentationLink"
              >
                <FormattedMessage
                  id="xpack.idxMgmt.home.idxMgmtDocsLinkText"
                  defaultMessage="Index Management docs"
                />
              </EuiButtonEmpty>
            </EuiFlexItem>
            <EuiFlexItem grow={false}>
              <EuiButton onClick={openRuntimeFieldEditor} fill>
                Create field
              </EuiButton>
            </EuiFlexItem>
          </EuiFlexGroup>
        </EuiTitle>

        {isRuntimeFieldEditorVisible && (
          <EuiFlyout size="m" maxWidth={720} onClose={() => setIsRuntimeFieldEditorVisible(false)}>
            <RuntimeFieldEditorFlyoutContent
              onSave={onSaveRuntimeField}
              onCancel={() => setIsRuntimeFieldEditorVisible(false)}
              docLinks={{
                ELASTIC_WEBSITE_URL: 'https://elastic.co/',
                DOC_LINK_VERSION: 'master',
                links: {} as any,
              }}
              defaultValue={defaultRuntimeField}
            />
          </EuiFlyout>
        )}

        <EuiSpacer size="m" />

        <EuiTabs>
          {tabs.map((tab) => (
            <EuiTab
              onClick={() => onSectionChange(tab.id)}
              isSelected={tab.id === section}
              key={tab.id}
              data-test-subj={`${tab.id}Tab`}
            >
              {tab.name}
            </EuiTab>
          ))}
        </EuiTabs>

        <EuiSpacer size="m" />

        <Switch>
          <Route
            exact
            path={[`/${Section.DataStreams}`, `/${Section.DataStreams}/:dataStreamName?`]}
            component={DataStreamList}
          />
          <Route exact path={`/${Section.Indices}`} component={IndexList} />
          <Route
            exact
            path={[`/${Section.IndexTemplates}`, `/${Section.IndexTemplates}/:templateName?`]}
            component={TemplateList}
          />
          <Route
            exact
            path={[
              `/${Section.ComponentTemplates}`,
              `/${Section.ComponentTemplates}/:componentTemplateName?`,
            ]}
            component={ComponentTemplateList}
          />
        </Switch>
      </EuiPageContent>
    </EuiPageBody>
  );
};
  1. On L24 of x-pack/plugins/index_management/public/application/app_context.tsx add
overlays: CoreStart['overlays'];
  1. On L45 & L57 of x-pack/plugins/index_management/public/application/mount_management_section.ts add overlays

@sebelga sebelga requested a review from a team as a code owner October 30, 2020 11:57
@sebelga sebelga added Team:Kibana Management Dev Tools, Index Management, Upgrade Assistant, ILM, Ingest Node Pipelines, and more v7.11.0 labels Oct 30, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/es-ui (Team:Elasticsearch UI)

export const RuntimeFieldEditor = ({ defaultValue, onChange, docLinks }: Props) => {
const links = getLinks(docLinks);

return <RuntimeFieldForm links={links} defaultValue={defaultValue} onChange={onChange} />;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, this component only renders the <RuntimeFieldForm /> but in a later stage it will also render the "Preview field" functionality.

@sebelga sebelga changed the title [Runtime fields editor] Add flyout wrapper [Runtime fields editor] Expose editor for consuming apps Oct 30, 2020
Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @sebelga ! Code looks good to me! Also tested locally. TIL about core.overalys.openFlyout.

As an aside, what are the original design decisions behind a service like this? It seems to be a way for UI rendering code to open a flyout.

})(props) as TestBed;

const docLinks: DocLinksStart = {
ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is just a test value, but looks like there is a typo in htts

<EuiButton onClick={() => setIsFlyoutVisible(true)}>Create field</EuiButton>

{isFlyoutVisible && (
<EuiFlyout onClose={() => setIsFlyoutVisible(false)}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I recall us talking about something similar to this in the past but why is the RuntimeFieldEditorFlyout wrapped in EuiFlyout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it can be injected in a flyout when calling core.overalys.openFlyout(). It is a similar mechanism that I put in place for the <GlobalFlyout />.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe renaming RuntimeFieldEditorFlyout to RuntimeFieldEditorFlyoutContent would make the relationship clearer? Otherwise it is a bit of a head-scratcher since the code makes it look like a flyout is being putting inside another flyout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I will rename the component 👍

@ryankeairns
Copy link
Contributor

@sebelga out of curiosity, were you working with a designer on this? I'm asking to better understand the depth of review needed here from the design team. Thanks!

@sebelga
Copy link
Contributor Author

sebelga commented Oct 30, 2020

Thanks for the review @jloleysens !

what are the original design decisions behind a service like this?

I think we added it when "actions and triggers" got released. With those, we can register dynamic action to panels, so we needed a way to programmatically open a flyout.

@ryankeairns Yes, we worked with @andreadelrio on this. The ping for the design team is because I added a .scss file. So I guess that's the only thing you'd need to review 😊. Of course, any other feedback is welcome!

@sebelga
Copy link
Contributor Author

sebelga commented Oct 30, 2020

@mistic What should I do about those 477.0B excess? Shorten some CSS class names 😄? or what are the options? Cheers!

@mistic
Copy link
Member

mistic commented Oct 30, 2020

@sebelga you can run node scripts/build_kibana_platform_plugins --update-limits and commit the changes to the limits file it will produce in order to start allowing that limit 😃 Also Spencer added documentation on how to do this in a more focused way per plugin #81716

Copy link
Contributor

@ryankeairns ryankeairns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a couple of quick observations regarding class naming and file organization. That said, I'm curious about the backgroud color override and will fire this locally to take a peek.

import { schema } from './schema';

import './runtime_field_form.scss';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of importing component-level SCSS files directly here, please create an index.scss and import it in your main application file. Commonly, the index.scss lives at public/ but it can also be in your components folder, if necessary. Main point being, we want to avoid the importation of multiple component level files in multiple places.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebelga You can see an example of what Ryan's describing in the Search Profiler plugin, which I recently refactored to follow this pattern (#82085).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought...

I'm thinking we might be able to avoid any SCSS overrides altogether in this PR. In looking at this locally, it strikes me that - since this is akin to a form input - we should be coloring it as such by default.

Specifically, I'm inclined to change the default editor theme's background color to $euiFormBackgroundColor in src/plugins/kibana_react/public/code_editor/editor_theme.ts (from its current empty/white color). This means we'd have to look at other instances of this CodeEditor component, but I suspect it's only used in a few spots (and it may not result in any changes for those instances any way).

If there are no objections, then you can remove your scss file, leave it with a white background for now, and I'll start a separate PR that changes the theme.

cc:/ @poffdeluxe since he originally built this for Canvas (I can clean up the container bg color there)

Copy link
Contributor

@cjcenizal cjcenizal Nov 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryankeairns Is this what you're thinking? elastic/eui#499

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of importing component-level SCSS files directly here, please create an index.scss and import it in your main application file.

I am a bit surprised here as it feels like going backward. We used to do that, then the recommended way (I think @cchaos mentioned it) was to keep the .scss file close to the component and import it directly from the component file. As we do here

I like that approach as it makes clearer the connection of the component with its style and we'd probably get the CSS lazyloaded if the component is lazyloaded. So now we need to go back and declare a top-level index.scss?

I'm thinking we might be able to avoid any SCSS overrides altogether in this PR. In looking at this locally, it strikes me that - since this is akin to a form input - we should be coloring it as such by default.

That'd be awesome if we could theme this globally. I did not want the runtime field editor to ship with a white background and no border as it lacked a frame for the user to work with. And from @cjcenizal PR I see the issue is from 2018 so I did not want to hold my breath on it 😄
But if you'll work on it I will remove the .scss override on this PR then. 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the current spec for adding new SASS files is to import them directly into the matching JS component file. https://github.com/elastic/kibana/blob/master/STYLEGUIDE.md#sass-files

@@ -0,0 +1,5 @@
.runtimeFieldEditor_form {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our css class naming follows the BEM schema and is made unique with a three-letter prefix to avoid style conflicts across applications. In this case, I would propose using something like .rtfEditor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Just so I understand you correctly, if I was going to correct this it would be .rtfEditor__form and not .rtfEditor right? As "form" is a child of the editor.

Copy link
Contributor

@ryankeairns ryankeairns Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some subjectivity, but yeah... in this case rtfEditor is the entity/block and the form is the element.

@@ -119,7 +123,7 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, docsBaseUri }: Props) =>
fullWidth
>
<CodeEditor
languageId="painless"
languageId={PainlessLang.ID}
// 99% width allows the editor to resize horizontally. 100% prevents it from resizing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebelga can you explain how to reproduce this?
I've tried this at 100% and 99% but am seeing what seems to be identical behavior (likely missing something). Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be inherited from https://github.com/elastic/kibana/pull/57538/files#diff-74f90896d62130f9108f9f6a68490f6cd9042ab39eaf4911145f5eb3b8de986bR18. It occurs in Painless Lab because the different panes (so tempted to riff on a pun here) resize as you change window width. One of those panes contains a <CodeEditor>.

Copy link
Contributor Author

@sebelga sebelga Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it was taken from the mappings editor "runtime" field type from https://github.com/elastic/kibana/pull/79940/files#diff-86ba24cfc405b9dded48e7e1eb210143eef125c734cbdc130f5bc3ddf95d2d6aR30

I will remove it here and in the mappings editor if there are not needed in those contexts. 👍

@sebelga
Copy link
Contributor Author

sebelga commented Nov 3, 2020

@elasticmachine merge upstream

@sebelga sebelga requested review from a team as code owners November 3, 2020 10:20
@sebelga
Copy link
Contributor Author

sebelga commented Nov 3, 2020

Thanks @mistic ! I've updated the plugin size limit. Can you have a look?

@ryankeairns I've addressed your comments, can you have another look? Cheers!

// 99% width allows the editor to resize horizontally. 100% prevents it from resizing.
width="99%"
languageId={PainlessLang.ID}
width="100%"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can safely remove the width prop and it will default to 100%

/** Width of editor. Defaults to 100%. */
width?: string | number;

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

@kbn/optimizer bundle module count

id before after diff
runtimeFields 11 17 +6

async chunks size

id before after diff
indexManagement 1.5MB 1.5MB +107.0B

page load bundle size

id before after diff
indexManagement 113.8KB 113.9KB +69.0B
runtimeFields 9.9KB 17.1KB +7.2KB
total +7.3KB

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

Copy link
Contributor

@spalger spalger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bump to 40k is totally fine, we plan to limit all bundles to 200k in the future.

@ryankeairns
Copy link
Contributor

@sebelga FYI, here's the PR to change the default theme background color. Once merged, this will result in the shaded color as opposed to the full white - #82451

Copy link
Contributor

@ryankeairns ryankeairns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for cleaning up the CSS stuff!

@sebelga
Copy link
Contributor Author

sebelga commented Nov 3, 2020

Thanks for the review @ryankeairns !

@sebelga sebelga merged commit 00faeb9 into elastic:feature/runtime-field-editor Nov 3, 2020
@sebelga sebelga deleted the runtime-fields/flyout-component branch November 3, 2020 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Project:RuntimeFields Team:Kibana Management Dev Tools, Index Management, Upgrade Assistant, ILM, Ingest Node Pipelines, and more
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants