Skip to content

Commit

Permalink
[UnifiedFieldList] Remove redundant server routes. Create new example…
Browse files Browse the repository at this point in the history
… plugin for unified field list components and migrate tests. (#158377)

- Closes #147885
- Closes #157109

## Summary

**Before:**

Unified Field List plugin has internal routes (wrappers for client code)
which exist only to run api functional tests against them:
 - `/api/unified_field_list/existing_fields/{dataViewId}`
 - `/api/unified_field_list/field_stats`

Client code does not call these routes directly. So there is no reason
in keeping and versioning them.

**After:**
- Internal routes are removed 
- A new "Unified Field List Examples" page was created
http://localhost:5601/app/unifiedFieldListExamples
- API functional tests (which used the routes) were converted to
functional tests against this new example page
- Created a new `unifiedFieldList` page object which is used now in
functional tests (methods are extracted from existing `discover` page
object).

**For testing:**

Steps:
1. Run Kibana with examples: `yarn start --run-examples` 
2. Install sample data
3. And navigate to Developer Examples > Unified Field List Examples
page.

![May-26-2023
13-24-03](https://github.com/elastic/kibana/assets/1415710/5a2149f7-beb8-40a5-b7d5-9eeaabfd42ca)


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
jughosta and kibanamachine authored May 31, 2023
1 parent f43096a commit 8d399fe
Show file tree
Hide file tree
Showing 90 changed files with 2,104 additions and 1,846 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ test/plugin_functional/plugins/ui_settings_plugin @elastic/kibana-core
packages/kbn-ui-shared-deps-npm @elastic/kibana-operations
packages/kbn-ui-shared-deps-src @elastic/kibana-operations
packages/kbn-ui-theme @elastic/kibana-operations
examples/unified_field_list_examples @elastic/kibana-data-discovery
src/plugins/unified_field_list @elastic/kibana-data-discovery
src/plugins/unified_histogram @elastic/kibana-data-discovery
src/plugins/unified_search @elastic/kibana-visualizations
Expand Down
9 changes: 9 additions & 0 deletions examples/unified_field_list_examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# unified_field_list_examples

Examples of unified field list components.

To run this example, ensure you have data to search against (for example, the sample datasets) and start kibana with the `--run-examples` flag.

```bash
yarn start --run-examples
```
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
* Side Public License, v 1.
*/

export const BASE_API_PATH = '/internal/unified_field_list';
export const FIELD_STATS_API_PATH = `${BASE_API_PATH}/field_stats`;
export const FIELD_EXISTING_API_PATH = `${BASE_API_PATH}/existing_fields/{dataViewId}`;
export const PLUGIN_ID = 'unifiedFieldListExamples';
export const PLUGIN_NAME = 'Unified Field List Examples';
24 changes: 24 additions & 0 deletions examples/unified_field_list_examples/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"type": "plugin",
"id": "@kbn/unified-field-list-examples-plugin",
"owner": "@elastic/kibana-data-discovery",
"description": "Examples for using unified field list.",
"plugin": {
"id": "unifiedFieldListExamples",
"server": false,
"browser": true,
"requiredPlugins": [
"navigation",
"developerExamples",
"inspector",
"kibanaUtils",
"unifiedSearch",
"unifiedFieldList",
"data",
"dataViews",
"dataViewFieldEditor",
"charts",
"fieldFormats"
]
}
}
37 changes: 37 additions & 0 deletions examples/unified_field_list_examples/public/application.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n-react';
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import { AppPluginStartDependencies } from './types';
import { UnifiedFieldListExampleApp } from './example_app';

export const renderApp = (
core: CoreStart,
deps: AppPluginStartDependencies,
{ element }: AppMountParameters
) => {
ReactDOM.render(
<I18nProvider>
<UnifiedFieldListExampleApp
services={{
core,
uiSettings: core.uiSettings,
...deps,
}}
/>
</I18nProvider>,
element
);

return () => {
ReactDOM.unmountComponentAtNode(element);
};
};
161 changes: 161 additions & 0 deletions examples/unified_field_list_examples/public/example_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { useCallback, useEffect, useState } from 'react';
import { css } from '@emotion/react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageSidebar,
EuiTitle,
EuiEmptyPrompt,
EuiLoadingLogo,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import { RootDragDropProvider } from '@kbn/dom-drag-drop';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { FieldListSidebar, FieldListSidebarProps } from './field_list_sidebar';
import { ExampleDropZone } from './example_drop_zone';

interface UnifiedFieldListExampleAppProps {
services: FieldListSidebarProps['services'];
}

export const UnifiedFieldListExampleApp: React.FC<UnifiedFieldListExampleAppProps> = ({
services,
}) => {
const { navigation, data, unifiedSearch } = services;
const { IndexPatternSelect } = unifiedSearch.ui;
const [dataView, setDataView] = useState<DataView | null>();
const [selectedFieldNames, setSelectedFieldNames] = useState<string[]>([]);

const onAddFieldToWorkplace = useCallback(
(field: DataViewField) => {
setSelectedFieldNames((names) => [...names, field.name]);
},
[setSelectedFieldNames]
);

const onRemoveFieldFromWorkspace = useCallback(
(field: DataViewField) => {
setSelectedFieldNames((names) => names.filter((name) => name !== field.name));
},
[setSelectedFieldNames]
);

const onDropFieldToWorkplace = useCallback(
(fieldName: string) => {
setSelectedFieldNames((names) => [...names.filter((name) => name !== fieldName), fieldName]);
},
[setSelectedFieldNames]
);

// Fetch the default data view using the `data.dataViews` service, as the component is mounted.
useEffect(() => {
const setDefaultDataView = async () => {
try {
const defaultDataView = await data.dataViews.getDefault();
setDataView(defaultDataView);
} catch (e) {
setDataView(null);
}
};

setDefaultDataView();
}, [data]);

if (typeof dataView === 'undefined') {
return (
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoKibana" size="xl" />}
title={<h2>{PLUGIN_NAME}</h2>}
body={<p>Loading...</p>}
/>
);
}

return (
<EuiPage grow={true}>
<EuiPageBody paddingSize="s">
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiTitle>
<h1>{PLUGIN_NAME}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<IndexPatternSelect
placeholder={i18n.translate('searchSessionExample.selectDataViewPlaceholder', {
defaultMessage: 'Select data view',
})}
indexPatternId={dataView?.id || ''}
onChange={async (dataViewId?: string) => {
if (dataViewId) {
const newDataView = await data.dataViews.get(dataViewId);
setDataView(newDataView);
} else {
setDataView(undefined);
}
}}
isClearable={false}
data-test-subj="dataViewSelector"
/>
</EuiFlexItem>
{dataView ? (
<>
<EuiFlexItem grow={false}>
<navigation.ui.TopNavMenu
appName={PLUGIN_ID}
showSearchBar={true}
useDefaultBehaviors={true}
indexPatterns={dataView ? [dataView] : undefined}
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<RootDragDropProvider>
<EuiFlexGroup direction="row" alignItems="stretch">
<EuiFlexItem grow={false}>
<EuiPageSidebar
css={css`
flex: 1;
width: 320px;
`}
>
<FieldListSidebar
services={services}
dataView={dataView}
selectedFieldNames={selectedFieldNames}
onAddFieldToWorkplace={onAddFieldToWorkplace}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
/>
</EuiPageSidebar>
</EuiFlexItem>
<EuiFlexItem>
<ExampleDropZone onDropField={onDropFieldToWorkplace} />
</EuiFlexItem>
</EuiFlexGroup>
</RootDragDropProvider>
</EuiFlexItem>
</>
) : (
<EuiEmptyPrompt
iconType="warning"
color="warning"
title={<h2>Select a data view</h2>}
body={<p>Make sure to have at least one data view or install sample data.</p>}
/>
)}
</EuiFlexGroup>
</EuiPageBody>
</EuiPage>
);
};
73 changes: 73 additions & 0 deletions examples/unified_field_list_examples/public/example_drop_zone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { useContext, useMemo } from 'react';
import { DragContext, DragDrop, DropOverlayWrapper, DropType } from '@kbn/dom-drag-drop';
import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';

const DROP_PROPS = {
value: {
id: 'exampleDropZone',
humanData: {
label: 'Drop zone for selecting fields',
},
},
order: [1, 0, 0, 0],
types: ['field_add'] as DropType[],
};

export interface ExampleDropZoneProps {
onDropField: (fieldName: string) => void;
}

export const ExampleDropZone: React.FC<ExampleDropZoneProps> = ({ onDropField }) => {
const dragDropContext = useContext(DragContext);
const draggingFieldName = dragDropContext.dragging?.id;

const onDroppingField = useMemo(() => {
if (!draggingFieldName) {
return undefined;
}

return () => onDropField(draggingFieldName);
}, [onDropField, draggingFieldName]);

const isDropAllowed = Boolean(onDroppingField);

return (
<DragDrop
draggable={false}
dropTypes={isDropAllowed ? DROP_PROPS.types : undefined}
value={DROP_PROPS.value}
order={DROP_PROPS.order}
onDrop={onDroppingField}
>
<DropOverlayWrapper isVisible={isDropAllowed}>
<EuiPanel hasShadow={false} paddingSize="l" className="eui-fullHeight">
<EuiEmptyPrompt
iconType="beaker"
title={<h3>Example drop zone</h3>}
body={
<p>
Drop <strong>{draggingFieldName || 'a field'}</strong> here to select it
</p>
}
/>
</EuiPanel>
</DropOverlayWrapper>
</DragDrop>
);
};
Loading

0 comments on commit 8d399fe

Please sign in to comment.