From 5beb47a1f7e629edba5433286b87923e3347e04d Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 19 Nov 2020 16:43:18 -0600 Subject: [PATCH] [Workplace Search] Migrate additional shared source components (#83850) * Initial copy/paste of components Changes for pre-commit hooks were: - Linting - Lodash imports - remove `any` type in favor of React.ReactNode - Move setState to top of component * Add image * Update component paths * Remove reference to ConfirmModal Since all the legacy component does it wrap the EUI component we decided to drop it. Legacy: https://github.com/elastic/ent-search/blob/master/app/javascript/shared/components/ConfirmModal/ConfirmModal.tsx * Remove local flash messages in favor of global flash messages * Various type fixes * Fix image location --- .../workplace_search/assets/supports_acl.svg | 1 + .../applications/workplace_search/types.ts | 24 +- .../components/add_source/add_source_list.tsx | 2 +- .../content_sources/components/overview.tsx | 532 ++++++++++++++++++ .../components/source_added.tsx | 47 ++ .../components/source_content.tsx | 209 +++++++ .../components/source_info_card.tsx | 107 ++++ .../components/source_settings.tsx | 176 ++++++ .../views/content_sources/source_logic.ts | 14 +- 9 files changed, 1104 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg new file mode 100644 index 0000000000000..f1267ae57f0bd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg @@ -0,0 +1 @@ + \ No newline at end of file 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 1bd3cabb0227d..73e7f7ed701d8 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 @@ -88,6 +88,12 @@ export interface ContentSource { name: string; } +export interface SourceContentItem { + id: string; + last_updated: string; + [key: string]: string; +} + export interface ContentSourceDetails extends ContentSource { status: string; statusMessage: string; @@ -105,11 +111,23 @@ interface DescriptionList { description: string; } +export interface DocumentSummaryItem { + count: number; + type: string; +} + +interface SourceActivity { + details: string[]; + event: string; + time: string; + status: string; +} + export interface ContentSourceFullData extends ContentSourceDetails { - activities: object[]; + activities: SourceActivity[]; details: DescriptionList[]; - summary: object[]; - groups: object[]; + summary: DocumentSummaryItem[]; + groups: Group[]; custom: boolean; accessToken: string; key: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 24c3a8f8ddb3b..c8fabaac2a4d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -5,7 +5,6 @@ */ import React, { useEffect, useState, ChangeEvent } from 'react'; -import noSharedSourcesIcon from 'workplace_search/components/assets/shareCircle.svg'; import { useActions, useValues } from 'kea'; @@ -18,6 +17,7 @@ import { EuiPanel, EuiEmptyPrompt, } from '@elastic/eui'; +import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; import { AppLogic } from '../../../../app_logic'; import { ContentSection } from '../../../../components/shared/content_section'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx new file mode 100644 index 0000000000000..0155c07f4e0bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -0,0 +1,532 @@ +/* + * 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 { useValues } from 'kea'; +import { Link } from 'react-router-dom'; + +import { + EuiAvatar, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiIconTip, + EuiLink, + EuiPanel, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; + +import { + CUSTOM_SOURCE_DOCS_URL, + DOCUMENT_PERMISSIONS_DOCS_URL, + ENT_SEARCH_LICENSE_MANAGEMENT, + EXTERNAL_IDENTITIES_DOCS_URL, + SOURCE_CONTENT_PATH, + getContentSourcePath, + getGroupPath, +} from '../../../routes'; + +import { AppLogic } from '../../../app_logic'; +import { User } from '../../../types'; + +import { ComponentLoader } from '../../../components/shared/component_loader'; +import { CredentialItem } from '../../../components/shared/credential_item'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { Loading } from '../../../../../applications/shared/loading'; + +import aclImage from '../../../assets/supports_acl.svg'; +import { SourceLogic } from '../source_logic'; + +export const Overview: React.FC = () => { + const { contentSource, dataLoading } = useValues(SourceLogic); + const { isOrganization } = useValues(AppLogic); + + const { + id, + summary, + documentCount, + activities, + groups, + details, + custom, + accessToken, + key, + licenseSupportsPermissions, + serviceTypeSupportsPermissions, + indexPermissions, + hasPermissions, + isFederatedSource, + } = contentSource; + + if (dataLoading) return ; + + const DocumentSummary = () => { + let totalDocuments = 0; + const tableContent = + summary && + summary.map((item, index) => { + totalDocuments += item.count; + return ( + item.count > 0 && ( + + {item.type} + {item.count.toLocaleString('en-US')} + + ) + ); + }); + + const emptyState = ( + <> + + + No content yet} + iconType="documents" + iconColor="subdued" + /> + + + ); + + return ( +
+
+ + + +

Content summary

+
+
+ {totalDocuments > 0 && ( + + + + Manage + + + + )} +
+
+ + {!summary && } + {!!summary && + (totalDocuments === 0 ? ( + emptyState + ) : ( + + + Content Type + Items + + + {tableContent} + + + {summary ? Total documents : 'Documents'} + + + {summary ? ( + {totalDocuments.toLocaleString('en-US')} + ) : ( + parseInt(documentCount, 10).toLocaleString('en-US') + )} + + + + + ))} +
+ ); + }; + + const ActivitySummary = () => { + const emptyState = ( + <> + + + There is no recent activity} + iconType="clock" + iconColor="subdued" + /> + + + ); + + const activitiesTable = ( + + + Event + {!custom && Status} + Time + + + {activities.map(({ details: activityDetails, event, time, status }, i) => ( + + + {event} + + {!custom && ( + + + {status}{' '} + {activityDetails && ( + ( +
{detail}
+ ))} + /> + )} +
+
+ )} + + {time} + +
+ ))} +
+
+ ); + + return ( +
+
+ +

Recent activity

+
+
+ + {activities.length === 0 ? emptyState : activitiesTable} +
+ ); + }; + + const GroupsSummary = () => { + const GroupAvatars = ({ users }: { users: User[] }) => { + const MAX_USERS = 4; + return ( + + {users.slice(0, MAX_USERS).map((user) => ( + + + + ))} + {users.slice(MAX_USERS).length > 0 && ( + + + +{users.slice(MAX_USERS).length} + + + )} + + ); + }; + + return !groups.length ? null : ( + + +
+ Group Access +
+
+ + + {groups.map((group, index) => ( + + + + + + + {group.name} + + + + + + + + + + ))} + +
+ ); + }; + + const detailsSummary = ( + + +
+ Configuration +
+
+ + + {details.map((detail, index) => ( + + + {detail.title} + + {detail.description} + + ))} + +
+ ); + + const documentPermissions = ( + <> + + +

Document-level permissions

+
+ + + + + + + + + Using document-level permissions + + + + + + ); + + const documentPermissionsDisabled = ( + <> + + +

Document-level permissions

+
+ + + + + + + + + + Disabled for this source + + + + Learn more + {' '} + about permissions + + + + + + + ); + + const sourceStatus = ( + + +
+ Status +
+
+ + + + + + + + Everything looks good + + +

Your endpoints are ready to accept requests.

+
+
+
+
+ ); + + const permissionsStatus = ( + + +
+ Status +
+
+ + + + + + + + Requires additional configuration + + +

+ The{' '} + + External Identities API + {' '} + must be used to configure user access mappings. Read the guide to learn more. +

+
+
+
+
+ ); + + const credentials = ( + + +
+ Credentials +
+
+ + + + +
+ ); + + const DocumentationCallout = ({ + title, + children, + }: { + title: string; + children: React.ReactNode; + }) => ( + + +
+ Documentation +
+
+ + +

{title}

+
+ {children} +
+ ); + + const documentPermssionsLicenseLocked = ( + + + + +

Document-level permissions

+
+ +

+ Document-level permissions manage content access content on individual or group + attributes. Allow or deny access to specific documents. +

+
+ + + + Learn about Platinum features + + +
+ ); + + return ( + <> + + + + + + + + + {!isFederatedSource && ( + + + + )} + + + + + + + + {details.length > 0 && {detailsSummary}} + {!custom && serviceTypeSupportsPermissions && ( + <> + {indexPermissions && !hasPermissions && ( + {permissionsStatus} + )} + {indexPermissions && {documentPermissions}} + {!indexPermissions && isOrganization && ( + {documentPermissionsDisabled} + )} + {indexPermissions && {credentials}} + + )} + {custom && ( + <> + {sourceStatus} + {credentials} + + +

+ + Learn more + {' '} + about custom sources. +

+
+
+ {!licenseSupportsPermissions && ( + {documentPermssionsLicenseLocked} + )} + + )} +
+
+ +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx new file mode 100644 index 0000000000000..16aceacbddcd5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -0,0 +1,47 @@ +/* + * 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 { Location } from 'history'; +import { useActions, useValues } from 'kea'; +import { Redirect, useLocation } from 'react-router-dom'; + +import { setErrorMessage } from '../../../../shared/flash_messages'; + +import { parseQueryParams } from '../../../../../applications/shared/query_params'; + +import { SOURCES_PATH, getSourcesPath } from '../../../routes'; + +import { AppLogic } from '../../../app_logic'; +import { SourcesLogic } from '../sources_logic'; + +interface SourceQueryParams { + name: string; + hasError: boolean; + errorMessages?: string[]; + serviceType: string; + indexPermissions: boolean; +} + +export const SourceAdded: React.FC = () => { + const { search } = useLocation() as Location; + const { name, hasError, errorMessages, serviceType, indexPermissions } = (parseQueryParams( + search + ) as unknown) as SourceQueryParams; + const { setAddedSource } = useActions(SourcesLogic); + const { isOrganization } = useValues(AppLogic); + const decodedName = decodeURIComponent(name); + + if (hasError) { + const defaultError = `${decodedName} failed to connect.`; + setErrorMessage(errorMessages ? errorMessages.join(' ') : defaultError); + } else { + setAddedSource(decodedName, indexPermissions, serviceType); + } + + return ; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx new file mode 100644 index 0000000000000..3f289a6394131 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import { useActions, useValues } from 'kea'; +import { startCase } from 'lodash'; +import moment from 'moment'; + +import { + EuiButton, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiLink, +} from '@elastic/eui'; + +import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; +import { SourceContentItem } from '../../../types'; + +import { TruncatedContent } from '../../../../shared/truncate'; + +const MAX_LENGTH = 28; + +import { ComponentLoader } from '../../../components/shared/component_loader'; +import { Loading } from '../../../../../applications/shared/loading'; +import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { CUSTOM_SERVICE_TYPE } from '../../../constants'; + +import { SourceLogic } from '../source_logic'; + +export const SourceContent: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + + const { + setActivePage, + searchContentSourceDocuments, + resetSourceState, + setContentFilterValue, + } = useActions(SourceLogic); + + const { + contentSource: { id, serviceType, urlField, titleField, urlFieldIsLinkable, isFederatedSource }, + contentMeta: { + page: { total_pages: totalPages, total_results: totalItems, current: activePage }, + }, + contentItems, + contentFilterValue, + dataLoading, + sectionLoading, + } = useValues(SourceLogic); + + useEffect(() => { + return resetSourceState; + }, []); + + useEffect(() => { + searchContentSourceDocuments(id); + }, [contentFilterValue, activePage]); + + if (dataLoading) return ; + + const showPagination = totalPages > 1; + const hasItems = totalItems > 0; + const emptyMessage = contentFilterValue + ? `No results for '${contentFilterValue}'` + : "This source doesn't have any content yet"; + + const paginationOptions = { + totalPages, + totalItems, + activePage, + onChangePage: (page: number) => { + // EUI component starts page at 0. API starts at 1. + setActivePage(page + 1); + }, + }; + + const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; + + const emptyState = ( + + + + {emptyMessage}} + iconType="documents" + body={ + isCustomSource ? ( +

+ Learn more about adding content in our{' '} + + documentation + +

+ ) : null + } + /> +
+ +
+ ); + + const contentItem = (item: SourceContentItem) => { + const { id: itemId, last_updated: updated } = item; + const url = item[urlField] || ''; + const title = item[titleField] || ''; + + return ( + + + + + + {!urlFieldIsLinkable && ( + + )} + {urlFieldIsLinkable && ( + + + + )} + + {moment(updated).format('M/D/YYYY, h:mm:ss A')} + + ); + }; + + const contentTable = ( + <> + {showPagination && } + + + + Title + {startCase(urlField)} + Last Updated + + {contentItems.map(contentItem)} + + + {showPagination && } + + ); + + const resetFederatedSearchTerm = () => { + setContentFilterValue(''); + setSearchTerm(''); + }; + const federatedSearchControls = ( + <> + + setContentFilterValue(searchTerm)} + > + Go + + + + + Reset + + + + ); + + return ( + <> + + + + + setSearchTerm(e.target.value)} + /> + + {isFederatedSource && federatedSearchControls} + + + {sectionLoading && } + {!sectionLoading && (hasItems ? contentTable : emptyState)} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx new file mode 100644 index 0000000000000..e3c3e76311018 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -0,0 +1,107 @@ +/* + * 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 { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSpacer, +} from '@elastic/eui'; + +import { SourceIcon } from '../../../components/shared/source_icon'; + +interface SourceInfoCardProps { + sourceName: string; + sourceType: string; + dateCreated: string; + isFederatedSource: boolean; +} + +export const SourceInfoCard: React.FC = ({ + sourceName, + sourceType, + dateCreated, + isFederatedSource, +}) => ( + + + + + Connector + + + + + + + + + {sourceName} + + + + + + + + + + + + + Created + + + + {dateCreated} + + + + + {isFederatedSource && ( + <> + + + + + + + Status + + + + + Ready to search + + + + + + + )} + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx new file mode 100644 index 0000000000000..1f756115e3ae4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, ChangeEvent, FormEvent } from 'react'; + +import { History } from 'history'; +import { useActions, useValues } from 'kea'; +import { isEmpty } from 'lodash'; +import { Link, useHistory } from 'react-router-dom'; + +import { + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; + +import { SOURCES_PATH, getSourcesPath } from '../../../routes'; + +import { ContentSection } from '../../../components/shared/content_section'; +import { SourceConfigFields } from '../../../components/shared/source_config_fields'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { SourceDataItem } from '../../../types'; +import { AppLogic } from '../../../app_logic'; +import { staticSourceData } from '../source_data'; + +import { SourceLogic } from '../source_logic'; + +export const SourceSettings: React.FC = () => { + const history = useHistory() as History; + const { + updateContentSource, + removeContentSource, + resetSourceState, + getSourceConfigData, + } = useActions(SourceLogic); + + const { + contentSource: { name, id, serviceType }, + buttonLoading, + sourceConfigData: { configuredFields }, + } = useValues(SourceLogic); + + const { isOrganization } = useValues(AppLogic); + + useEffect(() => { + getSourceConfigData(serviceType); + return resetSourceState; + }, []); + const { + configuration: { isPublicKey }, + editPath, + } = staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem; + + const [inputValue, setValue] = useState(name); + const [confirmModalVisible, setModalVisibility] = useState(false); + const showConfirm = () => setModalVisibility(true); + const hideConfirm = () => setModalVisibility(false); + + const showConfig = isOrganization && !isEmpty(configuredFields); + + const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; + + const handleNameChange = (e: ChangeEvent) => setValue(e.target.value); + + const submitNameChange = (e: FormEvent) => { + e.preventDefault(); + updateContentSource(id, { name: inputValue }); + }; + + const handleSourceRemoval = () => { + /** + * The modal was just hanging while the UI waited for the server to respond. + * EuiModal doens't allow the button to have a loading state so we just hide the + * modal here and set the button that was clicked to delete to a loading state. + */ + setModalVisibility(false); + const onSourceRemoved = () => history.push(getSourcesPath(SOURCES_PATH, isOrganization)); + removeContentSource(id, onSourceRemoved); + }; + + const confirmModal = ( + + + Your source documents will be deleted from Workplace Search.
+ Are you sure you want to remove {name}? +
+
+ ); + + return ( + <> + + + +
+ + + + + + + + + Save changes + + + +
+
+ {showConfig && ( + + + + + Edit content source connector settings + + + + )} + + + Remove + + {confirmModalVisible && confirmModal} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 889519b8a9985..0a11da02dc789 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -23,7 +23,13 @@ import { import { DEFAULT_META } from '../../../shared/constants'; import { AppLogic } from '../../app_logic'; import { NOT_FOUND_PATH } from '../../routes'; -import { ContentSourceFullData, CustomSource, Meta } from '../../types'; +import { + ContentSourceFullData, + CustomSource, + Meta, + DocumentSummaryItem, + SourceContentItem, +} from '../../types'; export interface SourceActions { onInitializeSource(contentSource: ContentSourceFullData): ContentSourceFullData; @@ -32,7 +38,7 @@ export interface SourceActions { setSourceConnectData(sourceConnectData: SourceConnectData): SourceConnectData; setSearchResults(searchResultsResponse: SearchResultsResponse): SearchResultsResponse; initializeFederatedSummary(sourceId: string): { sourceId: string }; - onUpdateSummary(summary: object[]): object[]; + onUpdateSummary(summary: DocumentSummaryItem[]): DocumentSummaryItem[]; setContentFilterValue(contentFilterValue: string): string; setActivePage(activePage: number): number; setClientIdValue(clientIdValue: string): string; @@ -108,7 +114,7 @@ interface SourceValues { dataLoading: boolean; sectionLoading: boolean; buttonLoading: boolean; - contentItems: object[]; + contentItems: SourceContentItem[]; contentMeta: Meta; contentFilterValue: string; customSourceNameValue: string; @@ -129,7 +135,7 @@ interface SourceValues { } interface SearchResultsResponse { - results: object[]; + results: SourceContentItem[]; meta: Meta; }