mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[cloud] Create IFRAME chat component; add to Unified Integrations (#123772)
* [cloud] Create IFRAME chat component; add to Unified Integrations * Provide chat user data from endpoint * Apply suggestions from code review Co-authored-by: Luke Elmers <lukeelmers@gmail.com> * Addressing feedback * Addressing feedback * Fixing package.json * Addressing feedback * Wrap chat config in an observable * Add tests * Add integration tests, docs Co-authored-by: Sergei Poluektov <sergei.poluektov@elastic.co> Co-authored-by: Luke Elmers <lukeelmers@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
fabaa88791
commit
940c4e2833
40 changed files with 1125 additions and 79 deletions
|
@ -15,6 +15,7 @@ const STORYBOOKS = [
|
|||
'apm',
|
||||
'canvas',
|
||||
'ci_composite',
|
||||
'cloud',
|
||||
'codeeditor',
|
||||
'custom_integrations',
|
||||
'dashboard_enhanced',
|
||||
|
|
|
@ -2,7 +2,24 @@
|
|||
"id": "cloud",
|
||||
"client": {
|
||||
"classes": [],
|
||||
"functions": [],
|
||||
"functions": [
|
||||
{
|
||||
"parentPluginId": "cloud",
|
||||
"id": "def-public.Chat",
|
||||
"type": "Function",
|
||||
"tags": [],
|
||||
"label": "Chat",
|
||||
"description": [],
|
||||
"signature": [
|
||||
"() => JSX.Element"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/components/index.tsx",
|
||||
"deprecated": false,
|
||||
"children": [],
|
||||
"returnComment": [],
|
||||
"initialIsOpen": false
|
||||
}
|
||||
],
|
||||
"interfaces": [
|
||||
{
|
||||
"parentPluginId": "cloud",
|
||||
|
@ -11,7 +28,7 @@
|
|||
"tags": [],
|
||||
"label": "CloudConfigType",
|
||||
"description": [],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false,
|
||||
"children": [
|
||||
{
|
||||
|
@ -24,7 +41,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -37,7 +54,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -50,7 +67,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -63,7 +80,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -76,7 +93,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -89,7 +106,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -102,8 +119,78 @@
|
|||
"signature": [
|
||||
"{ enabled: boolean; org_id?: string | undefined; }"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"parentPluginId": "cloud",
|
||||
"id": "def-public.CloudConfigType.chat",
|
||||
"type": "Object",
|
||||
"tags": [],
|
||||
"label": "chat",
|
||||
"description": [],
|
||||
"signature": [
|
||||
"{ enabled: boolean; chatURL: string; }"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
}
|
||||
],
|
||||
"initialIsOpen": false
|
||||
},
|
||||
{
|
||||
"parentPluginId": "cloud",
|
||||
"id": "def-public.CloudStart",
|
||||
"type": "Interface",
|
||||
"tags": [],
|
||||
"label": "CloudStart",
|
||||
"description": [],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false,
|
||||
"children": [
|
||||
{
|
||||
"parentPluginId": "cloud",
|
||||
"id": "def-public.CloudStart.CloudContextProvider",
|
||||
"type": "Function",
|
||||
"tags": [],
|
||||
"label": "CloudContextProvider",
|
||||
"description": [
|
||||
"\nA React component that provides a pre-wired `React.Context` which connects components to Cloud services."
|
||||
],
|
||||
"signature": [
|
||||
"React.FunctionComponent<{}>"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false,
|
||||
"returnComment": [],
|
||||
"children": [
|
||||
{
|
||||
"parentPluginId": "cloud",
|
||||
"id": "def-public.CloudStart.CloudContextProvider.$1",
|
||||
"type": "CompoundType",
|
||||
"tags": [],
|
||||
"label": "props",
|
||||
"description": [],
|
||||
"signature": [
|
||||
"P & { children?: React.ReactNode; }"
|
||||
],
|
||||
"path": "node_modules/@types/react/index.d.ts",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"parentPluginId": "cloud",
|
||||
"id": "def-public.CloudStart.CloudContextProvider.$2",
|
||||
"type": "Any",
|
||||
"tags": [],
|
||||
"label": "context",
|
||||
"description": [],
|
||||
"signature": [
|
||||
"any"
|
||||
],
|
||||
"path": "node_modules/@types/react/index.d.ts",
|
||||
"deprecated": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"initialIsOpen": false
|
||||
|
@ -119,7 +206,7 @@
|
|||
"tags": [],
|
||||
"label": "CloudSetup",
|
||||
"description": [],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false,
|
||||
"children": [
|
||||
{
|
||||
|
@ -132,7 +219,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -145,7 +232,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -158,7 +245,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -171,7 +258,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -184,7 +271,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -197,7 +284,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -210,7 +297,7 @@
|
|||
"signature": [
|
||||
"string | undefined"
|
||||
],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
|
@ -220,7 +307,7 @@
|
|||
"tags": [],
|
||||
"label": "isCloudEnabled",
|
||||
"description": [],
|
||||
"path": "x-pack/plugins/cloud/public/plugin.ts",
|
||||
"path": "x-pack/plugins/cloud/public/plugin.tsx",
|
||||
"deprecated": false
|
||||
}
|
||||
],
|
||||
|
|
|
@ -278,6 +278,7 @@
|
|||
"json-stable-stringify": "^1.0.1",
|
||||
"json-stringify-pretty-compact": "1.2.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"jsts": "^1.6.2",
|
||||
"kea": "^2.4.2",
|
||||
"load-json-file": "^6.2.0",
|
||||
|
|
|
@ -11,6 +11,7 @@ export const storybookAliases = {
|
|||
apm: 'x-pack/plugins/apm/.storybook',
|
||||
canvas: 'x-pack/plugins/canvas/storybook',
|
||||
ci_composite: '.ci/.storybook',
|
||||
cloud: 'x-pack/plugins/cloud/.storybook',
|
||||
codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook',
|
||||
controls: 'src/plugins/controls/storybook',
|
||||
custom_integrations: 'src/plugins/custom_integrations/storybook',
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
jest.mock('jsonwebtoken', () => ({
|
||||
sign: jest.fn(),
|
||||
}));
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
|
||||
import jwt, { Algorithm } from 'jsonwebtoken';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
|
|
35
x-pack/plugins/cloud/.storybook/decorator.tsx
Normal file
35
x-pack/plugins/cloud/.storybook/decorator.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DecoratorFn } from '@storybook/react';
|
||||
import { ServicesProvider, CloudServices } from '../public/services';
|
||||
|
||||
// TODO: move to a storybook implementation of the service using parameters.
|
||||
const services: CloudServices = {
|
||||
chat: {
|
||||
enabled: true,
|
||||
chatURL: 'https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html',
|
||||
user: {
|
||||
id: 'user-id',
|
||||
email: 'test-user@elastic.co',
|
||||
// this doesn't affect chat appearance,
|
||||
// but a user identity in Drift only
|
||||
jwt: 'identity-jwt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getCloudContextProvider: () => React.FC =
|
||||
() =>
|
||||
({ children }) =>
|
||||
<ServicesProvider {...services}>{children}</ServicesProvider>;
|
||||
|
||||
export const getCloudContextDecorator: DecoratorFn = (storyFn) => {
|
||||
const CloudContextProvider = getCloudContextProvider();
|
||||
return <CloudContextProvider>{storyFn()}</CloudContextProvider>;
|
||||
};
|
8
x-pack/plugins/cloud/.storybook/index.ts
Normal file
8
x-pack/plugins/cloud/.storybook/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { getCloudContextDecorator, getCloudContextProvider } from './decorator';
|
10
x-pack/plugins/cloud/.storybook/main.ts
Normal file
10
x-pack/plugins/cloud/.storybook/main.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { defaultConfig } from '@kbn/storybook';
|
||||
|
||||
module.exports = defaultConfig;
|
20
x-pack/plugins/cloud/.storybook/manager.ts
Normal file
20
x-pack/plugins/cloud/.storybook/manager.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { addons } from '@storybook/addons';
|
||||
import { create } from '@storybook/theming';
|
||||
import { PANEL_ID } from '@storybook/addon-actions';
|
||||
|
||||
addons.setConfig({
|
||||
theme: create({
|
||||
base: 'light',
|
||||
brandTitle: 'Cloud Storybook',
|
||||
brandUrl: 'https://github.com/elastic/kibana/tree/main/x-pack/plugins/cloud',
|
||||
}),
|
||||
showPanel: true.valueOf,
|
||||
selectedPanel: PANEL_ID,
|
||||
});
|
11
x-pack/plugins/cloud/.storybook/preview.ts
Normal file
11
x-pack/plugins/cloud/.storybook/preview.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { addDecorator } from '@storybook/react';
|
||||
import { getCloudContextDecorator } from './decorator';
|
||||
|
||||
addDecorator(getCloudContextDecorator);
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export const ELASTIC_SUPPORT_LINK = 'https://cloud.elastic.co/support';
|
||||
export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user';
|
||||
|
||||
/**
|
||||
* This is the page for managing your snapshots on Cloud.
|
||||
|
|
12
x-pack/plugins/cloud/common/types.ts
Normal file
12
x-pack/plugins/cloud/common/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface GetChatUserDataResponseBody {
|
||||
token: string;
|
||||
email: string;
|
||||
id: string;
|
||||
}
|
20
x-pack/plugins/cloud/public/components/chat/chat.stories.tsx
Normal file
20
x-pack/plugins/cloud/public/components/chat/chat.stories.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { Chat } from './chat';
|
||||
|
||||
export default {
|
||||
title: 'Chat Widget',
|
||||
description: '',
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return <Chat />;
|
||||
};
|
111
x-pack/plugins/cloud/public/components/chat/chat.tsx
Normal file
111
x-pack/plugins/cloud/public/components/chat/chat.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 React, { useEffect, useRef, useState, CSSProperties } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { useChat } from '../../services';
|
||||
import { getChatContext } from './get_chat_context';
|
||||
|
||||
type UseChatType =
|
||||
| { enabled: false }
|
||||
| {
|
||||
enabled: true;
|
||||
src: string;
|
||||
ref: React.MutableRefObject<HTMLIFrameElement | null>;
|
||||
style: CSSProperties;
|
||||
isReady: boolean;
|
||||
};
|
||||
|
||||
const MESSAGE_READY = 'driftIframeReady';
|
||||
const MESSAGE_RESIZE = 'driftIframeResize';
|
||||
const MESSAGE_SET_CONTEXT = 'driftSetContext';
|
||||
|
||||
const useChatConfig = (): UseChatType => {
|
||||
const ref = useRef<HTMLIFrameElement>(null);
|
||||
const chat = useChat();
|
||||
const [style, setStyle] = useState<CSSProperties>({});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent): void => {
|
||||
const { current: chatIframe } = ref;
|
||||
|
||||
if (
|
||||
!chat.enabled ||
|
||||
!chatIframe?.contentWindow ||
|
||||
event.source !== chatIframe?.contentWindow
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getChatContext();
|
||||
const { data: message } = event;
|
||||
const { user: userConfig } = chat;
|
||||
const { id, email, jwt } = userConfig;
|
||||
|
||||
switch (message.type) {
|
||||
case MESSAGE_READY: {
|
||||
const user = {
|
||||
id,
|
||||
attributes: {
|
||||
email,
|
||||
},
|
||||
jwt,
|
||||
};
|
||||
|
||||
chatIframe.contentWindow.postMessage(
|
||||
{
|
||||
type: MESSAGE_SET_CONTEXT,
|
||||
data: { context, user },
|
||||
},
|
||||
'*'
|
||||
);
|
||||
|
||||
setIsReady(true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MESSAGE_RESIZE: {
|
||||
const styles = message.data.styles || ({} as CSSProperties);
|
||||
setStyle({ ...style, ...styles });
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [chat, style]);
|
||||
|
||||
if (chat.enabled) {
|
||||
return { enabled: true, src: chat.chatURL, ref, style, isReady };
|
||||
}
|
||||
|
||||
return { enabled: false };
|
||||
};
|
||||
|
||||
export const Chat = () => {
|
||||
const config = useChatConfig();
|
||||
|
||||
if (!config.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iframeStyle = css`
|
||||
position: fixed;
|
||||
botton: 30px;
|
||||
right: 30px;
|
||||
visibility: ${config.isReady ? 'visible' : 'hidden'};
|
||||
`;
|
||||
|
||||
return <iframe css={iframeStyle} data-test-subj="floatingChatTrigger" title="chat" {...config} />;
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { getChatContext } from './get_chat_context';
|
||||
|
||||
const PROTOCOL = 'http:';
|
||||
const PORT = '1234';
|
||||
const HASH = '#/discover?_g=()&_a=()';
|
||||
const HOST_NAME = 'www.kibana.com';
|
||||
const PATH_NAME = '/app/kibana';
|
||||
const HOST = `${HOST_NAME}:${PORT}`;
|
||||
const ORIGIN = `${PROTOCOL}//${HOST}`;
|
||||
const HREF = `${ORIGIN}${PATH_NAME}${HASH}`;
|
||||
const USER_AGENT = 'user-agent';
|
||||
const LANGUAGE = 'la-ng';
|
||||
const TITLE = 'title';
|
||||
const REFERRER = 'referrer';
|
||||
|
||||
describe('getChatContext', () => {
|
||||
const url = new URL(HREF);
|
||||
|
||||
test('retreive the context', () => {
|
||||
Object.defineProperty(window, 'location', { value: url });
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: {
|
||||
language: LANGUAGE,
|
||||
userAgent: USER_AGENT,
|
||||
},
|
||||
});
|
||||
Object.defineProperty(window.document, 'referrer', { value: REFERRER });
|
||||
window.document.title = TITLE;
|
||||
|
||||
const context = getChatContext();
|
||||
|
||||
expect(context).toStrictEqual({
|
||||
window: {
|
||||
location: {
|
||||
hash: HASH,
|
||||
host: HOST,
|
||||
hostname: HOST_NAME,
|
||||
href: HREF,
|
||||
origin: ORIGIN,
|
||||
pathname: PATH_NAME,
|
||||
port: PORT,
|
||||
protocol: PROTOCOL,
|
||||
search: '',
|
||||
},
|
||||
navigator: {
|
||||
language: LANGUAGE,
|
||||
userAgent: USER_AGENT,
|
||||
},
|
||||
innerHeight: 768,
|
||||
innerWidth: 1024,
|
||||
},
|
||||
document: {
|
||||
title: TITLE,
|
||||
referrer: REFERRER,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const getChatContext = () => {
|
||||
const { location, navigator, innerHeight, innerWidth } = window;
|
||||
const { hash, host, hostname, href, origin, pathname, port, protocol, search } = location;
|
||||
const { language, userAgent } = navigator;
|
||||
const { title, referrer } = document;
|
||||
|
||||
return {
|
||||
window: {
|
||||
location: {
|
||||
hash,
|
||||
host,
|
||||
hostname,
|
||||
href,
|
||||
origin,
|
||||
pathname,
|
||||
port,
|
||||
protocol,
|
||||
search,
|
||||
},
|
||||
navigator: { language, userAgent },
|
||||
innerHeight,
|
||||
innerWidth,
|
||||
},
|
||||
document: {
|
||||
title,
|
||||
referrer,
|
||||
},
|
||||
};
|
||||
};
|
12
x-pack/plugins/cloud/public/components/chat/index.ts
Normal file
12
x-pack/plugins/cloud/public/components/chat/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A component that will display a trigger that will allow the user to chat with a human operator,
|
||||
* when the service is enabled; otherwise, it renders nothing.
|
||||
*/
|
||||
export { Chat } from './chat';
|
26
x-pack/plugins/cloud/public/components/index.tsx
Normal file
26
x-pack/plugins/cloud/public/components/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 React, { Suspense } from 'react';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
|
||||
/**
|
||||
* A suspense-compatible version of the Chat component.
|
||||
*/
|
||||
export const LazyChat = React.lazy(() => import('./chat').then(({ Chat }) => ({ default: Chat })));
|
||||
|
||||
/**
|
||||
* A lazily-loaded component that will display a trigger that will allow the user to chat with a
|
||||
* human operator when the service is enabled; otherwise, it renders nothing.
|
||||
*/
|
||||
export const Chat = () => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<div />}>
|
||||
<LazyChat />
|
||||
</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
);
|
|
@ -8,7 +8,10 @@
|
|||
import { PluginInitializerContext } from '../../../../src/core/public';
|
||||
import { CloudPlugin } from './plugin';
|
||||
|
||||
export type { CloudSetup, CloudConfigType } from './plugin';
|
||||
export type { CloudSetup, CloudConfigType, CloudStart } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export { Chat } from './components';
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function createSetupMock() {
|
||||
return {
|
||||
cloudId: 'mock-cloud-id',
|
||||
isCloudEnabled: true,
|
||||
cname: 'cname',
|
||||
baseUrl: 'base-url',
|
||||
deploymentUrl: 'deployment-url',
|
||||
profileUrl: 'profile-url',
|
||||
organizationUrl: 'organization-url',
|
||||
};
|
||||
}
|
||||
|
||||
export const cloudMock = {
|
||||
createSetup: createSetupMock,
|
||||
};
|
49
x-pack/plugins/cloud/public/mocks.tsx
Normal file
49
x-pack/plugins/cloud/public/mocks.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { CloudStart } from '.';
|
||||
import { ServicesProvider } from '../public/services';
|
||||
|
||||
function createSetupMock() {
|
||||
return {
|
||||
cloudId: 'mock-cloud-id',
|
||||
isCloudEnabled: true,
|
||||
cname: 'cname',
|
||||
baseUrl: 'base-url',
|
||||
deploymentUrl: 'deployment-url',
|
||||
profileUrl: 'profile-url',
|
||||
organizationUrl: 'organization-url',
|
||||
};
|
||||
}
|
||||
|
||||
const config = {
|
||||
chat: {
|
||||
enabled: true,
|
||||
chatURL: 'chat-url',
|
||||
user: {
|
||||
id: 'user-id',
|
||||
email: 'test-user@elastic.co',
|
||||
jwt: 'identity-jwt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getContextProvider: () => React.FC =
|
||||
() =>
|
||||
({ children }) =>
|
||||
<ServicesProvider {...config}>{children}</ServicesProvider>;
|
||||
|
||||
const createStartMock = (): jest.Mocked<CloudStart> => ({
|
||||
CloudContextProvider: jest.fn(getContextProvider()),
|
||||
});
|
||||
|
||||
export const cloudMock = {
|
||||
createSetup: createSetupMock,
|
||||
createStart: createStartMock,
|
||||
};
|
|
@ -40,6 +40,9 @@ describe('Cloud Plugin', () => {
|
|||
full_story: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
enabled: false,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
|
||||
|
@ -47,11 +50,15 @@ describe('Cloud Plugin', () => {
|
|||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
if (currentAppId$) {
|
||||
coreStart.application.currentAppId$ = currentAppId$;
|
||||
}
|
||||
|
||||
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
|
||||
|
||||
const securitySetup = securityMock.createSetup();
|
||||
|
||||
securitySetup.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser(currentUserProps)
|
||||
);
|
||||
|
@ -212,6 +219,101 @@ describe('Cloud Plugin', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setupChat', () => {
|
||||
let consoleMock: jest.SpyInstance<void, [message?: any, ...optionalParams: any[]]>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleMock.mockRestore();
|
||||
});
|
||||
|
||||
const setupPlugin = async ({
|
||||
config = {},
|
||||
securityEnabled = true,
|
||||
currentUserProps = {},
|
||||
isCloudEnabled = true,
|
||||
failHttp = false,
|
||||
}: {
|
||||
config?: Partial<CloudConfigType>;
|
||||
securityEnabled?: boolean;
|
||||
currentUserProps?: Record<string, any>;
|
||||
isCloudEnabled?: boolean;
|
||||
failHttp?: boolean;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
id: isCloudEnabled ? 'cloud-id' : null,
|
||||
base_url: 'https://cloud.elastic.co',
|
||||
deployment_url: '/abc123',
|
||||
profile_url: '/profile/alice',
|
||||
organization_url: '/org/myOrg',
|
||||
full_story: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
enabled: false,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
|
||||
const plugin = new CloudPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
if (failHttp) {
|
||||
coreSetup.http.get.mockImplementation(() => {
|
||||
throw new Error('HTTP request failed');
|
||||
});
|
||||
}
|
||||
|
||||
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
|
||||
|
||||
const securitySetup = securityMock.createSetup();
|
||||
securitySetup.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser(currentUserProps)
|
||||
);
|
||||
|
||||
const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {});
|
||||
|
||||
return { initContext, plugin, setup, coreSetup };
|
||||
};
|
||||
|
||||
it('chatConfig is not retrieved if cloud is not enabled', async () => {
|
||||
const { coreSetup } = await setupPlugin({ isCloudEnabled: false });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if security is not enabled', async () => {
|
||||
const { coreSetup } = await setupPlugin({ securityEnabled: false });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if chat is enabled but url is not provided', async () => {
|
||||
// @ts-expect-error 2741
|
||||
const { coreSetup } = await setupPlugin({ config: { chat: { enabled: true } } });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if internal API fails', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
|
||||
failHttp: true,
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
expect(consoleMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is retrieved if chat is enabled and url is provided', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interface', () => {
|
||||
const setupPlugin = () => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
|
@ -221,6 +323,12 @@ describe('Cloud Plugin', () => {
|
|||
deployment_url: '/abc123',
|
||||
profile_url: '/user/settings/',
|
||||
organization_url: '/account/',
|
||||
chat: {
|
||||
enabled: false,
|
||||
},
|
||||
full_story: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
const plugin = new CloudPlugin(initContext);
|
||||
|
||||
|
@ -284,6 +392,9 @@ describe('Cloud Plugin', () => {
|
|||
full_story: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
|
@ -15,17 +16,24 @@ import {
|
|||
ApplicationStart,
|
||||
} from 'src/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Subscription } from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import type {
|
||||
AuthenticatedUser,
|
||||
SecurityPluginSetup,
|
||||
SecurityPluginStart,
|
||||
} from '../../security/public';
|
||||
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
|
||||
import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants';
|
||||
import {
|
||||
ELASTIC_SUPPORT_LINK,
|
||||
CLOUD_SNAPSHOTS_PATH,
|
||||
GET_CHAT_USER_DATA_ROUTE_PATH,
|
||||
} from '../common/constants';
|
||||
import type { GetChatUserDataResponseBody } from '../common/types';
|
||||
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
|
||||
import { createUserMenuLinks } from './user_menu_links';
|
||||
import { getFullCloudUrl } from './utils';
|
||||
import { ChatConfig, ServicesProvider } from './services';
|
||||
|
||||
export interface CloudConfigType {
|
||||
id?: string;
|
||||
|
@ -38,6 +46,13 @@ export interface CloudConfigType {
|
|||
enabled: boolean;
|
||||
org_id?: string;
|
||||
};
|
||||
/** Configuration to enable live chat in Cloud-enabled instances of Kibana. */
|
||||
chat: {
|
||||
/** Determines if chat is enabled. */
|
||||
enabled: boolean;
|
||||
/** The URL to the remotely-hosted chat application. */
|
||||
chatURL: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CloudSetupDependencies {
|
||||
|
@ -49,6 +64,13 @@ interface CloudStartDependencies {
|
|||
security?: SecurityPluginStart;
|
||||
}
|
||||
|
||||
export interface CloudStart {
|
||||
/**
|
||||
* A React component that provides a pre-wired `React.Context` which connects components to Cloud services.
|
||||
*/
|
||||
CloudContextProvider: FC<{}>;
|
||||
}
|
||||
|
||||
export interface CloudSetup {
|
||||
cloudId?: string;
|
||||
cname?: string;
|
||||
|
@ -65,10 +87,15 @@ interface SetupFullstoryDeps extends CloudSetupDependencies {
|
|||
basePath: IBasePath;
|
||||
}
|
||||
|
||||
interface SetupChatDeps extends Pick<CloudSetupDependencies, 'security'> {
|
||||
http: CoreSetup['http'];
|
||||
}
|
||||
|
||||
export class CloudPlugin implements Plugin<CloudSetup> {
|
||||
private config!: CloudConfigType;
|
||||
private isCloudEnabled: boolean;
|
||||
private appSubscription?: Subscription;
|
||||
private chatConfig$ = new BehaviorSubject<ChatConfig>({ enabled: false });
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<CloudConfigType>();
|
||||
|
@ -79,6 +106,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
const application = core.getStartServices().then(([coreStart]) => {
|
||||
return coreStart.application;
|
||||
});
|
||||
|
||||
this.setupFullstory({ basePath: core.http.basePath, security, application }).catch((e) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error setting up FullStory: ${e.toString()}`)
|
||||
|
@ -95,6 +123,11 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
|
||||
this.isCloudEnabled = getIsCloudEnabled(id);
|
||||
|
||||
this.setupChat({ http: core.http, security }).catch((e) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error setting up Chat: ${e.toString()}`)
|
||||
);
|
||||
|
||||
if (home) {
|
||||
home.environment.update({ cloud: this.isCloudEnabled });
|
||||
if (this.isCloudEnabled) {
|
||||
|
@ -119,7 +152,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
};
|
||||
}
|
||||
|
||||
public start(coreStart: CoreStart, { security }: CloudStartDependencies) {
|
||||
public start(coreStart: CoreStart, { security }: CloudStartDependencies): CloudStart {
|
||||
const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config;
|
||||
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
|
||||
|
||||
|
@ -147,6 +180,17 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
// In the event of an unexpected error, fail *open*.
|
||||
// Cloud admin console will always perform the actual authorization checks.
|
||||
.catch(() => setLinks(true));
|
||||
|
||||
// There's a risk that the request for chat config will take too much time to complete, and the provider
|
||||
// will maintain a stale value. To avoid this, we'll use an Observable.
|
||||
const CloudContextProvider: FC = ({ children }) => {
|
||||
const chatConfig = useObservable(this.chatConfig$, { enabled: false });
|
||||
return <ServicesProvider chat={chatConfig}>{children}</ServicesProvider>;
|
||||
};
|
||||
|
||||
return {
|
||||
CloudContextProvider,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
@ -266,6 +310,43 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
...memoryInfo,
|
||||
});
|
||||
}
|
||||
|
||||
private async setupChat({ http, security }: SetupChatDeps) {
|
||||
if (!this.isCloudEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { enabled, chatURL } = this.config.chat;
|
||||
|
||||
if (!security || !enabled || !chatURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
id,
|
||||
token: jwt,
|
||||
} = await http.get<GetChatUserDataResponseBody>(GET_CHAT_USER_DATA_ROUTE_PATH);
|
||||
|
||||
if (!email || !id || !jwt) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatConfig$.next({
|
||||
enabled,
|
||||
chatURL,
|
||||
user: {
|
||||
email,
|
||||
id,
|
||||
jwt,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`[cloud.chat] Could not retrieve chat config: ${e.res.status} ${e.message}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal exported for testing */
|
46
x-pack/plugins/cloud/public/services/index.tsx
Normal file
46
x-pack/plugins/cloud/public/services/index.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 React, { FC, createContext, useContext } from 'react';
|
||||
|
||||
interface WithoutChat {
|
||||
enabled: false;
|
||||
}
|
||||
|
||||
interface WithChat {
|
||||
enabled: true;
|
||||
chatURL: string;
|
||||
user: {
|
||||
jwt: string;
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type ChatConfig = WithChat | WithoutChat;
|
||||
|
||||
export interface CloudServices {
|
||||
chat: ChatConfig;
|
||||
}
|
||||
|
||||
const ServicesContext = createContext<CloudServices>({ chat: { enabled: false } });
|
||||
|
||||
export const ServicesProvider: FC<CloudServices> = ({ children, ...services }) => (
|
||||
<ServicesContext.Provider value={services}>{children}</ServicesContext.Provider>
|
||||
);
|
||||
|
||||
/**
|
||||
* React hook for accessing the pre-wired `CloudServices`.
|
||||
*/
|
||||
export function useServices() {
|
||||
return useContext(ServicesContext);
|
||||
}
|
||||
|
||||
export function useChat(): ChatConfig {
|
||||
const { chat } = useServices();
|
||||
return chat;
|
||||
}
|
|
@ -28,28 +28,36 @@ const fullStoryConfigSchema = schema.object({
|
|||
),
|
||||
});
|
||||
|
||||
const chatConfigSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
chatURL: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
const configSchema = schema.object({
|
||||
id: schema.maybe(schema.string()),
|
||||
apm: schema.maybe(apmConfigSchema),
|
||||
cname: schema.maybe(schema.string()),
|
||||
base_url: schema.maybe(schema.string()),
|
||||
profile_url: schema.maybe(schema.string()),
|
||||
chat: chatConfigSchema,
|
||||
chatIdentitySecret: schema.maybe(schema.string()),
|
||||
cname: schema.maybe(schema.string()),
|
||||
deployment_url: schema.maybe(schema.string()),
|
||||
organization_url: schema.maybe(schema.string()),
|
||||
full_story: fullStoryConfigSchema,
|
||||
id: schema.maybe(schema.string()),
|
||||
organization_url: schema.maybe(schema.string()),
|
||||
profile_url: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export type CloudConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<CloudConfigType> = {
|
||||
exposeToBrowser: {
|
||||
id: true,
|
||||
cname: true,
|
||||
base_url: true,
|
||||
profile_url: true,
|
||||
chat: true,
|
||||
cname: true,
|
||||
deployment_url: true,
|
||||
organization_url: true,
|
||||
full_story: true,
|
||||
id: true,
|
||||
organization_url: true,
|
||||
profile_url: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
|
|
@ -7,14 +7,17 @@
|
|||
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server';
|
||||
import type { SecurityPluginSetup } from '../../security/server';
|
||||
import { CloudConfigType } from './config';
|
||||
import { registerCloudUsageCollector } from './collectors';
|
||||
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
|
||||
import { parseDeploymentIdFromDeploymentUrl } from './utils';
|
||||
import { registerFullstoryRoute } from './routes/fullstory';
|
||||
import { registerChatRoute } from './routes/chat';
|
||||
|
||||
interface PluginsSetup {
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
export interface CloudSetup {
|
||||
|
@ -30,13 +33,15 @@ export interface CloudSetup {
|
|||
export class CloudPlugin implements Plugin<CloudSetup> {
|
||||
private readonly logger: Logger;
|
||||
private readonly config: CloudConfigType;
|
||||
private isDev: boolean;
|
||||
|
||||
constructor(private readonly context: PluginInitializerContext) {
|
||||
this.logger = this.context.logger.get();
|
||||
this.config = this.context.config.get<CloudConfigType>();
|
||||
this.isDev = this.context.env.mode.dev;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, { usageCollection }: PluginsSetup) {
|
||||
public setup(core: CoreSetup, { usageCollection, security }: PluginsSetup) {
|
||||
this.logger.debug('Setting up Cloud plugin');
|
||||
const isCloudEnabled = getIsCloudEnabled(this.config.id);
|
||||
registerCloudUsageCollector(usageCollection, { isCloudEnabled });
|
||||
|
@ -48,6 +53,15 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.config.chat.enabled && this.config.chatIdentitySecret) {
|
||||
registerChatRoute({
|
||||
router: core.http.createRouter(),
|
||||
chatIdentitySecret: this.config.chatIdentitySecret,
|
||||
security,
|
||||
isDev: this.isDev,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cloudId: this.config.id,
|
||||
deploymentId: parseDeploymentIdFromDeploymentUrl(this.config.deployment_url),
|
||||
|
|
109
x-pack/plugins/cloud/server/routes/chat.test.ts
Normal file
109
x-pack/plugins/cloud/server/routes/chat.test.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('jsonwebtoken', () => ({
|
||||
sign: () => {
|
||||
return 'json-web-token';
|
||||
},
|
||||
}));
|
||||
|
||||
import { httpServiceMock, httpServerMock } from '../../../../../src/core/server/mocks';
|
||||
import { securityMock } from '../../../security/server/mocks';
|
||||
import { kibanaResponseFactory } from 'src/core/server';
|
||||
import { registerChatRoute } from './chat';
|
||||
|
||||
describe('chat route', () => {
|
||||
test('do not add the route if security is not enabled', async () => {
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, isDev: false, chatIdentitySecret: 'secret' });
|
||||
expect(router.get.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
test('error if no user', async () => {
|
||||
const security = securityMock.createSetup();
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(null);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, security, isDev: false, chatIdentitySecret: 'secret' });
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": "User has no email or username",
|
||||
},
|
||||
"payload": "User has no email or username",
|
||||
"status": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns user information and a token', async () => {
|
||||
const security = securityMock.createSetup();
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce({
|
||||
username,
|
||||
email,
|
||||
});
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, security, isDev: false, chatIdentitySecret: 'secret' });
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": Object {
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
},
|
||||
"payload": Object {
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
"status": 200,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns placeholder user information and a token in dev mode', async () => {
|
||||
const security = securityMock.createSetup();
|
||||
const username = 'first.last';
|
||||
const email = 'test+first.last@elasticsearch.com';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce({});
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, security, isDev: true, chatIdentitySecret: 'secret' });
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": Object {
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
},
|
||||
"payload": Object {
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
"status": 200,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
64
x-pack/plugins/cloud/server/routes/chat.ts
Normal file
64
x-pack/plugins/cloud/server/routes/chat.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { IRouter } from '../../../../../src/core/server';
|
||||
import type { SecurityPluginSetup } from '../../../security/server';
|
||||
import { GET_CHAT_USER_DATA_ROUTE_PATH } from '../../common/constants';
|
||||
import type { GetChatUserDataResponseBody } from '../../common/types';
|
||||
import { generateSignedJwt } from '../util/generate_jwt';
|
||||
|
||||
export const registerChatRoute = ({
|
||||
router,
|
||||
chatIdentitySecret,
|
||||
security,
|
||||
isDev,
|
||||
}: {
|
||||
router: IRouter;
|
||||
chatIdentitySecret: string;
|
||||
security?: SecurityPluginSetup;
|
||||
isDev: boolean;
|
||||
}) => {
|
||||
if (!security) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: GET_CHAT_USER_DATA_ROUTE_PATH,
|
||||
validate: {},
|
||||
},
|
||||
async (_context, request, response) => {
|
||||
const user = security.authc.getCurrentUser(request);
|
||||
let { email: userEmail, username: userId } = user || {};
|
||||
|
||||
// In local development, these values are not populated. This is a workaround
|
||||
// to allow for local testing.
|
||||
if (isDev) {
|
||||
if (!userId) {
|
||||
userId = 'first.last';
|
||||
}
|
||||
if (!userEmail) {
|
||||
userEmail = userEmail || `test+${userId}@elasticsearch.com`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userEmail || !userId) {
|
||||
return response.badRequest({
|
||||
body: 'User has no email or username',
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateSignedJwt(userId, chatIdentitySecret);
|
||||
const body: GetChatUserDataResponseBody = {
|
||||
token,
|
||||
email: userEmail,
|
||||
id: userId,
|
||||
};
|
||||
return response.ok({ body });
|
||||
}
|
||||
);
|
||||
};
|
23
x-pack/plugins/cloud/server/util/generate_jwt.test.ts
Normal file
23
x-pack/plugins/cloud/server/util/generate_jwt.test.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('jsonwebtoken', () => ({
|
||||
sign: (payload: {}, secret: string, options: {}) => {
|
||||
return `${JSON.stringify(payload)}.${secret}.${JSON.stringify(options)}`;
|
||||
},
|
||||
}));
|
||||
|
||||
import { generateSignedJwt } from './generate_jwt';
|
||||
|
||||
describe('generateSignedJwt', () => {
|
||||
test('creating a JWT token', () => {
|
||||
const jwtToken = generateSignedJwt('test', '123456');
|
||||
expect(jwtToken).toEqual(
|
||||
'{"sub":"test"}.123456.{"header":{"alg":"HS256","typ":"JWT"},"expiresIn":300}'
|
||||
);
|
||||
});
|
||||
});
|
24
x-pack/plugins/cloud/server/util/generate_jwt.ts
Normal file
24
x-pack/plugins/cloud/server/util/generate_jwt.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 jwt from 'jsonwebtoken';
|
||||
|
||||
export const generateSignedJwt = (userId: string, secret: string): string => {
|
||||
const options = {
|
||||
header: {
|
||||
alg: 'HS256',
|
||||
typ: 'JWT',
|
||||
},
|
||||
expiresIn: 5 * 60, // 5m
|
||||
};
|
||||
|
||||
const payload = {
|
||||
sub: userId,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, secret, options);
|
||||
};
|
|
@ -7,9 +7,11 @@
|
|||
"declarationMap": true,
|
||||
},
|
||||
"include": [
|
||||
".storybook/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
|
|
|
@ -53,7 +53,10 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({
|
|||
...stubbedStartServices,
|
||||
application: getApplication(),
|
||||
chrome: getChrome(),
|
||||
cloud: getCloud({ isCloudEnabled }),
|
||||
cloud: {
|
||||
...getCloud({ isCloudEnabled }),
|
||||
CloudContextProvider: () => <></>,
|
||||
},
|
||||
customIntegrations: {
|
||||
ContextProvider: getStorybookContextProvider(),
|
||||
},
|
||||
|
|
|
@ -11,5 +11,5 @@
|
|||
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security"],
|
||||
"optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry"],
|
||||
"extraPublicDirs": ["common"],
|
||||
"requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils", "usageCollection"]
|
||||
"requiredBundles": ["kibanaReact", "cloud", "esUiShared", "home", "infra", "kibanaUtils", "usageCollection"]
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
RedirectAppLinks,
|
||||
} from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
|
||||
import { Chat } from '../../../../cloud/public';
|
||||
|
||||
import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
|
@ -33,6 +34,8 @@ import { EPMApp } from './sections/epm';
|
|||
import { PackageInstallProvider, UIExtensionsContext } from './hooks';
|
||||
import { IntegrationsHeader } from './components/header';
|
||||
|
||||
const EmptyContext = () => <></>;
|
||||
|
||||
/**
|
||||
* Fleet Application context all the way down to the Router, but with no permissions or setup checks
|
||||
* and no routes defined
|
||||
|
@ -60,6 +63,7 @@ export const IntegrationsAppContext: React.FC<{
|
|||
theme$,
|
||||
}) => {
|
||||
const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode'));
|
||||
const CloudContext = startServices.cloud?.CloudContextProvider || EmptyContext;
|
||||
|
||||
return (
|
||||
<RedirectAppLinks application={startServices.application}>
|
||||
|
@ -73,17 +77,20 @@ export const IntegrationsAppContext: React.FC<{
|
|||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<FleetStatusProvider>
|
||||
<startServices.customIntegrations.ContextProvider>
|
||||
<Router history={history}>
|
||||
<AgentPolicyContextProvider>
|
||||
<PackageInstallProvider
|
||||
notifications={startServices.notifications}
|
||||
theme$={theme$}
|
||||
>
|
||||
<IntegrationsHeader {...{ setHeaderActionMenu, theme$ }} />
|
||||
{children}
|
||||
</PackageInstallProvider>
|
||||
</AgentPolicyContextProvider>
|
||||
</Router>
|
||||
<CloudContext>
|
||||
<Router history={history}>
|
||||
<AgentPolicyContextProvider>
|
||||
<PackageInstallProvider
|
||||
notifications={startServices.notifications}
|
||||
theme$={theme$}
|
||||
>
|
||||
<IntegrationsHeader {...{ setHeaderActionMenu, theme$ }} />
|
||||
{children}
|
||||
<Chat />
|
||||
</PackageInstallProvider>
|
||||
</AgentPolicyContextProvider>
|
||||
</Router>
|
||||
</CloudContext>
|
||||
</startServices.customIntegrations.ContextProvider>
|
||||
</FleetStatusProvider>
|
||||
</UIExtensionsContext.Provider>
|
||||
|
|
|
@ -16,7 +16,7 @@ import { setHttpClient } from '../hooks/use_request';
|
|||
|
||||
import type { FleetAuthz } from '../../common';
|
||||
|
||||
import { createStartDepsMock } from './plugin_dependencies';
|
||||
import { createStartDepsMock, createSetupDepsMock } from './plugin_dependencies';
|
||||
import type { MockedFleetStartServices } from './types';
|
||||
|
||||
// Taken from core. See: src/plugins/kibana_utils/public/storage/storage.test.ts
|
||||
|
@ -71,9 +71,16 @@ const configureStartServices = (services: MockedFleetStartServices): void => {
|
|||
};
|
||||
|
||||
export const createStartServices = (basePath: string = '/mock'): MockedFleetStartServices => {
|
||||
const { cloud: cloudStart, ...startDeps } = createStartDepsMock();
|
||||
const { cloud: cloudSetup } = createSetupDepsMock();
|
||||
|
||||
const startServices: MockedFleetStartServices = {
|
||||
...coreMock.createStart({ basePath }),
|
||||
...createStartDepsMock(),
|
||||
...startDeps,
|
||||
cloud: {
|
||||
...cloudStart,
|
||||
...cloudSetup,
|
||||
},
|
||||
storage: new Storage(createMockStore()) as jest.Mocked<Storage>,
|
||||
authz: fleetAuthzMock,
|
||||
};
|
||||
|
|
|
@ -7,27 +7,29 @@
|
|||
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
import { licensingMock } from '../../../licensing/public/mocks';
|
||||
import { cloudMock } from '../../../cloud/public/mocks';
|
||||
import { homePluginMock } from '../../../../../src/plugins/home/public/mocks';
|
||||
import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks';
|
||||
import { customIntegrationsMock } from '../../../../../src/plugins/custom_integrations/public/mocks';
|
||||
import { sharePluginMock } from '../../../../../src/plugins/share/public/mocks';
|
||||
|
||||
import type { MockedFleetSetupDeps, MockedFleetStartDeps } from './types';
|
||||
|
||||
export const createSetupDepsMock = (): MockedFleetSetupDeps => {
|
||||
export const createSetupDepsMock = () => {
|
||||
const cloud = cloudMock.createSetup();
|
||||
return {
|
||||
licensing: licensingMock.createSetup(),
|
||||
data: dataPluginMock.createSetupContract(),
|
||||
home: homePluginMock.createSetupContract(),
|
||||
customIntegrations: customIntegrationsMock.createSetup(),
|
||||
cloud,
|
||||
};
|
||||
};
|
||||
|
||||
export const createStartDepsMock = (): MockedFleetStartDeps => {
|
||||
export const createStartDepsMock = () => {
|
||||
return {
|
||||
data: dataPluginMock.createStartContract(),
|
||||
navigation: navigationPluginMock.createStartContract(),
|
||||
customIntegrations: customIntegrationsMock.createStart(),
|
||||
share: sharePluginMock.createStartContract(),
|
||||
cloud: cloudMock.createStart(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,6 +26,8 @@ import type { SharePluginStart } from 'src/plugins/share/public';
|
|||
|
||||
import { once } from 'lodash';
|
||||
|
||||
import type { CloudStart } from '../../cloud/public';
|
||||
|
||||
import type { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
|
||||
|
||||
import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '../../../../src/core/public';
|
||||
|
@ -94,12 +96,13 @@ export interface FleetStartDeps {
|
|||
navigation: NavigationPublicPluginStart;
|
||||
customIntegrations: CustomIntegrationsStart;
|
||||
share: SharePluginStart;
|
||||
cloud?: CloudStart;
|
||||
}
|
||||
|
||||
export interface FleetStartServices extends CoreStart, FleetStartDeps {
|
||||
export interface FleetStartServices extends CoreStart, Exclude<FleetStartDeps, 'cloud'> {
|
||||
storage: Storage;
|
||||
share: SharePluginStart;
|
||||
cloud?: CloudSetup;
|
||||
cloud?: CloudSetup & CloudStart;
|
||||
authz: FleetAuthz;
|
||||
}
|
||||
|
||||
|
@ -141,11 +144,16 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
|
|||
euiIconType: 'logoElastic',
|
||||
mount: async (params: AppMountParameters) => {
|
||||
const [coreStartServices, startDepsServices, fleetStart] = await core.getStartServices();
|
||||
const cloud =
|
||||
deps.cloud && startDepsServices.cloud
|
||||
? { ...deps.cloud, ...startDepsServices.cloud }
|
||||
: undefined;
|
||||
|
||||
const startServices: FleetStartServices = {
|
||||
...coreStartServices,
|
||||
...startDepsServices,
|
||||
storage: this.storage,
|
||||
cloud: deps.cloud,
|
||||
cloud,
|
||||
authz: await fleetStart.authz,
|
||||
};
|
||||
const { renderApp, teardownIntegrations } = await import('./applications/integrations');
|
||||
|
@ -178,11 +186,15 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
|
|||
appRoute: '/app/fleet',
|
||||
mount: async (params: AppMountParameters) => {
|
||||
const [coreStartServices, startDepsServices, fleetStart] = await core.getStartServices();
|
||||
const cloud =
|
||||
deps.cloud && startDepsServices.cloud
|
||||
? { ...deps.cloud, ...startDepsServices.cloud }
|
||||
: undefined;
|
||||
const startServices: FleetStartServices = {
|
||||
...coreStartServices,
|
||||
...startDepsServices,
|
||||
storage: this.storage,
|
||||
cloud: deps.cloud,
|
||||
cloud,
|
||||
authz: await fleetStart.authz,
|
||||
};
|
||||
const { renderApp, teardownFleet } = await import('./applications/fleet');
|
||||
|
|
|
@ -14,6 +14,10 @@ const FULLSTORY_ORG_ID = process.env.FULLSTORY_ORG_ID;
|
|||
const FULLSTORY_API_KEY = process.env.FULLSTORY_API_KEY;
|
||||
const RUN_FULLSTORY_TESTS = Boolean(FULLSTORY_ORG_ID && FULLSTORY_API_KEY);
|
||||
|
||||
const CHAT_URL = process.env.CHAT_URL;
|
||||
const CHAT_IDENTITY_SECRET = process.env.CHAT_IDENTITY_SECRET;
|
||||
const RUN_CHAT_TESTS = Boolean(CHAT_URL);
|
||||
|
||||
// the default export of config files must be a config provider
|
||||
// that returns an object with the projects config values
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
|
@ -29,7 +33,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
const samlIdPPlugin = resolve(__dirname, './fixtures/saml/saml_provider');
|
||||
|
||||
return {
|
||||
testFiles: [...(RUN_FULLSTORY_TESTS ? [resolve(__dirname, './tests/fullstory')] : [])],
|
||||
testFiles: [
|
||||
...(RUN_FULLSTORY_TESTS ? [resolve(__dirname, './tests/fullstory')] : []),
|
||||
...(RUN_CHAT_TESTS ? [resolve(__dirname, './tests/chat')] : []),
|
||||
...(!RUN_CHAT_TESTS ? [resolve(__dirname, './tests/chat_disabled')] : []),
|
||||
],
|
||||
|
||||
services,
|
||||
pageObjects,
|
||||
|
@ -69,6 +77,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
`--xpack.cloud.full_story.org_id=${FULLSTORY_ORG_ID}`,
|
||||
]
|
||||
: []),
|
||||
...(RUN_CHAT_TESTS
|
||||
? [
|
||||
'--xpack.cloud.id=5b2de169-2785-441b-ae8c-186a1936b17d',
|
||||
'--xpack.cloud.chat.enabled=true',
|
||||
`--xpack.cloud.chat.chatURL=${CHAT_URL}`,
|
||||
`--xpack.cloud.chatIdentitySecret=${CHAT_IDENTITY_SECRET}`,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
uiSettings: {
|
||||
|
|
30
x-pack/test/cloud_integration/tests/chat.ts
Normal file
30
x-pack/test/cloud_integration/tests/chat.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const find = getService('find');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
|
||||
describe('Cloud Chat integration', function () {
|
||||
before(async () => {
|
||||
// Create role mapping so user gets superuser access
|
||||
await getService('esSupertest')
|
||||
.post('/_security/role_mapping/saml1')
|
||||
.send({ roles: ['superuser'], enabled: true, rules: { field: { 'realm.name': 'saml1' } } })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('chat widget is present when enabled', async () => {
|
||||
PageObjects.common.navigateToUrl('integrations', 'browse', { useActualUrl: true });
|
||||
const chat = await find.byCssSelector('[data-test-subj="floatingChatTrigger"]', 20000);
|
||||
expect(chat).to.not.be(null);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue