+
{icon}
) : (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
index 1846115d73900..327ee7b30582b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
@@ -25,15 +25,27 @@ export const NAV = {
'xpack.enterpriseSearch.workplaceSearch.nav.groups.sourcePrioritization',
{ defaultMessage: 'Source Prioritization' }
),
+ CONTENT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.content', {
+ defaultMessage: 'Content',
+ }),
ROLE_MAPPINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', {
defaultMessage: 'Role Mappings',
}),
SECURITY: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', {
defaultMessage: 'Security',
}),
+ SCHEMA: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.schema', {
+ defaultMessage: 'Schema',
+ }),
+ DISPLAY_SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.displaySettings', {
+ defaultMessage: 'Display Settings',
+ }),
SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.settings', {
defaultMessage: 'Settings',
}),
+ ADD_SOURCE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.addSource', {
+ defaultMessage: 'Add Source',
+ }),
PERSONAL_DASHBOARD: i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.nav.personalDashboard',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx
index 5f1e2dd18d3b6..20b15bcfc45ca 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx
@@ -57,7 +57,7 @@ describe('WorkplaceSearchConfigured', () => {
it('renders layout and header actions', () => {
const wrapper = shallow(
);
- expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy();
+ expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy();
expect(wrapper.find(Overview)).toHaveLength(1);
expect(mockKibanaValues.renderHeaderActions).toHaveBeenCalledWith(WorkplaceSearchHeaderActions);
});
@@ -90,6 +90,6 @@ describe('WorkplaceSearchConfigured', () => {
const wrapper = shallow(
);
- expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true);
+ expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
index 776cae24dfdfb..562a2ffb32888 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
@@ -16,13 +16,17 @@ import { AppLogic } from './app_logic';
import { Layout } from '../shared/layout';
import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout';
-import { GROUPS_PATH, SETUP_GUIDE_PATH } from './routes';
+import { GROUPS_PATH, SETUP_GUIDE_PATH, SOURCES_PATH, PERSONAL_SOURCES_PATH } from './routes';
import { SetupGuide } from './views/setup_guide';
import { ErrorState } from './views/error_state';
import { NotFound } from '../shared/not_found';
import { Overview } from './views/overview';
import { GroupsRouter } from './views/groups';
+import { SourcesRouter } from './views/content_sources';
+
+import { GroupSubNav } from './views/groups/components/group_sub_nav';
+import { SourceSubNav } from './views/content_sources/components/source_sub_nav';
export const WorkplaceSearch: React.FC
= (props) => {
const { config } = useValues(KibanaLogic);
@@ -37,6 +41,10 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
const { pathname } = useLocation();
+ // We don't want so show the subnavs on the container root pages.
+ const showSourcesSubnav = pathname !== SOURCES_PATH && pathname !== PERSONAL_SOURCES_PATH;
+ const showGroupsSubnav = pathname !== GROUPS_PATH;
+
/**
* Personal dashboard urls begin with /p/
* EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources
@@ -45,6 +53,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
// TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`.
const isOrganization = !pathname.match(personalSourceUrlRegex);
+ setContext(isOrganization);
useEffect(() => {
if (!hasInitialized) {
@@ -53,10 +62,6 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
}
}, [hasInitialized]);
- useEffect(() => {
- setContext(isOrganization);
- }, [isOrganization]);
-
return (
@@ -65,19 +70,32 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
{errorConnecting ? : }
+
+ } />}
+ restrictWidth
+ readOnlyMode={readOnlyMode}
+ >
+
+
+
+
+ } />}
+ restrictWidth
+ readOnlyMode={readOnlyMode}
+ >
+
+
+
} restrictWidth readOnlyMode={readOnlyMode}>
{errorConnecting ? (
) : (
-
-
-
-
-
-
-
-
+
+
+
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
index d03c0abb441b9..3fddcf3b77fe4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
@@ -12,7 +12,7 @@ import { EuiLink } from '@elastic/eui';
import {
getContentSourcePath,
SOURCES_PATH,
- ORG_SOURCES_PATH,
+ PERSONAL_SOURCES_PATH,
SOURCE_DETAILS_PATH,
} from './routes';
@@ -26,13 +26,13 @@ describe('getContentSourcePath', () => {
const wrapper = shallow();
const path = wrapper.find(EuiLink).prop('href');
- expect(path).toEqual(`${ORG_SOURCES_PATH}/123`);
+ expect(path).toEqual(`${SOURCES_PATH}/123`);
});
it('should format user route', () => {
const wrapper = shallow();
const path = wrapper.find(EuiLink).prop('href');
- expect(path).toEqual(`${SOURCES_PATH}/123`);
+ expect(path).toEqual(`${PERSONAL_SOURCES_PATH}/123`);
});
});
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 e41a043911dc9..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
@@ -44,72 +44,72 @@ export const CUSTOM_API_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-sourc
export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = `${CUSTOM_SOURCE_DOCS_URL}#custom-api-source-document-level-access-control`;
export const ENT_SEARCH_LICENSE_MANAGEMENT = `${ENT_SEARCH_DOCS_PREFIX}/license-management.html`;
-export const ORG_PATH = '/org';
+export const PERSONAL_PATH = '/p';
-export const ROLE_MAPPINGS_PATH = `${ORG_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`;
-export const USERS_PATH = `${ORG_PATH}/users`;
-export const SECURITY_PATH = `${ORG_PATH}/security`;
+export const USERS_PATH = '/users';
+export const SECURITY_PATH = '/security';
export const GROUPS_PATH = '/groups';
export const GROUP_PATH = `${GROUPS_PATH}/:groupId`;
export const GROUP_SOURCE_PRIORITIZATION_PATH = `${GROUPS_PATH}/:groupId/source_prioritization`;
export const SOURCES_PATH = '/sources';
-export const ORG_SOURCES_PATH = `${ORG_PATH}${SOURCES_PATH}`;
+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`;
export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`;
export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`;
-export const PERSONAL_SETTINGS_PATH = '/settings';
+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 = `${ORG_PATH}/settings`;
+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`;
@@ -120,9 +120,9 @@ export const getContentSourcePath = (
path: string,
sourceId: string,
isOrganization: boolean
-): string => generatePath(isOrganization ? ORG_PATH + path : path, { sourceId });
-export const getGroupPath = (groupId: string) => generatePath(GROUP_PATH, { groupId });
-export const getGroupSourcePrioritizationPath = (groupId: string) =>
+): string => generatePath(isOrganization ? path : `${PERSONAL_PATH}${path}`, { sourceId });
+export const getGroupPath = (groupId: string): string => generatePath(GROUP_PATH, { groupId });
+export const getGroupSourcePrioritizationPath = (groupId: string): string =>
`${GROUPS_PATH}/${groupId}/source_prioritization`;
-export const getSourcesPath = (path: string, isOrganization: boolean) =>
- isOrganization ? `${ORG_PATH}${path}` : path;
+export const getSourcesPath = (path: string, isOrganization: boolean): string =>
+ isOrganization ? path : `${PERSONAL_PATH}${path}`;
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/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
index a95d5ca75b0b6..fbd053f9b8374 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
@@ -13,6 +13,7 @@ import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
+ EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
@@ -57,7 +58,7 @@ export const ConfiguredSourcesList: React.FC = ({
{sources.map(({ name, serviceType, addPath, connected, accountContextOnly }, i) => (
-
+
= ({
)}
-
+
))}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx
index ad183181b4eca..f9123ab4e1cca 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx
@@ -240,13 +240,13 @@ export const ConnectInstance: React.FC = ({
gutterSize="xl"
responsive={false}
>
-
+
{header}
{featureBadgeGroup()}
{descriptionBlock}
{formFields}
-
+
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.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
new file mode 100644
index 0000000000000..e34728beef5e5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx
@@ -0,0 +1,141 @@
+/*
+ * 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, { FormEvent, useEffect } from 'react';
+
+import { History } from 'history';
+import { useActions, useValues } from 'kea';
+import { useHistory } from 'react-router-dom';
+
+import './display_settings.scss';
+
+import {
+ EuiButton,
+ EuiEmptyPrompt,
+ EuiTabbedContent,
+ EuiPanel,
+ EuiTabbedContentTab,
+} from '@elastic/eui';
+
+import {
+ DISPLAY_SETTINGS_RESULT_DETAIL_PATH,
+ DISPLAY_SETTINGS_SEARCH_RESULT_PATH,
+ getContentSourcePath,
+} from '../../../../routes';
+
+import { AppLogic } from '../../../../app_logic';
+
+import { Loading } from '../../../../../shared/loading';
+import { ViewContentHeader } from '../../../../components/shared/view_content_header';
+
+import { DisplaySettingsLogic } from './display_settings_logic';
+
+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?';
+
+interface DisplaySettingsProps {
+ tabId: number;
+}
+
+export const DisplaySettings: React.FC = ({ tabId }) => {
+ const history = useHistory() as History;
+ const { initializeDisplaySettings, setServerData, resetDisplaySettingsState } = useActions(
+ DisplaySettingsLogic
+ );
+
+ const {
+ dataLoading,
+ 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',
+ content: ,
+ },
+ {
+ id: 'result_detail',
+ name: 'Result Detail',
+ content: ,
+ },
+ ] as EuiTabbedContentTab[];
+
+ const onSelectedTabChanged = (tab: EuiTabbedContentTab) => {
+ 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: FormEvent) => {
+ e.preventDefault();
+ setServerData();
+ };
+
+ return (
+ <>
+
+ {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..c52665524f566
--- /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,350 @@
+/*
+ * 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 { HttpLogic } from '../../../../../shared/http';
+
+import {
+ setSuccessMessage,
+ FlashMessagesLogic,
+ flashAPIErrors,
+} from '../../../../../shared/flash_messages';
+
+import { AppLogic } from '../../../../app_logic';
+import { SourceLogic } from '../../source_logic';
+
+const SUCCESS_MESSAGE = 'Display Settings have been successfuly updated.';
+
+import { DetailField, SearchResultConfig, OptionValue, Result } from '../../../../types';
+
+export interface DisplaySettingsResponseProps {
+ sourceName: string;
+ searchResultConfig: SearchResultConfig;
+ schemaFields: object;
+ exampleDocuments: Result[];
+}
+
+export interface DisplaySettingsInitialData extends DisplaySettingsResponseProps {
+ sourceId: string;
+ serverRoute: string;
+}
+
+interface DisplaySettingsActions {
+ initializeDisplaySettings(): void;
+ setServerData(): void;
+ onInitializeDisplaySettings(
+ displaySettingsProps: DisplaySettingsInitialData
+ ): DisplaySettingsInitialData;
+ setServerResponseData(
+ displaySettingsProps: DisplaySettingsResponseProps
+ ): DisplaySettingsResponseProps;
+ 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): 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;
+ toggleDescriptionFieldHover(): void;
+ toggleUrlFieldHover(): void;
+ resetDisplaySettingsState(): void;
+}
+
+interface DisplaySettingsValues {
+ sourceName: string;
+ sourceId: string;
+ schemaFields: object;
+ exampleDocuments: Result[];
+ serverSearchResultConfig: SearchResultConfig;
+ searchResultConfig: SearchResultConfig;
+ serverRoute: string;
+ 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,
+ 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,
+ },
+ ],
+ 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: DetailField) =>
+ euiSelectObjectFromValue(usedField.fieldName)
+ );
+ return differenceBy(fieldOptions, usedFields, 'value');
+ },
+ ],
+ unsavedChanges: [
+ () => [selectors.searchResultConfig, selectors.serverSearchResultConfig],
+ (uiConfig, serverConfig) => !isEqual(uiConfig, serverConfig),
+ ],
+ }),
+ listeners: ({ actions, values }) => ({
+ initializeDisplaySettings: async () => {
+ const { isOrganization } = AppLogic.values;
+ const {
+ contentSource: { id: sourceId },
+ } = SourceLogic.values;
+
+ const route = isOrganization
+ ? `/api/workplace_search/org/sources/${sourceId}/display_settings/config`
+ : `/api/workplace_search/account/sources/${sourceId}/display_settings/config`;
+
+ try {
+ const response = await HttpLogic.values.http.get(route);
+ actions.onInitializeDisplaySettings({
+ isOrganization,
+ sourceId,
+ serverRoute: route,
+ ...response,
+ });
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ setServerData: async () => {
+ const { searchResultConfig, serverRoute } = values;
+
+ try {
+ const response = await HttpLogic.values.http.post(serverRoute, {
+ body: JSON.stringify({ ...searchResultConfig }),
+ });
+ actions.setServerResponseData(response);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ setServerResponseData: () => {
+ setSuccessMessage(SUCCESS_MESSAGE);
+ },
+ toggleFieldEditorModal: () => {
+ FlashMessagesLogic.actions.clearFlashMessages();
+ },
+ resetDisplaySettingsState: () => {
+ FlashMessagesLogic.actions.clearFlashMessages();
+ },
+ }),
+});
+
+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
+// and this allows the `searchResultConfig` and the `serverSearchResultConfig` reducers to
+// stay synced on initialization.
+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/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..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
@@ -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 '../../../../app_logic';
+
+import {
+ DISPLAY_SETTINGS_RESULT_DETAIL_PATH,
+ DISPLAY_SETTINGS_SEARCH_RESULT_PATH,
+ getSourcesPath,
+} from '../../../../routes';
+
+import { DisplaySettings } from './display_settings';
+
+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..468f7d2f7ad05
--- /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 './display_settings_logic';
+
+import { CustomSourceIcon } from './custom_source_icon';
+import { TitleField } from './title_field';
+
+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..14239b1654308
--- /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 './display_settings_logic';
+
+import { CustomSourceIcon } from './custom_source_icon';
+import { SubtitleField } from './subtitle_field';
+import { TitleField } from './title_field';
+
+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..4ef3b1fe14b93
--- /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 './display_settings_logic';
+
+import { CustomSourceIcon } from './custom_source_icon';
+import { SubtitleField } from './subtitle_field';
+import { TitleField } from './title_field';
+
+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..587916a741d66
--- /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, { FormEvent, 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 './display_settings_logic';
+
+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: FormEvent) => {
+ e.preventDefault();
+ if (isEditing) {
+ updateDetailField({ fieldName, label }, editFieldIndex);
+ } else {
+ addDetailField({ fieldName, label });
+ }
+ };
+
+ const ACTION_LABEL = isEditing ? 'Update' : 'Add';
+
+ return (
+
+
+
+ );
+};
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..cb65d8ef671e6
--- /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 './display_settings_logic';
+
+import { ExampleResultDetailCard } from './example_result_detail_card';
+
+export const ResultDetail: React.FC = () => {
+ const {
+ toggleFieldEditorModal,
+ setDetailFields,
+ openEditDetailField,
+ removeDetailField,
+ } = useActions(DisplaySettingsLogic);
+
+ const {
+ searchResultConfig: { detailFields },
+ availableFieldOptions,
+ } = useValues(DisplaySettingsLogic);
+
+ return (
+ <>
+
+
+
+
+
+
+ <>
+
+
+
+