From 7462baaabf4afff2279569b63690235732876fbe Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 12:51:56 -0600 Subject: [PATCH 01/11] Initial copy/paste of components Changes for pre-commit hooks were: - Linting - Lodash imports - Fixed warnings for `jsx-a11y/mouse-events-have-key-events` with stubbed onFocus and onBlue events with FIXME comments --- .../display_settings/custom_source_icon.tsx | 36 ++ .../display_settings/display_settings.tsx | 137 ++++++++ .../display_settings_logic.ts | 332 ++++++++++++++++++ .../display_settings_router.tsx | 31 +- .../example_result_detail_card.tsx | 75 ++++ .../example_search_result_group.tsx | 74 ++++ .../example_standout_result.tsx | 66 ++++ .../display_settings/field_editor_modal.tsx | 101 ++++++ .../components/display_settings/index.ts | 2 +- .../display_settings/result_detail.tsx | 146 ++++++++ .../display_settings/search_results.tsx | 162 +++++++++ .../display_settings/subtitle_field.tsx | 35 ++ .../display_settings/title_field.tsx | 35 ++ 13 files changed, 1230 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.tsx new file mode 100644 index 0000000000000..16129324b56d1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.tsx @@ -0,0 +1,36 @@ +/* + * 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 from 'react'; + +const BLACK_RGB = '#000'; + +interface CustomSourceIconProps { + color?: string; +} + +export const CustomSourceIcon: React.FC = ({ color = BLACK_RGB }) => ( + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx new file mode 100644 index 0000000000000..518704acabaaa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -0,0 +1,137 @@ +/* + * 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 } from 'react'; + +import { History } from 'history'; +import { useActions, useValues } from 'kea'; +import { useHistory } from 'react-router-dom'; + +import { EuiButton, EuiEmptyPrompt, EuiTabbedContent, EuiPanel } from '@elastic/eui'; + +import { + DISPLAY_SETTINGS_RESULT_DETAIL_PATH, + DISPLAY_SETTINGS_SEARCH_RESULT_PATH, + getContentSourcePath, +} from 'workplace_search/utils/routePaths'; + +import { AppLogic } from 'workplace_search/App/AppLogic'; + +import FlashMessages from 'shared/components/FlashMessages'; + +import { Loading, ViewContentHeader } from 'workplace_search/components'; +import { DisplaySettingsLogic } from './DisplaySettingsLogic'; + +import { FieldEditorModal } from './FieldEditorModal'; +import { ResultDetail } from './ResultDetail'; +import { SearchResults } from './SearchResults'; + +const UNSAVED_MESSAGE = + 'Your display settings have not been saved. Are you sure you want to leave?'; + +interface DisplaySettingsProps { + tabId: number; +} + +export const DisplaySettings: React.FC = ({ tabId }) => { + const history = useHistory() as History; + const { initializeDisplaySettings, setServerData, resetDisplaySettingsState } = useActions( + DisplaySettingsLogic + ); + + const { + dataLoading, + flashMessages, + sourceId, + addFieldModalVisible, + unsavedChanges, + exampleDocuments, + } = useValues(DisplaySettingsLogic); + + const { isOrganization } = useValues(AppLogic); + + const hasDocuments = exampleDocuments.length > 0; + + useEffect(() => { + initializeDisplaySettings(); + return resetDisplaySettingsState; + }, []); + + useEffect(() => { + window.onbeforeunload = hasDocuments && unsavedChanges ? () => UNSAVED_MESSAGE : null; + return () => { + window.onbeforeunload = null; + }; + }, [unsavedChanges]); + + if (dataLoading) return ; + + const tabs = [ + { + id: 'search_results', + name: 'Search Results', + disabled: false, + content: , + }, + { + id: 'result_detail', + name: 'Result Detail', + disabled: false, + content: , + }, + ]; + + const onSelectedTabChanged = (tab) => { + const path = + tab.id === tabs[1].id + ? getContentSourcePath(DISPLAY_SETTINGS_RESULT_DETAIL_PATH, sourceId, isOrganization) + : getContentSourcePath(DISPLAY_SETTINGS_SEARCH_RESULT_PATH, sourceId, isOrganization); + + history.push(path); + }; + + const handleFormSubmit = (e) => { + e.preventDefault(); + setServerData(); + }; + + return ( + <> +
+ + Save + + ) : null + } + /> + {!!flashMessages && } + {hasDocuments ? ( + + ) : ( + + You have no content yet} + body={ +

You need some content to display in order to configure the display settings.

+ } + /> +
+ )} + + {addFieldModalVisible && } + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts new file mode 100644 index 0000000000000..b6d7c20233709 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -0,0 +1,332 @@ +/* + * 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 { cloneDeep, isEqual, differenceBy } from 'lodash'; +import { DropResult } from 'react-beautiful-dnd'; + +import { kea, MakeLogicType } from 'kea'; +import http from 'shared/http'; + +import { euiSelectObjectFromValue } from 'shared/utils'; +import routes from 'workplace_search/routes'; + +import { AppLogic } from 'workplace_search/App/AppLogic'; +import { SourceLogic } from 'workplace_search/ContentSources/SourceLogic'; + +const SUCCESS_MESSAGE = 'Display Settings have been successfuly updated.'; + +import { IFlashMessagesProps } from 'shared/types'; +import { IObject, DetailField, SearchResultConfig, OptionValue } from 'workplace_search/types'; + +export interface DisplaySettingsResponseProps { + sourceName: string; + searchResultConfig: SearchResultConfig; + schemaFields: IObject; + exampleDocuments: IObject[]; +} + +export interface DisplaySettingsInitialData extends DisplaySettingsResponseProps { + sourceId: string; + serverRoute: string; +} + +interface DisplaySettingsActions { + initializeDisplaySettings(): void; + setServerData(): void; + onInitializeDisplaySettings( + displaySettingsProps: DisplaySettingsInitialData + ): DisplaySettingsInitialData; + setServerResponseData( + displaySettingsProps: DisplaySettingsResponseProps + ): DisplaySettingsResponseProps; + setFlashMessages(flashMessages: IFlashMessagesProps): { flashMessages: IFlashMessagesProps }; + setTitleField(titleField: string | null): string | null; + setUrlField(urlField: string): string; + setSubtitleField(subtitleField: string | null): string | null; + setDescriptionField(descriptionField: string | null): string | null; + setColorField(hex: string); + setDetailFields(result: DropResult); + openEditDetailField(editFieldIndex: number | null); + removeDetailField(index: number); + addDetailField(newField: DetailField); + updateDetailField(updatedField: DetailField, index: number | null); + toggleFieldEditorModal(): void; + toggleTitleFieldHover(): void; + toggleSubtitleFieldHover(): void; + toggleDescriptionFieldHover(): void; + toggleUrlFieldHover(): void; + resetDisplaySettingsState(): void; +} + +interface DisplaySettingsValues { + sourceName: string; + sourceId: string; + schemaFields: IObject; + exampleDocuments: IObject[]; + serverSearchResultConfig: SearchResultConfig; + searchResultConfig: SearchResultConfig; + serverRoute: string; + flashMessages: IFlashMessagesProps; + editFieldIndex: number | null; + dataLoading: boolean; + addFieldModalVisible: boolean; + titleFieldHover: boolean; + urlFieldHover: boolean; + subtitleFieldHover: boolean; + descriptionFieldHover: boolean; + fieldOptions: OptionValue[]; + optionalFieldOptions: OptionValue[]; + availableFieldOptions: OptionValue[]; + unsavedChanges: boolean; +} + +const defaultSearchResultConfig = { + titleField: '', + subtitleField: '', + descriptionField: '', + urlField: '', + color: '#000000', + detailFields: [], +}; + +export const DisplaySettingsLogic = kea< + MakeLogicType +>({ + actions: { + onInitializeDisplaySettings: (displaySettingsProps: DisplaySettingsInitialData) => + displaySettingsProps, + setServerResponseData: (displaySettingsProps: DisplaySettingsResponseProps) => + displaySettingsProps, + setFlashMessages: (flashMessages: IFlashMessagesProps) => ({ flashMessages }), + setTitleField: (titleField: string) => titleField, + setUrlField: (urlField: string) => urlField, + setSubtitleField: (subtitleField: string | null) => subtitleField, + setDescriptionField: (descriptionField: string) => descriptionField, + setColorField: (hex: string) => hex, + setDetailFields: (result: DropResult) => ({ result }), + openEditDetailField: (editFieldIndex: number | null) => editFieldIndex, + removeDetailField: (index: number) => index, + addDetailField: (newField: DetailField) => newField, + updateDetailField: (updatedField: DetailField, index: number) => ({ updatedField, index }), + toggleFieldEditorModal: () => true, + toggleTitleFieldHover: () => true, + toggleSubtitleFieldHover: () => true, + toggleDescriptionFieldHover: () => true, + toggleUrlFieldHover: () => true, + resetDisplaySettingsState: () => true, + initializeDisplaySettings: () => true, + setServerData: () => true, + }, + reducers: { + sourceName: [ + '', + { + onInitializeDisplaySettings: (_, { sourceName }) => sourceName, + }, + ], + sourceId: [ + '', + { + onInitializeDisplaySettings: (_, { sourceId }) => sourceId, + }, + ], + schemaFields: [ + {}, + { + onInitializeDisplaySettings: (_, { schemaFields }) => schemaFields, + }, + ], + exampleDocuments: [ + [], + { + onInitializeDisplaySettings: (_, { exampleDocuments }) => exampleDocuments, + }, + ], + serverSearchResultConfig: [ + defaultSearchResultConfig, + { + onInitializeDisplaySettings: (_, { searchResultConfig }) => + setDefaultColor(searchResultConfig), + setServerResponseData: (_, { searchResultConfig }) => searchResultConfig, + }, + ], + searchResultConfig: [ + defaultSearchResultConfig, + { + onInitializeDisplaySettings: (_, { searchResultConfig }) => + setDefaultColor(searchResultConfig), + setServerResponseData: (_, { searchResultConfig }) => searchResultConfig, + setTitleField: (searchResultConfig, titleField) => ({ ...searchResultConfig, titleField }), + setSubtitleField: (searchResultConfig, subtitleField) => ({ + ...searchResultConfig, + subtitleField, + }), + setUrlField: (searchResultConfig, urlField) => ({ ...searchResultConfig, urlField }), + setDescriptionField: (searchResultConfig, descriptionField) => ({ + ...searchResultConfig, + descriptionField, + }), + setColorField: (searchResultConfig, color) => ({ ...searchResultConfig, color }), + setDetailFields: (searchResultConfig, { result: { destination, source } }) => { + const detailFields = cloneDeep(searchResultConfig.detailFields); + const element = detailFields[source.index]; + detailFields.splice(source.index, 1); + detailFields.splice(destination.index, 0, element); + return { + ...searchResultConfig, + detailFields, + }; + }, + addDetailField: (searchResultConfig, newfield) => { + const detailFields = cloneDeep(searchResultConfig.detailFields); + detailFields.push(newfield); + return { + ...searchResultConfig, + detailFields, + }; + }, + removeDetailField: (searchResultConfig, index) => { + const detailFields = cloneDeep(searchResultConfig.detailFields); + detailFields.splice(index, 1); + return { + ...searchResultConfig, + detailFields, + }; + }, + updateDetailField: (searchResultConfig, { updatedField, index }) => { + const detailFields = cloneDeep(searchResultConfig.detailFields); + detailFields[index] = updatedField; + return { + ...searchResultConfig, + detailFields, + }; + }, + }, + ], + serverRoute: [ + '', + { + onInitializeDisplaySettings: (_, { serverRoute }) => serverRoute, + }, + ], + flashMessages: [ + {}, + { + setServerResponseData: () => ({ success: [SUCCESS_MESSAGE] }), + setFlashMessages: (_, { flashMessages }) => flashMessages, + toggleFieldEditorModal: () => ({}), + resetDisplaySettingsState: () => ({}), + }, + ], + editFieldIndex: [ + null, + { + openEditDetailField: (_, openEditDetailField) => openEditDetailField, + toggleFieldEditorModal: () => null, + }, + ], + dataLoading: [ + true, + { + onInitializeDisplaySettings: () => false, + }, + ], + addFieldModalVisible: [ + false, + { + toggleFieldEditorModal: (addFieldModalVisible) => !addFieldModalVisible, + openEditDetailField: () => true, + updateDetailField: () => false, + addDetailField: () => false, + }, + ], + titleFieldHover: [ + false, + { + toggleTitleFieldHover: (titleFieldHover) => !titleFieldHover, + }, + ], + urlFieldHover: [ + false, + { + toggleUrlFieldHover: (urlFieldHover) => !urlFieldHover, + }, + ], + subtitleFieldHover: [ + false, + { + toggleSubtitleFieldHover: (subtitleFieldHover) => !subtitleFieldHover, + }, + ], + descriptionFieldHover: [ + false, + { + toggleDescriptionFieldHover: (addFieldModalVisible) => !addFieldModalVisible, + }, + ], + }, + selectors: ({ selectors }) => ({ + fieldOptions: [ + () => [selectors.schemaFields], + (schemaFields) => Object.keys(schemaFields).map(euiSelectObjectFromValue), + ], + optionalFieldOptions: [ + () => [selectors.fieldOptions], + (fieldOptions) => { + const optionalFieldOptions = cloneDeep(fieldOptions); + optionalFieldOptions.unshift({ value: '', text: '' }); + return optionalFieldOptions; + }, + ], + // We don't want to let the user add a duplicate detailField. + availableFieldOptions: [ + () => [selectors.fieldOptions, selectors.searchResultConfig], + (fieldOptions, { detailFields }) => { + const usedFields = detailFields.map((usedField) => + euiSelectObjectFromValue(usedField.fieldName) + ); + return differenceBy(fieldOptions, usedFields, 'value'); + }, + ], + unsavedChanges: [ + () => [selectors.searchResultConfig, selectors.serverSearchResultConfig], + (uiConfig, serverConfig) => !isEqual(uiConfig, serverConfig), + ], + }), + listeners: ({ actions, values }) => ({ + initializeDisplaySettings: () => { + const { isOrganization } = AppLogic.values; + const { + contentSource: { id: sourceId }, + } = SourceLogic.values; + + const serverRoute = isOrganization + ? routes.fritoPieOrganizationContentSourceDisplaySettingsConfigPath(sourceId) + : routes.fritoPieAccountContentSourceDisplaySettingsConfigPath(sourceId); + + http(serverRoute).then(({ data }) => + actions.onInitializeDisplaySettings({ isOrganization, sourceId, serverRoute, ...data }) + ); + }, + setServerData: () => { + const { searchResultConfig, serverRoute } = values; + http + .post(serverRoute, searchResultConfig) + .then(({ data }) => actions.setServerResponseData(data)) + .catch(({ response }) => actions.setFlashMessages({ error: response.data.errors })); + }, + }), +}); + +// By default, the color is `null` on the server. The color is a required field and the +// EuiColorPicker components doesn't allow the field to be required so the form can be +// submitted with no color and this results in a server error. The default should be black +// and this allows the `searchResultConfig` and the `serverSearchResultConfig` reducers to +// stay synced on initialization. +const setDefaultColor = (searchResultConfig) => ({ + ...searchResultConfig, + color: searchResultConfig.color || '#000000', +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx index 5cebaad95e3a8..1283b0359adb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx @@ -6,4 +6,33 @@ import React from 'react'; -export const DisplaySettingsRouter: React.FC = () => <>Display Settings Placeholder; +import { useValues } from 'kea'; +import { Route, Switch } from 'react-router-dom'; + +import { AppLogic } from 'workplace_search/App/AppLogic'; + +import { + DISPLAY_SETTINGS_RESULT_DETAIL_PATH, + DISPLAY_SETTINGS_SEARCH_RESULT_PATH, + getSourcesPath, +} from 'workplace_search/utils/routePaths'; + +import { DisplaySettings } from './DisplaySettings'; + +export const DisplaySettingsRouter: React.FC = () => { + const { isOrganization } = useValues(AppLogic); + return ( + + } + /> + } + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx new file mode 100644 index 0000000000000..1a7a6e346fc62 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -0,0 +1,75 @@ +/* + * 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 from 'react'; + +import classNames from 'classnames'; +import { useValues } from 'kea'; + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; + +import { DisplaySettingsLogic } from './DisplaySettingsLogic'; + +import { CustomSourceIcon } from './CustomSourceIcon'; +import { TitleField } from './TitleField'; + +export const ExampleResultDetailCard: React.FC = () => { + const { + sourceName, + searchResultConfig: { titleField, urlField, color, detailFields }, + titleFieldHover, + urlFieldHover, + exampleDocuments, + } = useValues(DisplaySettingsLogic); + + const result = exampleDocuments[0]; + + return ( +
+
+
+
+ + + + + {sourceName} + +
+
+ +
+ {urlField ? ( +
{result[urlField]}
+ ) : ( + URL + )} +
+
+
+
+ {detailFields.length > 0 ? ( + detailFields.map(({ fieldName, label }, index) => ( +
+ +

{label}

+
+ +
{result[fieldName]}
+
+
+ )) + ) : ( + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx new file mode 100644 index 0000000000000..0749a7507f09a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -0,0 +1,74 @@ +/* + * 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 from 'react'; + +import { isColorDark, hexToRgb } from '@elastic/eui'; +import classNames from 'classnames'; +import { useValues } from 'kea'; + +import { DisplaySettingsLogic } from './DisplaySettingsLogic'; + +import { CustomSourceIcon } from './CustomSourceIcon'; +import { SubtitleField } from './SubtitleField'; +import { TitleField } from './TitleField'; + +export const ExampleSearchResultGroup: React.FC = () => { + const { + sourceName, + searchResultConfig: { titleField, subtitleField, descriptionField, color }, + titleFieldHover, + subtitleFieldHover, + descriptionFieldHover, + exampleDocuments, + } = useValues(DisplaySettingsLogic); + + return ( +
+
+ + + {sourceName} + +
+
+
+
+ {exampleDocuments.map((result, id) => ( +
+
+ + +
+ {descriptionField ? ( +
{result[descriptionField]}
+ ) : ( + Description + )} +
+
+
+ ))} +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx new file mode 100644 index 0000000000000..1d04302aef331 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -0,0 +1,66 @@ +/* + * 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 from 'react'; + +import classNames from 'classnames'; +import { useValues } from 'kea'; + +import { isColorDark, hexToRgb } from '@elastic/eui'; + +import { DisplaySettingsLogic } from './DisplaySettingsLogic'; + +import { CustomSourceIcon } from './CustomSourceIcon'; +import { SubtitleField } from './SubtitleField'; +import { TitleField } from './TitleField'; + +export const ExampleStandoutResult: React.FC = () => { + const { + sourceName, + searchResultConfig: { titleField, subtitleField, descriptionField, color }, + titleFieldHover, + subtitleFieldHover, + descriptionFieldHover, + exampleDocuments, + } = useValues(DisplaySettingsLogic); + + const result = exampleDocuments[0]; + + return ( +
+
+ + + {sourceName} + +
+
+
+ + +
+ {descriptionField ? ( + {result[descriptionField]} + ) : ( + Description + )} +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx new file mode 100644 index 0000000000000..34d7ff8ecbf97 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx @@ -0,0 +1,101 @@ +/* + * 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, { useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSelect, +} from '@elastic/eui'; + +import { DisplaySettingsLogic } from './DisplaySettingsLogic'; + +const emptyField = { fieldName: '', label: '' }; + +export const FieldEditorModal: React.FC = () => { + const { toggleFieldEditorModal, addDetailField, updateDetailField } = useActions( + DisplaySettingsLogic + ); + + const { + searchResultConfig: { detailFields }, + availableFieldOptions, + fieldOptions, + editFieldIndex, + } = useValues(DisplaySettingsLogic); + + const isEditing = editFieldIndex || editFieldIndex === 0; + const field = isEditing ? detailFields[editFieldIndex || 0] : emptyField; + const [fieldName, setName] = useState(field.fieldName || ''); + const [label, setLabel] = useState(field.label || ''); + + const handleSubmit = (e) => { + e.preventDefault(); + if (isEditing) { + updateDetailField({ fieldName, label }, editFieldIndex); + } else { + addDetailField({ fieldName, label }); + } + }; + + const ACTION_LABEL = isEditing ? 'Update' : 'Add'; + + return ( + +
+ + + {ACTION_LABEL} Field + + + + + setName(e.target.value)} + /> + + + setLabel(e.target.value)} + /> + + + + + Cancel + + {ACTION_LABEL} Field + + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts index f8c6834db7805..cbe699af22d61 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { DisplaySettingsRouter } from './display_settings_router'; +export { DisplaySettingsRouter } from './DisplaySettingsRouter'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx new file mode 100644 index 0000000000000..0bf48bce755e2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx @@ -0,0 +1,146 @@ +/* + * 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 from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; + +import { DisplaySettingsLogic } from './DisplaySettingsLogic'; + +import { ExampleResultDetailCard } from './ExampleResultDetailCard'; + +export const ResultDetail: React.FC = () => { + const { + toggleFieldEditorModal, + setDetailFields, + openEditDetailField, + removeDetailField, + } = useActions(DisplaySettingsLogic); + + const { + searchResultConfig: { detailFields }, + availableFieldOptions, + } = useValues(DisplaySettingsLogic); + + return ( + <> + + + + + + + <> + + + +

Visible Fields

+
+
+ + + Add Field + + +
+ + {detailFields.length > 0 ? ( + + + <> + {detailFields.map(({ fieldName, label }, index) => ( + + {(provided) => ( + + + +
+ +
+
+ + +

{fieldName}

+
+ +
“{label || ''}”
+
+
+ +
+ openEditDetailField(index)} + /> + removeDetailField(index)} + /> +
+
+
+
+ )} +
+ ))} + +
+
+ ) : ( +

Add fields and move them into the order you want them to appear.

+ )} + +
+
+
+ + + +

Preview

+
+ + +
+
+
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx new file mode 100644 index 0000000000000..0ca96daef0eb9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -0,0 +1,162 @@ +/* + * 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 from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiColorPicker, + EuiFlexGrid, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, + EuiSelect, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { DisplaySettingsLogic } from './DisplaySettingsLogic'; + +import { ExampleSearchResultGroup } from './ExampleSearchResultGroup'; +import { ExampleStandoutResult } from './ExampleStandoutResult'; + +export const SearchResults: React.FC = () => { + const { + toggleTitleFieldHover, + toggleSubtitleFieldHover, + toggleDescriptionFieldHover, + setTitleField, + setSubtitleField, + setDescriptionField, + setUrlField, + setColorField, + } = useActions(DisplaySettingsLogic); + + const { + searchResultConfig: { titleField, descriptionField, subtitleField, urlField, color }, + fieldOptions, + optionalFieldOptions, + } = useValues(DisplaySettingsLogic); + + return ( + <> + + + + + +

Search Result Settings

+
+ + + null} // FIXME + onBlur={() => null} // FIXME + > + setTitleField(e.target.value)} + /> + + + setUrlField(e.target.value)} + /> + + + null} // FIXME + onBlur={() => null} // FIXME + /> + + null} // FIXME + onBlur={() => null} // FIXME + > + setSubtitleField(value === '' ? null : value)} + /> + + null} // FIXME + onBlur={() => null} // FIXME + > + + setDescriptionField(value === '' ? null : value) + } + /> + + +
+ + + +

Preview

+
+ +
+ +

Featured Results

+
+

+ A matching document will appear as a single bold card. +

+
+ + +
+ +

Standard Results

+
+

+ Somewhat matching documents will appear as a set. +

+
+ +
+
+
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx new file mode 100644 index 0000000000000..7376e0df6374a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx @@ -0,0 +1,35 @@ +/* + * 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 from 'react'; + +import classNames from 'classnames'; + +import { IObject } from 'workplace_search/types'; + +interface SubtitleFieldProps { + result: IObject; + subtitleField: string | null; + subtitleFieldHover: boolean; +} + +export const SubtitleField: React.FC = ({ + result, + subtitleField, + subtitleFieldHover, +}) => ( +
+ {subtitleField ? ( +
{result[subtitleField]}
+ ) : ( + Subtitle + )} +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx new file mode 100644 index 0000000000000..234bedf3db29d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx @@ -0,0 +1,35 @@ +/* + * 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 from 'react'; + +import classNames from 'classnames'; + +import { IObject } from 'workplace_search/types'; + +interface TitleFieldProps { + result: IObject; + titleField: string | null; + titleFieldHover: boolean; +} + +export const TitleField: React.FC = ({ result, titleField, titleFieldHover }) => { + const title = titleField ? result[titleField] : ''; + const titleDisplay = Array.isArray(title) ? title.join(', ') : title; + return ( +
+ {titleField ? ( +
{titleDisplay}
+ ) : ( + Title + )} +
+ ); +}; From 3a08fa75599ca346189c07315f03713aa71757a0 Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 13:49:34 -0600 Subject: [PATCH 02/11] Add server routes --- .../routes/workplace_search/sources.test.ts | 152 ++++++++++++++++++ .../server/routes/workplace_search/sources.ts | 100 ++++++++++++ 2 files changed, 252 insertions(+) diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 22e2deaace1dc..62f4dceeac363 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -18,6 +18,7 @@ import { registerAccountPreSourceRoute, registerAccountPrepareSourcesRoute, registerAccountSourceSearchableRoute, + registerAccountSourceDisplaySettingsConfig, registerOrgSourcesRoute, registerOrgSourcesStatusRoute, registerOrgSourceRoute, @@ -29,6 +30,7 @@ import { registerOrgPreSourceRoute, registerOrgPrepareSourcesRoute, registerOrgSourceSearchableRoute, + registerOrgSourceDisplaySettingsConfig, registerOrgSourceOauthConfigurationsRoute, registerOrgSourceOauthConfigurationRoute, } from './sources'; @@ -446,6 +448,81 @@ describe('sources routes', () => { }); }); + describe('GET /api/workplace_search/account/sources/{id}/display_settings/config', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/{id}/display_settings/config', + payload: 'params', + }); + + registerAccountSourceDisplaySettingsConfig({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/display_settings/config', + }); + }); + }); + + describe('POST /api/workplace_search/account/sources/{id}/display_settings/config', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/account/sources/{id}/display_settings/config', + payload: 'body', + }); + + registerAccountSourceDisplaySettingsConfig({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: { + titleField: 'foo', + subtitleField: 'bar', + descriptionField: 'this is a thing', + urlField: 'http://youknowfor.search', + color: '#aaa', + detailFields: { + fieldName: 'myField', + label: 'My Field', + }, + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/display_settings/config', + body: mockRequest.body, + }); + }); + }); + describe('GET /api/workplace_search/org/sources', () => { let mockRouter: MockRouter; @@ -848,6 +925,81 @@ describe('sources routes', () => { }); }); + describe('GET /api/workplace_search/org/sources/{id}/display_settings/config', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/{id}/display_settings/config', + payload: 'params', + }); + + registerOrgSourceDisplaySettingsConfig({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/display_settings/config', + }); + }); + }); + + describe('POST /api/workplace_search/org/sources/{id}/display_settings/config', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/sources/{id}/display_settings/config', + payload: 'body', + }); + + registerOrgSourceDisplaySettingsConfig({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: { + titleField: 'foo', + subtitleField: 'bar', + descriptionField: 'this is a thing', + urlField: 'http://youknowfor.search', + color: '#aaa', + detailFields: { + fieldName: 'myField', + label: 'My Field', + }, + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/display_settings/config', + body: mockRequest.body, + }); + }); + }); + describe('GET /api/workplace_search/org/settings/connectors', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 24473388c03b1..1bc0edb34a65c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -285,6 +285,55 @@ export function registerAccountSourceSearchableRoute({ ); } +export function registerAccountSourceDisplaySettingsConfig({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/{id}/display_settings/config', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/display_settings/config`, + })(context, request, response); + } + ); + + router.post( + { + path: '/api/workplace_search/account/sources/{id}/display_settings/config', + validate: { + body: schema.object({ + titleField: schema.maybe(schema.string()), + subtitleField: schema.maybe(schema.string()), + descriptionField: schema.maybe(schema.string()), + urlField: schema.maybe(schema.string()), + color: schema.string(), + detailFields: schema.object({ + fieldName: schema.string(), + label: schema.string(), + }), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/display_settings/config`, + body: request.body, + })(context, request, response); + } + ); +} + export function registerOrgSourcesRoute({ router, enterpriseSearchRequestHandler, @@ -545,6 +594,55 @@ export function registerOrgSourceSearchableRoute({ ); } +export function registerOrgSourceDisplaySettingsConfig({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/{id}/display_settings/config', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/display_settings/config`, + })(context, request, response); + } + ); + + router.post( + { + path: '/api/workplace_search/org/sources/{id}/display_settings/config', + validate: { + body: schema.object({ + titleField: schema.maybe(schema.string()), + subtitleField: schema.maybe(schema.string()), + descriptionField: schema.maybe(schema.string()), + urlField: schema.maybe(schema.string()), + color: schema.string(), + detailFields: schema.object({ + fieldName: schema.string(), + label: schema.string(), + }), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/display_settings/config`, + body: request.body, + })(context, request, response); + } + ); +} + export function registerOrgSourceOauthConfigurationsRoute({ router, enterpriseSearchRequestHandler, @@ -647,6 +745,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerAccountPreSourceRoute(dependencies); registerAccountPrepareSourcesRoute(dependencies); registerAccountSourceSearchableRoute(dependencies); + registerAccountSourceDisplaySettingsConfig(dependencies); registerOrgSourcesRoute(dependencies); registerOrgSourcesStatusRoute(dependencies); registerOrgSourceRoute(dependencies); @@ -658,6 +757,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerOrgPreSourceRoute(dependencies); registerOrgPrepareSourcesRoute(dependencies); registerOrgSourceSearchableRoute(dependencies); + registerOrgSourceDisplaySettingsConfig(dependencies); registerOrgSourceOauthConfigurationsRoute(dependencies); registerOrgSourceOauthConfigurationRoute(dependencies); }; From d839e1624f6f67477e2879f6846b58e3329b037b Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 13:53:07 -0600 Subject: [PATCH 03/11] Remove reference to shared lib This one-liner appears only once in ent-search so adding it here in the logic file` --- .../components/display_settings/display_settings_logic.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index b6d7c20233709..f6a0d00f120b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -10,7 +10,6 @@ import { DropResult } from 'react-beautiful-dnd'; import { kea, MakeLogicType } from 'kea'; import http from 'shared/http'; -import { euiSelectObjectFromValue } from 'shared/utils'; import routes from 'workplace_search/routes'; import { AppLogic } from 'workplace_search/App/AppLogic'; @@ -321,6 +320,8 @@ export const DisplaySettingsLogic = kea< }), }); +const euiSelectObjectFromValue = (value: string) => ({ text: value, value }); + // By default, the color is `null` on the server. The color is a required field and the // EuiColorPicker components doesn't allow the field to be required so the form can be // submitted with no color and this results in a server error. The default should be black From 0103582f7978208d5aa5cfe61424fd0238225fc2 Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 14:02:56 -0600 Subject: [PATCH 04/11] Fix paths --- .../display_settings/display_settings.tsx | 17 +++++++++-------- .../display_settings/display_settings_logic.ts | 9 +++++---- .../display_settings_router.tsx | 6 +++--- .../example_result_detail_card.tsx | 6 +++--- .../example_search_result_group.tsx | 8 ++++---- .../example_standout_result.tsx | 8 ++++---- .../display_settings/field_editor_modal.tsx | 2 +- .../components/display_settings/index.ts | 2 +- .../display_settings/result_detail.tsx | 4 ++-- .../display_settings/search_results.tsx | 6 +++--- 10 files changed, 35 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index 518704acabaaa..2f4165e485090 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -12,22 +12,23 @@ import { useHistory } from 'react-router-dom'; import { EuiButton, EuiEmptyPrompt, EuiTabbedContent, EuiPanel } from '@elastic/eui'; +import FlashMessages from 'shared/components/FlashMessages'; import { DISPLAY_SETTINGS_RESULT_DETAIL_PATH, DISPLAY_SETTINGS_SEARCH_RESULT_PATH, getContentSourcePath, -} from 'workplace_search/utils/routePaths'; +} from '../../../../routes'; -import { AppLogic } from 'workplace_search/App/AppLogic'; +import { AppLogic } from '../../../../app_logic'; -import FlashMessages from 'shared/components/FlashMessages'; +import { Loading } from '../../../../../shared/loading'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { Loading, ViewContentHeader } from 'workplace_search/components'; -import { DisplaySettingsLogic } from './DisplaySettingsLogic'; +import { DisplaySettingsLogic } from './display_settings_logic'; -import { FieldEditorModal } from './FieldEditorModal'; -import { ResultDetail } from './ResultDetail'; -import { SearchResults } from './SearchResults'; +import { FieldEditorModal } from './field_editor_modal'; +import { ResultDetail } from './result_detail'; +import { SearchResults } from './search_results'; const UNSAVED_MESSAGE = 'Your display settings have not been saved. Are you sure you want to leave?'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index f6a0d00f120b1..77c98f37f297a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -9,16 +9,17 @@ import { DropResult } from 'react-beautiful-dnd'; import { kea, MakeLogicType } from 'kea'; import http from 'shared/http'; +import { IFlashMessagesProps } from 'shared/types'; + import routes from 'workplace_search/routes'; -import { AppLogic } from 'workplace_search/App/AppLogic'; -import { SourceLogic } from 'workplace_search/ContentSources/SourceLogic'; +import { AppLogic } from '../../../../app_logic'; +import { SourceLogic } from '../../source_logic'; const SUCCESS_MESSAGE = 'Display Settings have been successfuly updated.'; -import { IFlashMessagesProps } from 'shared/types'; -import { IObject, DetailField, SearchResultConfig, OptionValue } from 'workplace_search/types'; +import { IObject, DetailField, SearchResultConfig, OptionValue } from '../../../../types'; export interface DisplaySettingsResponseProps { sourceName: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx index 1283b0359adb7..01ac93735b8a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx @@ -9,15 +9,15 @@ import React from 'react'; import { useValues } from 'kea'; import { Route, Switch } from 'react-router-dom'; -import { AppLogic } from 'workplace_search/App/AppLogic'; +import { AppLogic } from '../../../../app_logic'; import { DISPLAY_SETTINGS_RESULT_DETAIL_PATH, DISPLAY_SETTINGS_SEARCH_RESULT_PATH, getSourcesPath, -} from 'workplace_search/utils/routePaths'; +} from '../../../../routes'; -import { DisplaySettings } from './DisplaySettings'; +import { DisplaySettings } from './display_settings'; export const DisplaySettingsRouter: React.FC = () => { const { isOrganization } = useValues(AppLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx index 1a7a6e346fc62..468f7d2f7ad05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -11,10 +11,10 @@ import { useValues } from 'kea'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { DisplaySettingsLogic } from './DisplaySettingsLogic'; +import { DisplaySettingsLogic } from './display_settings_logic'; -import { CustomSourceIcon } from './CustomSourceIcon'; -import { TitleField } from './TitleField'; +import { CustomSourceIcon } from './custom_source_icon'; +import { TitleField } from './title_field'; export const ExampleResultDetailCard: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx index 0749a7507f09a..14239b1654308 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -10,11 +10,11 @@ import { isColorDark, hexToRgb } from '@elastic/eui'; import classNames from 'classnames'; import { useValues } from 'kea'; -import { DisplaySettingsLogic } from './DisplaySettingsLogic'; +import { DisplaySettingsLogic } from './display_settings_logic'; -import { CustomSourceIcon } from './CustomSourceIcon'; -import { SubtitleField } from './SubtitleField'; -import { TitleField } from './TitleField'; +import { CustomSourceIcon } from './custom_source_icon'; +import { SubtitleField } from './subtitle_field'; +import { TitleField } from './title_field'; export const ExampleSearchResultGroup: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx index 1d04302aef331..4ef3b1fe14b93 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -11,11 +11,11 @@ import { useValues } from 'kea'; import { isColorDark, hexToRgb } from '@elastic/eui'; -import { DisplaySettingsLogic } from './DisplaySettingsLogic'; +import { DisplaySettingsLogic } from './display_settings_logic'; -import { CustomSourceIcon } from './CustomSourceIcon'; -import { SubtitleField } from './SubtitleField'; -import { TitleField } from './TitleField'; +import { CustomSourceIcon } from './custom_source_icon'; +import { SubtitleField } from './subtitle_field'; +import { TitleField } from './title_field'; export const ExampleStandoutResult: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx index 34d7ff8ecbf97..6211fd3acd6a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx @@ -23,7 +23,7 @@ import { EuiSelect, } from '@elastic/eui'; -import { DisplaySettingsLogic } from './DisplaySettingsLogic'; +import { DisplaySettingsLogic } from './display_settings_logic'; const emptyField = { fieldName: '', label: '' }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts index cbe699af22d61..f8c6834db7805 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { DisplaySettingsRouter } from './DisplaySettingsRouter'; +export { DisplaySettingsRouter } from './display_settings_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx index 0bf48bce755e2..cb65d8ef671e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx @@ -26,9 +26,9 @@ import { EuiTitle, } from '@elastic/eui'; -import { DisplaySettingsLogic } from './DisplaySettingsLogic'; +import { DisplaySettingsLogic } from './display_settings_logic'; -import { ExampleResultDetailCard } from './ExampleResultDetailCard'; +import { ExampleResultDetailCard } from './example_result_detail_card'; export const ResultDetail: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index 0ca96daef0eb9..ffffc79481041 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -20,10 +20,10 @@ import { EuiTitle, } from '@elastic/eui'; -import { DisplaySettingsLogic } from './DisplaySettingsLogic'; +import { DisplaySettingsLogic } from './display_settings_logic'; -import { ExampleSearchResultGroup } from './ExampleSearchResultGroup'; -import { ExampleStandoutResult } from './ExampleStandoutResult'; +import { ExampleSearchResultGroup } from './example_search_result_group'; +import { ExampleStandoutResult } from './example_standout_result'; export const SearchResults: React.FC = () => { const { From 78f5f0a465e8c3496607d74f50b0f3f5070410cb Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 14:07:37 -0600 Subject: [PATCH 05/11] Add types and fix TypeScript issues --- .../applications/workplace_search/types.ts | 23 +++++++++++++ .../display_settings/display_settings.tsx | 18 +++++++---- .../display_settings_logic.ts | 32 ++++++++++--------- .../display_settings/field_editor_modal.tsx | 4 +-- .../display_settings/subtitle_field.tsx | 4 +-- .../display_settings/title_field.tsx | 4 +-- 6 files changed, 57 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 73e7f7ed701d8..9bda686ebbf00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -181,3 +181,26 @@ export interface CustomSource { name: string; id: string; } + +export interface Result { + [key: string]: string; +} + +export interface OptionValue { + value: string; + text: string; +} + +export interface DetailField { + fieldName: string; + label: string; +} + +export interface SearchResultConfig { + titleField: string | null; + subtitleField: string | null; + descriptionField: string | null; + urlField: string | null; + color: string; + detailFields: DetailField[]; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index 2f4165e485090..d8be8834268ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -4,13 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { FormEvent, useEffect } from 'react'; import { History } from 'history'; import { useActions, useValues } from 'kea'; import { useHistory } from 'react-router-dom'; -import { EuiButton, EuiEmptyPrompt, EuiTabbedContent, EuiPanel } from '@elastic/eui'; +import { + EuiButton, + EuiEmptyPrompt, + EuiTabbedContent, + EuiPanel, + EuiTabbedContentTab, +} from '@elastic/eui'; import FlashMessages from 'shared/components/FlashMessages'; import { @@ -74,18 +80,16 @@ export const DisplaySettings: React.FC = ({ tabId }) => { { id: 'search_results', name: 'Search Results', - disabled: false, content: , }, { id: 'result_detail', name: 'Result Detail', - disabled: false, content: , }, - ]; + ] as EuiTabbedContentTab[]; - const onSelectedTabChanged = (tab) => { + const onSelectedTabChanged = (tab: EuiTabbedContentTab) => { const path = tab.id === tabs[1].id ? getContentSourcePath(DISPLAY_SETTINGS_RESULT_DETAIL_PATH, sourceId, isOrganization) @@ -94,7 +98,7 @@ export const DisplaySettings: React.FC = ({ tabId }) => { history.push(path); }; - const handleFormSubmit = (e) => { + const handleFormSubmit = (e: FormEvent) => { e.preventDefault(); setServerData(); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index 77c98f37f297a..474cdc889a925 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -11,7 +11,6 @@ import { kea, MakeLogicType } from 'kea'; import http from 'shared/http'; import { IFlashMessagesProps } from 'shared/types'; - import routes from 'workplace_search/routes'; import { AppLogic } from '../../../../app_logic'; @@ -19,13 +18,13 @@ import { SourceLogic } from '../../source_logic'; const SUCCESS_MESSAGE = 'Display Settings have been successfuly updated.'; -import { IObject, DetailField, SearchResultConfig, OptionValue } from '../../../../types'; +import { DetailField, SearchResultConfig, OptionValue } from '../../../../types'; export interface DisplaySettingsResponseProps { sourceName: string; searchResultConfig: SearchResultConfig; - schemaFields: IObject; - exampleDocuments: IObject[]; + schemaFields: object; + exampleDocuments: object[]; } export interface DisplaySettingsInitialData extends DisplaySettingsResponseProps { @@ -47,12 +46,15 @@ interface DisplaySettingsActions { setUrlField(urlField: string): string; setSubtitleField(subtitleField: string | null): string | null; setDescriptionField(descriptionField: string | null): string | null; - setColorField(hex: string); - setDetailFields(result: DropResult); - openEditDetailField(editFieldIndex: number | null); - removeDetailField(index: number); - addDetailField(newField: DetailField); - updateDetailField(updatedField: DetailField, index: number | null); + setColorField(hex: string): string; + setDetailFields(result: DropResult): { result: DropResult }; + openEditDetailField(editFieldIndex: number | null): number | null; + removeDetailField(index: number): number; + addDetailField(newField: DetailField): DetailField; + updateDetailField( + updatedField: DetailField, + index: number | null + ): { updatedField: DetailField; index: number }; toggleFieldEditorModal(): void; toggleTitleFieldHover(): void; toggleSubtitleFieldHover(): void; @@ -64,8 +66,8 @@ interface DisplaySettingsActions { interface DisplaySettingsValues { sourceName: string; sourceId: string; - schemaFields: IObject; - exampleDocuments: IObject[]; + schemaFields: object; + exampleDocuments: object[]; serverSearchResultConfig: SearchResultConfig; searchResultConfig: SearchResultConfig; serverRoute: string; @@ -174,7 +176,7 @@ export const DisplaySettingsLogic = kea< const detailFields = cloneDeep(searchResultConfig.detailFields); const element = detailFields[source.index]; detailFields.splice(source.index, 1); - detailFields.splice(destination.index, 0, element); + detailFields.splice(destination!.index, 0, element); return { ...searchResultConfig, detailFields, @@ -285,7 +287,7 @@ export const DisplaySettingsLogic = kea< availableFieldOptions: [ () => [selectors.fieldOptions, selectors.searchResultConfig], (fieldOptions, { detailFields }) => { - const usedFields = detailFields.map((usedField) => + const usedFields = detailFields.map((usedField: DetailField) => euiSelectObjectFromValue(usedField.fieldName) ); return differenceBy(fieldOptions, usedFields, 'value'); @@ -328,7 +330,7 @@ const euiSelectObjectFromValue = (value: string) => ({ text: value, value }); // submitted with no color and this results in a server error. The default should be black // and this allows the `searchResultConfig` and the `serverSearchResultConfig` reducers to // stay synced on initialization. -const setDefaultColor = (searchResultConfig) => ({ +const setDefaultColor = (searchResultConfig: SearchResultConfig) => ({ ...searchResultConfig, color: searchResultConfig.color || '#000000', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx index 6211fd3acd6a9..587916a741d66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { FormEvent, useState } from 'react'; import { useActions, useValues } from 'kea'; @@ -44,7 +44,7 @@ export const FieldEditorModal: React.FC = () => { const [fieldName, setName] = useState(field.fieldName || ''); const [label, setLabel] = useState(field.label || ''); - const handleSubmit = (e) => { + const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (isEditing) { updateDetailField({ fieldName, label }, editFieldIndex); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx index 7376e0df6374a..e27052ddffc04 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx @@ -8,10 +8,10 @@ import React from 'react'; import classNames from 'classnames'; -import { IObject } from 'workplace_search/types'; +import { Result } from '../../../../types'; interface SubtitleFieldProps { - result: IObject; + result: Result; subtitleField: string | null; subtitleFieldHover: boolean; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx index 234bedf3db29d..a54c0977b464f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx @@ -8,10 +8,10 @@ import React from 'react'; import classNames from 'classnames'; -import { IObject } from 'workplace_search/types'; +import { Result } from '../../../../types'; interface TitleFieldProps { - result: IObject; + result: Result; titleField: string | null; titleFieldHover: boolean; } From cb2a799644b5a3da6d4a8e1795e46a8483b710c1 Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 14:24:34 -0600 Subject: [PATCH 06/11] Replace FlashMessages with global component --- .../display_settings/display_settings.tsx | 3 --- .../display_settings_logic.ts | 23 ++++++++----------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index d8be8834268ad..1d51c3521c81d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -18,7 +18,6 @@ import { EuiTabbedContentTab, } from '@elastic/eui'; -import FlashMessages from 'shared/components/FlashMessages'; import { DISPLAY_SETTINGS_RESULT_DETAIL_PATH, DISPLAY_SETTINGS_SEARCH_RESULT_PATH, @@ -51,7 +50,6 @@ export const DisplaySettings: React.FC = ({ tabId }) => { const { dataLoading, - flashMessages, sourceId, addFieldModalVisible, unsavedChanges, @@ -117,7 +115,6 @@ export const DisplaySettings: React.FC = ({ tabId }) => { ) : null } /> - {!!flashMessages && } {hasDocuments ? ( displaySettingsProps, - setFlashMessages: (flashMessages: IFlashMessagesProps) => ({ flashMessages }), setTitleField: (titleField: string) => titleField, setUrlField: (urlField: string) => urlField, setSubtitleField: (subtitleField: string | null) => subtitleField, @@ -214,15 +211,6 @@ export const DisplaySettingsLogic = kea< onInitializeDisplaySettings: (_, { serverRoute }) => serverRoute, }, ], - flashMessages: [ - {}, - { - setServerResponseData: () => ({ success: [SUCCESS_MESSAGE] }), - setFlashMessages: (_, { flashMessages }) => flashMessages, - toggleFieldEditorModal: () => ({}), - resetDisplaySettingsState: () => ({}), - }, - ], editFieldIndex: [ null, { @@ -320,6 +308,15 @@ export const DisplaySettingsLogic = kea< .then(({ data }) => actions.setServerResponseData(data)) .catch(({ response }) => actions.setFlashMessages({ error: response.data.errors })); }, + setServerResponseData: () => { + setSuccessMessage(SUCCESS_MESSAGE); + }, + toggleFieldEditorModal: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + resetDisplaySettingsState: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, }), }); From 9d9c6527c76c3fc653f2cf10449052c05dad7f4b Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 14:27:55 -0600 Subject: [PATCH 07/11] More explicit Result type --- .../components/display_settings/display_settings_logic.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index cb3f5e23aa2ce..ceea681c95ac7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -18,13 +18,13 @@ import { SourceLogic } from '../../source_logic'; const SUCCESS_MESSAGE = 'Display Settings have been successfuly updated.'; -import { DetailField, SearchResultConfig, OptionValue } from '../../../../types'; +import { DetailField, SearchResultConfig, OptionValue, Result } from '../../../../types'; export interface DisplaySettingsResponseProps { sourceName: string; searchResultConfig: SearchResultConfig; schemaFields: object; - exampleDocuments: object[]; + exampleDocuments: Result[]; } export interface DisplaySettingsInitialData extends DisplaySettingsResponseProps { @@ -66,7 +66,7 @@ interface DisplaySettingsValues { sourceName: string; sourceId: string; schemaFields: object; - exampleDocuments: object[]; + exampleDocuments: Result[]; serverSearchResultConfig: SearchResultConfig; searchResultConfig: SearchResultConfig; serverRoute: string; From c51bd322af2936167437dd3248f4f2f1dfb1eeb0 Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 14:33:01 -0600 Subject: [PATCH 08/11] Remove routes/http in favor of HttpLogic --- .../display_settings_logic.ts | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index ceea681c95ac7..c52665524f566 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -8,10 +8,14 @@ import { cloneDeep, isEqual, differenceBy } from 'lodash'; import { DropResult } from 'react-beautiful-dnd'; import { kea, MakeLogicType } from 'kea'; -import http from 'shared/http'; -import routes from 'workplace_search/routes'; -import { setSuccessMessage, FlashMessagesLogic } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; + +import { + setSuccessMessage, + FlashMessagesLogic, + flashAPIErrors, +} from '../../../../../shared/flash_messages'; import { AppLogic } from '../../../../app_logic'; import { SourceLogic } from '../../source_logic'; @@ -287,26 +291,39 @@ export const DisplaySettingsLogic = kea< ], }), listeners: ({ actions, values }) => ({ - initializeDisplaySettings: () => { + initializeDisplaySettings: async () => { const { isOrganization } = AppLogic.values; const { contentSource: { id: sourceId }, } = SourceLogic.values; - const serverRoute = isOrganization - ? routes.fritoPieOrganizationContentSourceDisplaySettingsConfigPath(sourceId) - : routes.fritoPieAccountContentSourceDisplaySettingsConfigPath(sourceId); + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/display_settings/config` + : `/api/workplace_search/account/sources/${sourceId}/display_settings/config`; - http(serverRoute).then(({ data }) => - actions.onInitializeDisplaySettings({ isOrganization, sourceId, serverRoute, ...data }) - ); + try { + const response = await HttpLogic.values.http.get(route); + actions.onInitializeDisplaySettings({ + isOrganization, + sourceId, + serverRoute: route, + ...response, + }); + } catch (e) { + flashAPIErrors(e); + } }, - setServerData: () => { + setServerData: async () => { const { searchResultConfig, serverRoute } = values; - http - .post(serverRoute, searchResultConfig) - .then(({ data }) => actions.setServerResponseData(data)) - .catch(({ response }) => actions.setFlashMessages({ error: response.data.errors })); + + try { + const response = await HttpLogic.values.http.post(serverRoute, { + body: JSON.stringify({ ...searchResultConfig }), + }); + actions.setServerResponseData(response); + } catch (e) { + flashAPIErrors(e); + } }, setServerResponseData: () => { setSuccessMessage(SUCCESS_MESSAGE); From 2a5bdba876feb0251243b5a533a2f64ba859a563 Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 15:58:48 -0600 Subject: [PATCH 09/11] Fix server routes `urlFieldIsLinkable` was missing and `detailFields` can either be an object or an array of objects --- .../server/routes/workplace_search/sources.ts | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 1bc0edb34a65c..d43a4252c7e1f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -25,6 +25,21 @@ const oAuthConfigSchema = schema.object({ consumer_key: schema.string(), }); +const displayFieldSchema = schema.object({ + fieldName: schema.string(), + label: schema.string(), +}); + +const displaySettingsSchema = schema.object({ + titleField: schema.maybe(schema.string()), + subtitleField: schema.maybe(schema.string()), + descriptionField: schema.maybe(schema.string()), + urlField: schema.maybe(schema.string()), + color: schema.string(), + urlFieldIsLinkable: schema.boolean(), + detailFields: schema.oneOf([schema.arrayOf(displayFieldSchema), displayFieldSchema]), +}); + export function registerAccountSourcesRoute({ router, enterpriseSearchRequestHandler, @@ -309,17 +324,7 @@ export function registerAccountSourceDisplaySettingsConfig({ { path: '/api/workplace_search/account/sources/{id}/display_settings/config', validate: { - body: schema.object({ - titleField: schema.maybe(schema.string()), - subtitleField: schema.maybe(schema.string()), - descriptionField: schema.maybe(schema.string()), - urlField: schema.maybe(schema.string()), - color: schema.string(), - detailFields: schema.object({ - fieldName: schema.string(), - label: schema.string(), - }), - }), + body: displaySettingsSchema, params: schema.object({ id: schema.string(), }), @@ -618,17 +623,7 @@ export function registerOrgSourceDisplaySettingsConfig({ { path: '/api/workplace_search/org/sources/{id}/display_settings/config', validate: { - body: schema.object({ - titleField: schema.maybe(schema.string()), - subtitleField: schema.maybe(schema.string()), - descriptionField: schema.maybe(schema.string()), - urlField: schema.maybe(schema.string()), - color: schema.string(), - detailFields: schema.object({ - fieldName: schema.string(), - label: schema.string(), - }), - }), + body: displaySettingsSchema, params: schema.object({ id: schema.string(), }), From e3d851d909adc961c11d6c603c611be26d172143 Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 15:59:31 -0600 Subject: [PATCH 10/11] Add base styles These were ported from ent-search. Decided to use spacers where some global styles were missing. --- .../display_settings/display_settings.scss | 206 ++++++++++++++++++ .../display_settings/display_settings.tsx | 2 + .../display_settings/search_results.tsx | 2 + 3 files changed, 210 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss new file mode 100644 index 0000000000000..27935104f4ef6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss @@ -0,0 +1,206 @@ +/* + * 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. + */ + +// -------------------------------------------------- +// Custom Source display settings +// -------------------------------------------------- + +@mixin source_name { + font-size: .6875em; + text-transform: uppercase; + font-weight: 600; + letter-spacing: 0.06em; +} + +@mixin example_result_box_shadow { + box-shadow: + 0 1px 3px rgba(black, 0.1), + 0 0 20px $euiColorLightestShade; +} + +// Wrapper +.custom-source-display-settings { + font-size: 16px; +} + +// Example result content +.example-result-content { + & > * { + line-height: 1.5em; + } + + &__title { + font-size: 1em; + font-weight: 600; + color: $euiColorPrimary; + + .example-result-detail-card & { + font-size: 20px; + } + } + + &__subtitle, + &__description { + font-size: .875; + } + + &__subtitle { + color: $euiColorDarkestShade; + } + + &__description { + padding: .1rem 0 .125rem .35rem; + border-left: 3px solid $euiColorLightShade; + color: $euiColorDarkShade; + line-height: 1.8; + word-break: break-word; + + @supports (display: -webkit-box) { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__url { + .example-result-detail-card & { + color: $euiColorDarkShade; + } + } +} + +.example-result-content-placeholder { + color: $euiColorMediumShade; +} + +// Example standout result +.example-standout-result { + border-radius: 4px; + overflow: hidden; + @include example_result_box_shadow; + + &__header, + &__content { + padding-left: 1em; + padding-right: 1em; + } + + &__content { + padding-top: 1em; + padding-bottom: 1em; + } + + &__source-name { + line-height: 34px; + @include source_name; + } +} + +// Example result group +.example-result-group { + &__header { + padding: 0 .5em; + border-radius: 4px; + display: inline-flex; + align-items: center; + + .euiIcon { + margin-right: .25rem; + } + } + + &__source-name { + line-height: 1.75em; + @include source_name; + } + + &__content { + display: flex; + align-items: stretch; + padding: .75em 0; + } + + &__border { + width: 4px; + border-radius: 2px; + flex-shrink: 0; + margin-left: .875rem; + } + + &__results { + flex: 1; + max-width: 100%; + } +} + +.example-grouped-result { + padding: 1em; +} + +.example-result-field-hover { + background: lighten($euiColorVis1_behindText, 30%); + position: relative; + + &:before, + &:after { + content: ''; + position: absolute; + height: 100%; + width: 4px; + background: lighten($euiColorVis1_behindText, 30%); + } + + &:before { + right: 100%; + border-radius: 2px 0 0 2px; + } + + &:after { + left: 100%; + border-radius: 0 2px 2px 0; + } + + .example-result-content-placeholder { + color: $euiColorFullShade; + } +} + +.example-result-detail-card { + @include example_result_box_shadow; + + &__header { + position: relative; + padding: 1.25em 1em 0; + } + + &__border { + height: 4px; + position: absolute; + top: 0; + right: 0; + left: 0; + } + + &__source-name { + margin-bottom: 1em; + font-weight: 500; + } + + &__field { + padding: 1em; + + & + & { + border-top: 1px solid $euiColorLightShade; + } + } +} + +.visible-fields-container { + background: $euiColorLightestShade; + border-color: transparent; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index 1d51c3521c81d..e34728beef5e5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -10,6 +10,8 @@ import { History } from 'history'; import { useActions, useValues } from 'kea'; import { useHistory } from 'react-router-dom'; +import './display_settings.scss'; + import { EuiButton, EuiEmptyPrompt, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index ffffc79481041..cfe0ddb1533ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -143,6 +143,7 @@ export const SearchResults: React.FC = () => { A matching document will appear as a single bold card.

+
@@ -153,6 +154,7 @@ export const SearchResults: React.FC = () => { Somewhat matching documents will appear as a set.

+ From 5d08fe93facfea3008f6d8b6519d7b45e77c87f3 Mon Sep 17 00:00:00 2001 From: scottybollinger Date: Tue, 24 Nov 2020 16:17:01 -0600 Subject: [PATCH 11/11] Kibana prefers underscores in URLs Was only going to do display-settings and result-details but decided to YOLO all of them --- .../applications/workplace_search/routes.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 3ec22ede888ab..14c288de5a0c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -46,7 +46,7 @@ export const ENT_SEARCH_LICENSE_MANAGEMENT = `${ENT_SEARCH_DOCS_PREFIX}/license- export const PERSONAL_PATH = '/p'; -export const ROLE_MAPPINGS_PATH = '/role-mappings'; +export const ROLE_MAPPINGS_PATH = '/role_mappings'; export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`; export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`; @@ -63,18 +63,18 @@ export const PERSONAL_SOURCES_PATH = `${PERSONAL_PATH}${SOURCES_PATH}`; export const SOURCE_ADDED_PATH = `${SOURCES_PATH}/added`; export const ADD_SOURCE_PATH = `${SOURCES_PATH}/add`; export const ADD_BOX_PATH = `${SOURCES_PATH}/add/box`; -export const ADD_CONFLUENCE_PATH = `${SOURCES_PATH}/add/confluence-cloud`; -export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence-server`; +export const ADD_CONFLUENCE_PATH = `${SOURCES_PATH}/add/confluence_cloud`; +export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence_server`; export const ADD_DROPBOX_PATH = `${SOURCES_PATH}/add/dropbox`; -export const ADD_GITHUB_ENTERPRISE_PATH = `${SOURCES_PATH}/add/github-enterprise-server`; +export const ADD_GITHUB_ENTERPRISE_PATH = `${SOURCES_PATH}/add/github_enterprise_server`; export const ADD_GITHUB_PATH = `${SOURCES_PATH}/add/github`; export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; -export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google-drive`; -export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira-cloud`; -export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira-server`; +export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google_drive`; +export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira_cloud`; +export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira_server`; export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/onedrive`; export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; -export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce-sandbox`; +export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce_sandbox`; export const ADD_SERVICENOW_PATH = `${SOURCES_PATH}/add/servicenow`; export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/sharepoint`; export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; @@ -86,30 +86,30 @@ export const PERSONAL_SETTINGS_PATH = `${PERSONAL_PATH}/settings`; export const SOURCE_DETAILS_PATH = `${SOURCES_PATH}/:sourceId`; export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`; export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`; -export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display-settings`; +export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display_settings`; export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`; -export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema-errors/:activeReindexJobId`; +export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema_errors/:activeReindexJobId`; export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`; -export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result-detail`; +export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result_detail`; export const ORG_SETTINGS_PATH = '/settings'; export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`; export const ORG_SETTINGS_CONNECTORS_PATH = `${ORG_SETTINGS_PATH}/connectors`; export const ORG_SETTINGS_OAUTH_APPLICATION_PATH = `${ORG_SETTINGS_PATH}/oauth`; export const EDIT_BOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/box/edit`; -export const EDIT_CONFLUENCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-cloud/edit`; -export const EDIT_CONFLUENCE_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-server/edit`; +export const EDIT_CONFLUENCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence_cloud/edit`; +export const EDIT_CONFLUENCE_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence_server/edit`; export const EDIT_DROPBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/dropbox/edit`; -export const EDIT_GITHUB_ENTERPRISE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github-enterprise-server/edit`; +export const EDIT_GITHUB_ENTERPRISE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github_enterprise_server/edit`; export const EDIT_GITHUB_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github/edit`; export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; -export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google-drive/edit`; -export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-cloud/edit`; -export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-server/edit`; +export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google_drive/edit`; +export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_cloud/edit`; +export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_server/edit`; export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/onedrive/edit`; export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; -export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce-sandbox/edit`; +export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce_sandbox/edit`; export const EDIT_SERVICENOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/servicenow/edit`; export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/sharepoint/edit`; export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`;