diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx index 8cb83176a6591..cd80b2489012e 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx @@ -43,6 +43,7 @@ function entryToDisplaylistItem(entry: ConfigEntryView): { description: string; interface ConnectorConfigurationProps { connector: Connector; hasPlatinumLicense: boolean; + isDisabled?: boolean; isLoading: boolean; saveConfig: (configuration: Record) => void; saveAndSync?: (configuration: Record) => void; @@ -89,6 +90,7 @@ export const ConnectorConfigurationComponent: FC< children, connector, hasPlatinumLicense, + isDisabled, isLoading, saveConfig, saveAndSync, @@ -207,6 +209,7 @@ export const ConnectorConfigurationComponent: FC< data-test-subj="entSearchContent-connector-configuration-editConfiguration" data-telemetry-id="entSearchContent-connector-overview-configuration-editConfiguration" onClick={() => setIsEditing(!isEditing)} + isDisabled={isDisabled} > {i18n.translate( 'searchConnectors.configurationConnector.config.editButton.title', diff --git a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx index 3d8ea94b3599a..62521b3e2b3fa 100644 --- a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx +++ b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx @@ -66,6 +66,7 @@ interface ConnectorContentSchedulingProps { hasPlatinumLicense: boolean; hasChanges: boolean; hasIngestionError: boolean; + isDisabled?: boolean; setHasChanges: (changes: boolean) => void; shouldShowAccessControlSync: boolean; shouldShowIncrementalSync: boolean; @@ -81,6 +82,7 @@ export const ConnectorSchedulingComponent: React.FC {shouldShowIncrementalSync && ( @@ -153,6 +156,7 @@ export const ConnectorSchedulingComponent: React.FC )} @@ -186,6 +190,7 @@ export const ConnectorSchedulingComponent: React.FC diff --git a/packages/kbn-search-connectors/components/scheduling/full_content.tsx b/packages/kbn-search-connectors/components/scheduling/full_content.tsx index de85f8fb2e4a9..3ec1fd4ab9e49 100644 --- a/packages/kbn-search-connectors/components/scheduling/full_content.tsx +++ b/packages/kbn-search-connectors/components/scheduling/full_content.tsx @@ -29,6 +29,7 @@ export interface ConnectorContentSchedulingProps { dataTelemetryIdPrefix: string; hasPlatinumLicense?: boolean; hasSyncTypeChanges: boolean; + isDisabled?: boolean; setHasChanges: (hasChanges: boolean) => void; setHasSyncTypeChanges: (state: boolean) => void; type: SyncJobType; @@ -104,6 +105,7 @@ export const ConnectorContentScheduling: React.FC @@ -217,7 +221,7 @@ export const ConnectorContentScheduling: React.FC { setScheduling({ diff --git a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx index 296118410f868..38f880ec5298b 100644 --- a/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/api_key/api_key.tsx @@ -20,8 +20,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; -import { ApiKey } from '@kbn/security-plugin-types-common'; -import { useQuery } from '@tanstack/react-query'; import React, { useEffect, useState } from 'react'; import { ApiKeySelectableTokenField } from '@kbn/security-api-key-management'; import { @@ -32,6 +30,7 @@ import { useKibanaServices } from '../../hooks/use_kibana'; import { MANAGEMENT_API_KEYS } from '../../../../common/routes'; import { CreateApiKeyFlyout } from './create_api_key_flyout'; import './api_key.scss'; +import { useGetApiKeys } from '../../hooks/api/use_api_key'; function isCreatedResponse( value: SecurityCreateApiKeyResponse | SecurityUpdateApiKeyResponse @@ -45,15 +44,16 @@ function isCreatedResponse( export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: string) => void }) => { const { http, user } = useKibanaServices(); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); - const { data } = useQuery({ - queryKey: ['apiKey'], - queryFn: () => http.fetch<{ apiKeys: ApiKey[] }>('/internal/serverless_search/api_keys'), - }); + const { data } = useGetApiKeys(); + const [apiKey, setApiKey] = useState(undefined); const saveApiKey = (value: SecurityCreateApiKeyResponse) => { setApiKey(value); }; + // Prevent flickering in the most common case of having access to manage api keys + const canManageOwnApiKey = !data || data.canManageOwnApiKey; + useEffect(() => { if (apiKey) { setClientApiKey(apiKey.encoded); @@ -101,7 +101,7 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri ) : ( - +

{i18n.translate('xpack.serverlessSearch.apiKey.panel.title', { @@ -117,6 +117,16 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri })} + {!canManageOwnApiKey && ( + <> + + {i18n.translate('xpack.serverlessSearch.apiKey.panel.noUserPrivileges', { + defaultMessage: "You don't have access to manage API keys", + })} + + + + )} @@ -127,6 +137,7 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri size="s" fill onClick={() => setIsFlyoutOpen(true)} + disabled={!canManageOwnApiKey} data-test-subj="new-api-key-button" aria-label={i18n.translate( 'xpack.serverlessSearch.apiKey.newButton.ariaLabel', @@ -143,24 +154,29 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri - - - - {i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', { - defaultMessage: 'Manage', - })} - - - + {canManageOwnApiKey && ( + + + + {i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', { + defaultMessage: 'Manage', + })} + + + + )} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/conector_scheduling_tab/connector_scheduling.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/conector_scheduling_tab/connector_scheduling.tsx index 12961bfc4a093..f8c48cbb3c8ab 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/conector_scheduling_tab/connector_scheduling.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/conector_scheduling_tab/connector_scheduling.tsx @@ -10,26 +10,33 @@ import { ConnectorSchedulingComponent } from '@kbn/search-connectors/components/ import { useConnectorScheduling } from '../../../hooks/api/use_update_connector_scheduling'; interface ConnectorSchedulingPanels { + canManageConnectors: boolean; connector: Connector; } -export const ConnectorScheduling: React.FC = ({ connector }) => { +export const ConnectorScheduling: React.FC = ({ + canManageConnectors, + connector, +}) => { const [hasChanges, setHasChanges] = useState(false); const { isLoading, mutate } = useConnectorScheduling(connector.id); const hasIncrementalSyncFeature = connector?.features?.incremental_sync ?? false; const shouldShowIncrementalSync = hasIncrementalSyncFeature && (connector?.features?.incremental_sync?.enabled ?? false); return ( - + <> + + ); }; diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/api_key_panel.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/api_key_panel.tsx index eb1f4aba2ec2d..0ee9e3c638528 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/api_key_panel.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/api_key_panel.tsx @@ -23,11 +23,13 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { OPTIONAL_LABEL } from '../../../../../common/i18n_string'; import { useCreateApiKey } from '../../../hooks/api/use_create_api_key'; +import { useGetApiKeys } from '../../../hooks/api/use_api_key'; interface ApiKeyPanelProps { connector: Connector; } export const ApiKeyPanel: React.FC = ({ connector }) => { const { data, isLoading, mutate } = useCreateApiKey(); + const { data: apiKeysData } = useGetApiKeys(); return ( @@ -59,7 +61,7 @@ export const ApiKeyPanel: React.FC = ({ connector }) => { = ({ connector }) => { +export const ConnectorConfigFields: React.FC = ({ + connector, + isDisabled, +}) => { const { data, isLoading, isSuccess, mutate, reset } = useEditConnectorConfiguration(connector.id); const { queryKey } = useConnector(connector.id); const queryClient = useQueryClient(); @@ -53,6 +57,7 @@ export const ConnectorConfigFields: React.FC = ({ co diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_config_panels.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_config_panels.tsx index e07874a4676e4..93e3881020b27 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_config_panels.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_config_panels.tsx @@ -16,10 +16,12 @@ import { ConnectionDetails } from './connection_details_panel'; import { ConnectorIndexnamePanel } from './connector_index_name_panel'; interface ConnectorConfigurationPanels { + canManageConnectors: boolean; connector: Connector; } export const ConnectorConfigurationPanels: React.FC = ({ + canManageConnectors, connector, }) => { const { data, isLoading, isSuccess, mutate, reset } = useEditConnectorConfiguration(connector.id); @@ -37,6 +39,7 @@ export const ConnectorConfigurationPanels: React.FC - + = ({ connector }) => { + const { data } = useConnectors(); + const { canManageConnectors } = data || { canManageConnectors: false }; const [currentStep, setCurrentStep] = useState('link'); useEffect(() => { let step: ConnectorConfigurationStep = 'link'; @@ -99,7 +102,9 @@ export const ConnectorConfiguration: React.FC = ({ const tabs: EuiTabbedContentTab[] = [ { - content: , + content: ( + + ), id: 'overview', name: OVERVIEW_LABEL, }, @@ -107,7 +112,10 @@ export const ConnectorConfiguration: React.FC = ({ content: ( <> - + ), id: 'configuration', @@ -117,7 +125,7 @@ export const ConnectorConfiguration: React.FC = ({ content: ( <> - + ), id: 'scheduling', @@ -140,8 +148,12 @@ export const ConnectorConfiguration: React.FC = ({ status={connector.status} /> )} - {currentStep === 'configure' && } - {currentStep === 'connect' && } + {currentStep === 'configure' && ( + + )} + {currentStep === 'connect' && ( + + )} ); diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name.tsx index a421af47a0a79..153c41c0332e9 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name.tsx @@ -32,9 +32,13 @@ import { docLinks } from '../../../../../common/doc_links'; import { DEFAULT_INGESTION_PIPELINE } from '../../../../../common'; interface ConnectorIndexNameProps { connector: Connector; + isDisabled?: boolean; } -export const ConnectorIndexName: React.FC = ({ connector }) => { +export const ConnectorIndexName: React.FC = ({ + connector, + isDisabled, +}) => { const { http } = useKibanaServices(); const queryClient = useQueryClient(); const { queryKey } = useConnector(connector.id); @@ -88,6 +92,7 @@ export const ConnectorIndexName: React.FC = ({ connecto setNewIndexname(name)} /> @@ -145,7 +150,7 @@ export const ConnectorIndexName: React.FC = ({ connecto mutate({ inputName: newIndexName, sync: false })} > @@ -162,7 +167,7 @@ export const ConnectorIndexName: React.FC = ({ connecto !( isValidIndexName(newIndexName) && [ConnectorStatus.CONFIGURED, ConnectorStatus.CONNECTED].includes(connector.status) - ) + ) || isDisabled } fill isLoading={isLoading} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name_panel.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name_panel.tsx index ac472d9a4b440..3f4a51487a52e 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name_panel.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_index_name_panel.tsx @@ -17,9 +17,13 @@ import { ConnectorIndexNameForm } from './connector_index_name_form'; interface ConnectorIndexNamePanelProps { connector: Connector; + canManageConnectors: boolean; } -export const ConnectorIndexnamePanel: React.FC = ({ connector }) => { +export const ConnectorIndexnamePanel: React.FC = ({ + canManageConnectors, + connector, +}) => { const { http } = useKibanaServices(); const { data, isLoading, isSuccess, mutate, reset } = useMutation({ mutationFn: async (inputName: string) => { @@ -48,7 +52,7 @@ export const ConnectorIndexnamePanel: React.FC = ( return ( <> setNewIndexName(name)} /> diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_overview.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_overview.tsx index a4d79759d71cb..7a3d07eb5022b 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_overview.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_overview.tsx @@ -21,10 +21,14 @@ import { useKibanaServices } from '../../../hooks/use_kibana'; import { SyncScheduledCallOut } from './sync_scheduled_callout'; interface ConnectorOverviewProps { + canManageConnectors: boolean; connector: Connector; } -export const ConnectorOverview: React.FC = ({ connector }) => { +export const ConnectorOverview: React.FC = ({ + canManageConnectors, + connector, +}) => { const { http } = useKibanaServices(); const queryClient = useQueryClient(); const { queryKey } = useConnector(connector.id); @@ -64,9 +68,11 @@ export const ConnectorOverview: React.FC = ({ connector mutate()} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_privileges_callout.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_privileges_callout.tsx new file mode 100644 index 0000000000000..d2b1d7196cffb --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_privileges_callout.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { useConnectors } from '../../../hooks/api/use_connectors'; + +export const ConnectorPrivilegesCallout: React.FC = () => { + const { data } = useConnectors(); + if (!data || (data.canManageConnectors && data.canReadConnectors)) { + return null; + } + const calloutTitle = i18n.translate('xpack.serverlessSearch.connectors.noPrivilegesTitle', { + defaultMessage: 'Insufficient access', + }); + return ( + <> + + {data.canReadConnectors + ? i18n.translate('xpack.serverlessSearch.connectors.noManagePrivileges', { + defaultMessage: + 'You have read-only access to connectors. Contact your administrator for elevated privileges.', + }) + : i18n.translate('xpack.serverlessSearch.connectors.noPrivileges', { + defaultMessage: + "You don't have access to connectors. Contact your administrator for access.", + })} + + + + ); +}; diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx index 6f8dbd0edb4bc..4e559c74e8c87 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx @@ -211,7 +211,12 @@ export const ConnectorsTable: React.FC = () => { onClick: (connector: Connector) => copyToClipboard(connector.id), }, { - render: (connector: Connector) => , + render: (connector: Connector) => ( + + ), }, ], name: i18n.translate('xpack.serverlessSearch.connectors.actionsLabel', { @@ -287,7 +292,10 @@ export const ConnectorsTable: React.FC = () => { ); }; -const DeleteConnectorModalAction: React.FC<{ connector: Connector }> = ({ connector }) => { +const DeleteConnectorModalAction: React.FC<{ connector: Connector; disabled: boolean }> = ({ + connector, + disabled, +}) => { const [modalIsOpen, setModalIsOpen] = useState(false); return ( @@ -301,6 +309,7 @@ const DeleteConnectorModalAction: React.FC<{ connector: Connector }> = ({ connec )} setModalIsOpen(true)} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx index 2932ffc7bf79a..a8c9ef2cd52eb 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx @@ -33,10 +33,14 @@ import { EditDescription } from './edit_description'; import { DeleteConnectorModal } from './delete_connector_modal'; import { ConnectorConfiguration } from './connector_config/connector_configuration'; import { useConnector } from '../../hooks/api/use_connector'; +import { useConnectors } from '../../hooks/api/use_connectors'; +import { ConnectorPrivilegesCallout } from './connector_config/connector_privileges_callout'; export const EditConnector: React.FC = () => { const [deleteModalIsOpen, setDeleteModalIsOpen] = useState(false); const [menuIsOpen, setMenuIsOpen] = useState(false); + const { data: connectorsData } = useConnectors(); + const isDisabled = !connectorsData?.canManageConnectors; const { id } = useParams<{ id: string }>(); @@ -47,7 +51,7 @@ export const EditConnector: React.FC = () => { const { data, isLoading } = useConnector(id); - if (isLoading) { + if (!data || isLoading) { { {CONNECTOR_LABEL} - + {deleteModalIsOpen && ( @@ -142,6 +146,7 @@ export const EditConnector: React.FC = () => { }, { name: DELETE_CONNECTOR_LABEL, + disabled: isDisabled, icon: 'trash', onClick: () => { setDeleteModalIsOpen(true); @@ -157,12 +162,13 @@ export const EditConnector: React.FC = () => { + - + - + diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx index 1749e1673e269..76f22f36f02af 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx @@ -25,10 +25,11 @@ import { useKibanaServices } from '../../hooks/use_kibana'; import { useConnector } from '../../hooks/api/use_connector'; interface EditDescriptionProps { + isDisabled?: boolean; connector: Connector; } -export const EditDescription: React.FC = ({ connector }) => { +export const EditDescription: React.FC = ({ connector, isDisabled }) => { const [isEditing, setIsEditing] = useState(false); const [newDescription, setNewDescription] = useState(connector.description || ''); const { http } = useKibanaServices(); @@ -66,15 +67,17 @@ export const EditDescription: React.FC = ({ connector }) = defaultMessage: 'Description', })} labelAppend={ - - setIsEditing(true)} - role="button" - > - {EDIT_LABEL} - - + isDisabled ? undefined : ( + + setIsEditing(true)} + role="button" + > + {EDIT_LABEL} + + + ) } fullWidth > diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx index b81fc51b07bcf..2fccd9554abaf 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx @@ -26,10 +26,11 @@ import { useKibanaServices } from '../../hooks/use_kibana'; import { useConnector } from '../../hooks/api/use_connector'; interface EditNameProps { + isDisabled?: boolean; connector: Connector; } -export const EditName: React.FC = ({ connector }) => { +export const EditName: React.FC = ({ connector, isDisabled }) => { const [isEditing, setIsEditing] = useState(false); const [newName, setNewName] = useState(connector.name || CONNECTOR_LABEL); const { http } = useKibanaServices(); @@ -77,6 +78,7 @@ export const EditName: React.FC = ({ connector }) => { > = ({ connector }) => { +export const EditServiceType: React.FC = ({ connector, isDisabled }) => { const { http } = useKibanaServices(); const connectorTypes = useConnectorTypes(); const queryClient = useQueryClient(); @@ -71,7 +72,7 @@ export const EditServiceType: React.FC = ({ connector }) = > mutate(event)} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/empty_connectors_prompt.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/empty_connectors_prompt.tsx index 5a90a9b4ff0da..56c7a9aaf8155 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/empty_connectors_prompt.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/empty_connectors_prompt.tsx @@ -20,13 +20,16 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../../common/doc_links'; import { useConnectorTypes } from '../../hooks/api/use_connector_types'; import { useCreateConnector } from '../../hooks/api/use_create_connector'; import { useAssetBasePath } from '../../hooks/use_asset_base_path'; +import { useConnectors } from '../../hooks/api/use_connectors'; export const EmptyConnectorsPrompt: React.FC = () => { const connectorTypes = useConnectorTypes(); const { createConnector, isLoading } = useCreateConnector(); + const { data } = useConnectors(); const assetBasePath = useAssetBasePath(); const connectorsPath = assetBasePath + '/connectors.svg'; @@ -94,7 +97,7 @@ export const EmptyConnectorsPrompt: React.FC = () => { source: ( {i18n.translate( 'xpack.serverlessSearch.connectorsEmpty.sourceLabel', @@ -105,7 +108,7 @@ export const EmptyConnectorsPrompt: React.FC = () => { docker: ( {i18n.translate( 'xpack.serverlessSearch.connectorsEmpty.dockerLabel', @@ -167,6 +170,7 @@ export const EmptyConnectorsPrompt: React.FC = () => { createConnector()} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors_callout.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors_callout.tsx index 52fb878d4b619..625973a1f377a 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors_callout.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors_callout.tsx @@ -10,9 +10,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useCreateConnector } from '../hooks/api/use_create_connector'; +import { useConnectors } from '../hooks/api/use_connectors'; export const ConnectorsCallout = () => { const { createConnector, isLoading } = useCreateConnector(); + const { data } = useConnectors(); return ( { createConnector()} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx index ede574abba12b..9f7f92b031e8f 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx @@ -19,9 +19,12 @@ import { GithubLink } from '@kbn/search-api-panels'; import React from 'react'; import { useCreateConnector } from '../hooks/api/use_create_connector'; +import { useConnectors } from '../hooks/api/use_connectors'; export const ConnectorIngestionPanel: React.FC<{ assetBasePath: string }> = ({ assetBasePath }) => { const { createConnector } = useCreateConnector(); + const { data } = useConnectors(); + return ( @@ -49,19 +52,21 @@ export const ConnectorIngestionPanel: React.FC<{ assetBasePath: string }> = ({ a - - createConnector()} - > - {i18n.translate( - 'xpack.serverlessSearch.ingestData.alternativeOptions.setupConnectorLabel', - { - defaultMessage: 'Set up a connector', - } - )} - - + {Boolean(data?.canManageConnectors) && ( + + createConnector()} + > + {i18n.translate( + 'xpack.serverlessSearch.ingestData.alternativeOptions.setupConnectorLabel', + { + defaultMessage: 'Set up a connector', + } + )} + + + )} { const { data, isLoading: connectorsLoading } = useConnectors(); @@ -35,6 +37,8 @@ export const ConnectorsOverview = () => { [consolePlugin] ); + const canManageConnectors = !data || data.canManageConnectors; + return ( { { data-test-subj="serverlessSearchConnectorsOverviewLink" external target="_blank" - href={'TODO TODO'} + href={docLinks.connectors} > {LEARN_MORE_LABEL} @@ -110,15 +115,14 @@ export const ConnectorsOverview = () => {

- {connectorsLoading || (data?.connectors || []).length > 0 ? ( - + + + {connectorsLoading || (data?.connectors || []).length > 0 ? ( - - ) : ( - + ) : ( - - )} + )} + {embeddableConsole}
); diff --git a/x-pack/plugins/serverless_search/public/application/components/pipeline_manage_button.tsx b/x-pack/plugins/serverless_search/public/application/components/pipeline_manage_button.tsx index 15337cf6a7f30..14ec93402324b 100644 --- a/x-pack/plugins/serverless_search/public/application/components/pipeline_manage_button.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/pipeline_manage_button.tsx @@ -11,21 +11,24 @@ import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibanaServices } from '../hooks/use_kibana'; +import { useIngestPipelines } from '../hooks/api/use_ingest_pipelines'; export const PipelineManageButton: React.FC = () => { const { http } = useKibanaServices(); + const { data } = useIngestPipelines(); return ( <> {i18n.translate('xpack.serverlessSearch.pipeline.description.manageButtonLabel', { - defaultMessage: 'Manage pipeline', + defaultMessage: 'Manage pipelines', })} diff --git a/x-pack/plugins/serverless_search/public/application/components/pipeline_overview_button.tsx b/x-pack/plugins/serverless_search/public/application/components/pipeline_overview_button.tsx index cc01bb538600a..d9be14209cb7e 100644 --- a/x-pack/plugins/serverless_search/public/application/components/pipeline_overview_button.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/pipeline_overview_button.tsx @@ -11,9 +11,11 @@ import { EuiSpacer, EuiText, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibanaServices } from '../hooks/use_kibana'; +import { useIngestPipelines } from '../hooks/api/use_ingest_pipelines'; export const PipelineOverviewButton: React.FC = () => { const { http } = useKibanaServices(); + const { data } = useIngestPipelines(); return ( <> @@ -21,6 +23,7 @@ export const PipelineOverviewButton: React.FC = () => { diff --git a/x-pack/plugins/serverless_search/public/application/hooks/api/use_api_key.tsx b/x-pack/plugins/serverless_search/public/application/hooks/api/use_api_key.tsx new file mode 100644 index 0000000000000..cb0dce762ad14 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/hooks/api/use_api_key.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApiKey } from '@kbn/security-plugin-types-common'; +import { useQuery } from '@tanstack/react-query'; +import { useKibanaServices } from '../use_kibana'; + +export const useGetApiKeys = () => { + const { http } = useKibanaServices(); + return useQuery({ + queryKey: ['apiKey'], + queryFn: () => + http.fetch<{ apiKeys: ApiKey[]; canManageOwnApiKey: boolean }>( + '/internal/serverless_search/api_keys' + ), + }); +}; diff --git a/x-pack/plugins/serverless_search/public/application/hooks/api/use_connectors.tsx b/x-pack/plugins/serverless_search/public/application/hooks/api/use_connectors.tsx index f6de4223cfdce..2c880f3923149 100644 --- a/x-pack/plugins/serverless_search/public/application/hooks/api/use_connectors.tsx +++ b/x-pack/plugins/serverless_search/public/application/hooks/api/use_connectors.tsx @@ -14,6 +14,10 @@ export const useConnectors = () => { return useQuery({ queryKey: ['fetchConnectors'], queryFn: () => - http.fetch<{ connectors: Connector[] }>('/internal/serverless_search/connectors'), + http.fetch<{ + connectors: Connector[]; + canManageConnectors: boolean; + canReadConnectors: boolean; + }>('/internal/serverless_search/connectors'), }); }; diff --git a/x-pack/plugins/serverless_search/public/application/hooks/api/use_ingest_pipelines.tsx b/x-pack/plugins/serverless_search/public/application/hooks/api/use_ingest_pipelines.tsx index bd7d4292cfe18..5507ff16f7705 100644 --- a/x-pack/plugins/serverless_search/public/application/hooks/api/use_ingest_pipelines.tsx +++ b/x-pack/plugins/serverless_search/public/application/hooks/api/use_ingest_pipelines.tsx @@ -14,7 +14,7 @@ export const useIngestPipelines = () => { return useQuery({ queryKey: ['fetchIngestPipelines'], queryFn: async () => - http.fetch>( + http.fetch<{ pipelines: IngestGetPipelineResponse; canManagePipelines: boolean }>( `/internal/serverless_search/ingest_pipelines` ), }); diff --git a/x-pack/plugins/serverless_search/public/application/hooks/use_kibana.tsx b/x-pack/plugins/serverless_search/public/application/hooks/use_kibana.tsx index df73a27f9097d..2c659f9e3fe44 100644 --- a/x-pack/plugins/serverless_search/public/application/hooks/use_kibana.tsx +++ b/x-pack/plugins/serverless_search/public/application/hooks/use_kibana.tsx @@ -12,12 +12,14 @@ import type { SharePluginStart } from '@kbn/share-plugin/public'; import { useKibana as useKibanaBase } from '@kbn/kibana-react-plugin/public'; import { AuthenticatedUser } from '@kbn/security-plugin/common'; import { SearchConnectorsPluginStart } from '@kbn/search-connectors-plugin/public'; +import { SecurityPluginStart } from '@kbn/security-plugin-types-public'; export interface ServerlessSearchContext { cloud: CloudStart; console: ConsolePluginStart; history: AppMountParameters['history']; searchConnectors?: SearchConnectorsPluginStart; + security: SecurityPluginStart; share: SharePluginStart; user?: AuthenticatedUser; } diff --git a/x-pack/plugins/serverless_search/server/routes/api_key_routes.ts b/x-pack/plugins/serverless_search/server/routes/api_key_routes.ts index 3d89d104eaa1c..a4f702a4d9d2b 100644 --- a/x-pack/plugins/serverless_search/server/routes/api_key_routes.ts +++ b/x-pack/plugins/serverless_search/server/routes/api_key_routes.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDependencies } from '../plugin'; +import { errorHandler } from '../utils/error_handler'; export const registerApiKeyRoutes = ({ logger, router, getSecurity }: RouteDependencies) => { router.get( @@ -14,20 +15,32 @@ export const registerApiKeyRoutes = ({ logger, router, getSecurity }: RouteDepen path: '/internal/serverless_search/api_keys', validate: {}, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const core = await context.core; const { client } = core.elasticsearch; const user = core.security.authc.getCurrentUser(); if (user) { - const apiKeys = await client.asCurrentUser.security.getApiKey({ username: user.username }); - const validKeys = apiKeys.api_keys.filter(({ invalidated }) => !invalidated); - return response.ok({ body: { apiKeys: validKeys } }); + const privileges = await client.asCurrentUser.security.hasPrivileges({ + cluster: ['manage_own_api_key'], + }); + const canManageOwnApiKey = privileges?.cluster.manage_own_api_key; + + try { + const apiKeys = await client.asCurrentUser.security.getApiKey({ + username: user.username, + }); + + const validKeys = apiKeys.api_keys.filter(({ invalidated }) => !invalidated); + return response.ok({ body: { apiKeys: validKeys, canManageOwnApiKey } }); + } catch { + return response.ok({ body: { apiKeys: [], canManageOwnApiKey } }); + } } return response.customError({ statusCode: 502, body: 'Could not retrieve current user, security plugin is not ready', }); - } + }) ); router.post( @@ -37,7 +50,7 @@ export const registerApiKeyRoutes = ({ logger, router, getSecurity }: RouteDepen body: schema.any(), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const security = await getSecurity(); const result = await security.authc.apiKeys.create(request, request.body); @@ -49,6 +62,6 @@ export const registerApiKeyRoutes = ({ logger, router, getSecurity }: RouteDepen statusCode: 502, body: 'Could not retrieve current user, security plugin is not ready', }); - } + }) ); }; diff --git a/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts b/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts index c57610dd9523a..09896a1808e4d 100644 --- a/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts +++ b/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts @@ -22,24 +22,32 @@ import { } from '@kbn/search-connectors'; import { DEFAULT_INGESTION_PIPELINE } from '../../common'; import { RouteDependencies } from '../plugin'; +import { errorHandler } from '../utils/error_handler'; -export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => { +export const registerConnectorsRoutes = ({ logger, http, router }: RouteDependencies) => { router.get( { path: '/internal/serverless_search/connectors', validate: {}, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; - const connectors = await fetchConnectors(client.asCurrentUser); + const privileges = await client.asCurrentUser.security.hasPrivileges({ + index: [{ names: ['.elastic-connectors'], privileges: ['read', 'write'] }], + }); + const canManageConnectors = privileges.index['.elastic-connectors'].write; + const canReadConnectors = privileges.index['.elastic-connectors'].read; + + const connectors = canReadConnectors ? await fetchConnectors(client.asCurrentUser) : []; return response.ok({ body: { connectors, + canManageConnectors, + canReadConnectors, }, - headers: { 'content-type': 'application/json' }, }); - } + }) ); router.get( @@ -51,7 +59,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const connector = await fetchConnectorById(client.asCurrentUser, request.params.connectorId); @@ -63,7 +71,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => headers: { 'content-type': 'application/json' }, }) : response.notFound(); - } + }) ); router.post( @@ -71,7 +79,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => path: '/internal/serverless_search/connectors', validate: {}, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const defaultPipeline: IngestPipelineParams = { name: DEFAULT_INGESTION_PIPELINE, @@ -92,7 +100,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.post( @@ -107,7 +115,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const result = await updateConnectorNameAndDescription( client.asCurrentUser, @@ -123,7 +131,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.post( @@ -138,7 +146,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const result = await updateConnectorNameAndDescription( client.asCurrentUser, @@ -154,7 +162,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.post( @@ -169,7 +177,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; try { const result = await updateConnectorIndexName( @@ -186,7 +194,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => } catch (e) { return response.conflict({ body: e }); } - } + }) ); router.post( @@ -201,7 +209,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const result = await updateConnectorServiceType( client.asCurrentUser, @@ -215,7 +223,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.delete( @@ -227,7 +235,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const result = await deleteConnectorById(client.asCurrentUser, request.params.connectorId); return response.ok({ @@ -236,7 +244,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.post( @@ -254,7 +262,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const result = await updateConnectorConfiguration( client.asCurrentUser, @@ -266,7 +274,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => body: result, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.post( @@ -278,7 +286,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const result = await startConnectorSync(client.asCurrentUser, { connectorId: request.params.connectorId, @@ -288,7 +296,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => body: result, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.get( @@ -305,7 +313,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const result = await fetchSyncJobs( client.asCurrentUser, @@ -319,7 +327,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => body: result, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.post( { @@ -335,7 +343,7 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; await updateConnectorScheduling( client.asCurrentUser, @@ -343,6 +351,6 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => request.body ); return response.ok(); - } + }) ); }; diff --git a/x-pack/plugins/serverless_search/server/routes/indices_routes.ts b/x-pack/plugins/serverless_search/server/routes/indices_routes.ts index 5c3e2187b1333..6b7ba424fde22 100644 --- a/x-pack/plugins/serverless_search/server/routes/indices_routes.ts +++ b/x-pack/plugins/serverless_search/server/routes/indices_routes.ts @@ -13,8 +13,9 @@ import { DEFAULT_DOCS_PER_PAGE } from '@kbn/search-index-documents/types'; import { fetchIndices } from '../lib/indices/fetch_indices'; import { fetchIndex } from '../lib/indices/fetch_index'; import { RouteDependencies } from '../plugin'; +import { errorHandler } from '../utils/error_handler'; -export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies) => { +export const registerIndicesRoutes = ({ logger, router }: RouteDependencies) => { router.get( { path: '/internal/serverless_search/indices', @@ -26,7 +27,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const core = await context.core; const client = core.elasticsearch.client.asCurrentUser; const user = core.security.authc.getCurrentUser(); @@ -47,7 +48,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies }, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.get( @@ -59,7 +60,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const client = (await context.core).elasticsearch.client.asCurrentUser; const result = await client.indices.get({ @@ -74,7 +75,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies }, headers: { 'content-type': 'application/json' }, }); - } + }) ); router.get( @@ -86,7 +87,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const body = await fetchIndex(client.asCurrentUser, request.params.indexName); return body @@ -95,7 +96,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies headers: { 'content-type': 'application/json' }, }) : response.notFound(); - } + }) ); router.post( @@ -119,7 +120,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const client = (await context.core).elasticsearch.client.asCurrentUser; const indexName = decodeURIComponent(request.params.index_name); const searchQuery = request.body.searchQuery; @@ -134,7 +135,7 @@ export const registerIndicesRoutes = ({ router, getSecurity }: RouteDependencies }, headers: { 'content-type': 'application/json' }, }); - } + }) ); }; diff --git a/x-pack/plugins/serverless_search/server/routes/ingest_pipeline_routes.ts b/x-pack/plugins/serverless_search/server/routes/ingest_pipeline_routes.ts index 0853540c66f04..349a637273135 100644 --- a/x-pack/plugins/serverless_search/server/routes/ingest_pipeline_routes.ts +++ b/x-pack/plugins/serverless_search/server/routes/ingest_pipeline_routes.ts @@ -6,23 +6,36 @@ */ import { RouteDependencies } from '../plugin'; +import { errorHandler } from '../utils/error_handler'; -export const registerIngestPipelineRoutes = ({ router }: RouteDependencies) => { +export const registerIngestPipelineRoutes = ({ logger, router }: RouteDependencies) => { router.get( { path: '/internal/serverless_search/ingest_pipelines', validate: {}, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; + const privileges = await client.asCurrentUser.security.hasPrivileges({ + cluster: ['manage_pipeline'], + }); + + const canManagePipelines = privileges?.cluster.manage_pipeline; + + if (!canManagePipelines) { + return response.ok({ + body: { pipelines: {}, canManagePipelines: false }, + }); + } const pipelines = await client.asCurrentUser.ingest.getPipeline(); return response.ok({ body: { pipelines, + canManagePipelines, }, headers: { 'content-type': 'application/json' }, }); - } + }) ); }; diff --git a/x-pack/plugins/serverless_search/server/routes/mapping_routes.ts b/x-pack/plugins/serverless_search/server/routes/mapping_routes.ts index bb6e22a1bd8fe..520de808b41c9 100644 --- a/x-pack/plugins/serverless_search/server/routes/mapping_routes.ts +++ b/x-pack/plugins/serverless_search/server/routes/mapping_routes.ts @@ -7,8 +7,9 @@ import { schema } from '@kbn/config-schema'; import { RouteDependencies } from '../plugin'; +import { errorHandler } from '../utils/error_handler'; -export const registerMappingRoutes = ({ router }: RouteDependencies) => { +export const registerMappingRoutes = ({ logger, router }: RouteDependencies) => { router.get( { path: '/internal/serverless_search/mappings/{index_name}', @@ -18,7 +19,7 @@ export const registerMappingRoutes = ({ router }: RouteDependencies) => { }), }, }, - async (context, request, response) => { + errorHandler(logger)(async (context, request, response) => { const { client } = (await context.core).elasticsearch; const mapping = await client.asCurrentUser.indices.getMapping({ expand_wildcards: ['open'], @@ -28,6 +29,6 @@ export const registerMappingRoutes = ({ router }: RouteDependencies) => { body: mapping[request.params.index_name], headers: { 'content-type': 'application/json' }, }); - } + }) ); }; diff --git a/x-pack/plugins/serverless_search/server/utils/error_handler.ts b/x-pack/plugins/serverless_search/server/utils/error_handler.ts new file mode 100644 index 0000000000000..b4b3894125bdb --- /dev/null +++ b/x-pack/plugins/serverless_search/server/utils/error_handler.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandlerWrapper } from '@kbn/core-http-server'; +import { KibanaServerError } from '@kbn/kibana-utils-plugin/common'; +import type { Logger } from '@kbn/logging'; + +function isKibanaServerError(error: any): error is KibanaServerError { + return error.statusCode && error.message; +} + +export const errorHandler: (logger: Logger) => RequestHandlerWrapper = (logger) => (handler) => { + return async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (e) { + logger.error(e); + if (isKibanaServerError(e)) { + return response.customError({ statusCode: e.statusCode, body: e.message }); + } + throw e; + } + }; +}; diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json index cc3b7b073dcee..0f7a803a68f7d 100644 --- a/x-pack/plugins/serverless_search/tsconfig.json +++ b/x-pack/plugins/serverless_search/tsconfig.json @@ -52,5 +52,8 @@ "@kbn/search-inference-endpoints", "@kbn/security-plugin-types-common", "@kbn/search-indices", + "@kbn/core-http-server", + "@kbn/logging", + "@kbn/security-plugin-types-public", ] } diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_landing_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_landing_page.ts index 3618fed58dcf3..4a9d858914dc6 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_landing_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_landing_page.ts @@ -75,7 +75,7 @@ export function SvlSearchLandingPageProvider({ getService }: FtrProviderContext) }, pipeline: { async createPipeline() { - await testSubjects.click('create-a-pipeline-button'); + await testSubjects.clickWhenNotDisabled('create-a-pipeline-button'); }, async expectNavigateToCreatePipelinePage() { expect(await browser.getCurrentUrl()).contain( @@ -83,7 +83,7 @@ export function SvlSearchLandingPageProvider({ getService }: FtrProviderContext) ); }, async managePipeline() { - await testSubjects.click('manage-pipeline-button'); + await testSubjects.clickWhenNotDisabled('manage-pipeline-button'); }, async expectNavigateToManagePipelinePage() { expect(await browser.getCurrentUrl()).contain('/app/management/ingest/ingest_pipelines'); diff --git a/x-pack/test_serverless/functional/test_suites/search/getting_started.ts b/x-pack/test_serverless/functional/test_suites/search/getting_started.ts index f521a03ccde85..dac9b2d7dae94 100644 --- a/x-pack/test_serverless/functional/test_suites/search/getting_started.ts +++ b/x-pack/test_serverless/functional/test_suites/search/getting_started.ts @@ -15,7 +15,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('getting started', function () { before(async () => { - await pageObjects.svlCommonPage.loginAsViewer(); + await pageObjects.svlCommonPage.loginAsAdmin(); }); it('has serverless side nav', async () => {