mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [Clean up `cloud_chat` (#194571)](https://github.com/elastic/kibana/pull/194571) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Anton Dosov","email":"anton.dosov@elastic.co"},"sourceCommit":{"committedDate":"2024-10-03T11:37:47Z","message":"Clean up `cloud_chat` (#194571)\n\n## Summary\r\n\r\nClose https://github.com/elastic/kibana-team/issues/1017\r\n\r\nThis PR removes the unused Cloud Chat functionality from Kibana. The\r\nchat was not used for some time. Moreover, we've seen some issues with\r\nit where users saw it when it wasn't expected. Given the absence of\r\nautomated tests and the fact that the feature is no longer needed, we\r\nare removing it to improve the overall maintainability and reliability\r\nof the codebase. This will also decrease the amount of code loaded for\r\ntrial users of Kibana in cloud making the app slightly faster.","sha":"568e40accaaee06a6dadfec2028263889cf7104a","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor"],"title":"Clean up `cloud_chat`","number":194571,"url":"https://github.com/elastic/kibana/pull/194571","mergeCommit":{"message":"Clean up `cloud_chat` (#194571)\n\n## Summary\r\n\r\nClose https://github.com/elastic/kibana-team/issues/1017\r\n\r\nThis PR removes the unused Cloud Chat functionality from Kibana. The\r\nchat was not used for some time. Moreover, we've seen some issues with\r\nit where users saw it when it wasn't expected. Given the absence of\r\nautomated tests and the fact that the feature is no longer needed, we\r\nare removing it to improve the overall maintainability and reliability\r\nof the codebase. This will also decrease the amount of code loaded for\r\ntrial users of Kibana in cloud making the app slightly faster.","sha":"568e40accaaee06a6dadfec2028263889cf7104a"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194571","number":194571,"mergeCommit":{"message":"Clean up `cloud_chat` (#194571)\n\n## Summary\r\n\r\nClose https://github.com/elastic/kibana-team/issues/1017\r\n\r\nThis PR removes the unused Cloud Chat functionality from Kibana. The\r\nchat was not used for some time. Moreover, we've seen some issues with\r\nit where users saw it when it wasn't expected. Given the absence of\r\nautomated tests and the fact that the feature is no longer needed, we\r\nare removing it to improve the overall maintainability and reliability\r\nof the codebase. This will also decrease the amount of code loaded for\r\ntrial users of Kibana in cloud making the app slightly faster.","sha":"568e40accaaee06a6dadfec2028263889cf7104a"}}]}] BACKPORT--> Co-authored-by: Anton Dosov <anton.dosov@elastic.co>
This commit is contained in:
parent
8347dedf0f
commit
023e3ed826
51 changed files with 41 additions and 2044 deletions
|
@ -18,7 +18,6 @@ const STORYBOOKS = [
|
|||
'canvas',
|
||||
'cases',
|
||||
'cell_actions',
|
||||
'cloud_chat',
|
||||
'coloring',
|
||||
'chart_icons',
|
||||
'content_management_examples',
|
||||
|
|
|
@ -487,7 +487,8 @@ The plugin exposes the static DefaultEditorController class to consume.
|
|||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_chat/README.md[cloudChat]
|
||||
|Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud.
|
||||
|The plugin was meant to integrate with DriftChat in order to provide live support to our Elastic Cloud users.
|
||||
It was removed, but the plugin was left behind to register no longer used config keys.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_data_migration/README.md[cloudDataMigration]
|
||||
|
|
|
@ -11,7 +11,6 @@ pageLoadAssetSize:
|
|||
cases: 180037
|
||||
charts: 55000
|
||||
cloud: 21076
|
||||
cloudChat: 19894
|
||||
cloudDataMigration: 19170
|
||||
cloudDefend: 18697
|
||||
cloudExperiments: 109746
|
||||
|
|
|
@ -16,7 +16,6 @@ export const storybookAliases = {
|
|||
canvas: 'x-pack/plugins/canvas/storybook',
|
||||
cases: 'packages/kbn-cases-components/.storybook',
|
||||
cell_actions: 'packages/kbn-cell-actions/.storybook',
|
||||
cloud_chat: 'x-pack/plugins/cloud_integrations/cloud_chat/.storybook',
|
||||
cloud: 'packages/cloud/.storybook',
|
||||
coloring: 'packages/kbn-coloring/.storybook',
|
||||
language_documentation_popover: 'packages/kbn-language-documentation/.storybook',
|
||||
|
|
|
@ -59,8 +59,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
// We want to test when the banner is shown
|
||||
'--telemetry.banner=true',
|
||||
// explicitly enable the cloud integration plugins to validate the rendered config keys
|
||||
'--xpack.cloud_integrations.chat.enabled=true',
|
||||
'--xpack.cloud_integrations.chat.chatURL=a_string',
|
||||
'--xpack.cloud_integrations.experiments.enabled=true',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.sdk_key=a_string',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string',
|
||||
|
|
|
@ -233,8 +233,6 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cloud.deployments_url (string?)',
|
||||
'xpack.cloud.is_elastic_staff_owned (boolean?)',
|
||||
'xpack.cloud.trial_end_date (string?)',
|
||||
'xpack.cloud_integrations.chat.chatURL (string?)',
|
||||
'xpack.cloud_integrations.chat.trialBuffer (number?)',
|
||||
// Commented because it's inside a schema conditional, and the test is not able to resolve it. But it's shared.
|
||||
// Added here for documentation purposes.
|
||||
// 'xpack.cloud_integrations.experiments.launch_darkly.client_id (string)',
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"xpack.canvas": "plugins/canvas",
|
||||
"xpack.cases": "plugins/cases",
|
||||
"xpack.cloud": "plugins/cloud",
|
||||
"xpack.cloudChat": "plugins/cloud_integrations/cloud_chat",
|
||||
"xpack.cloudDefend": "plugins/cloud_defend",
|
||||
"xpack.cloudLinks": "plugins/cloud_integrations/cloud_links",
|
||||
"xpack.cloudDataMigration": "plugins/cloud_integrations/cloud_data_migration",
|
||||
|
|
|
@ -1,38 +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.
|
||||
*/
|
||||
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
import { DecoratorFn } from '@storybook/react';
|
||||
import { ServicesProvider, CloudChatServices } from '../public/services';
|
||||
|
||||
// TODO: move to a storybook implementation of the service using parameters.
|
||||
const services: CloudChatServices = {
|
||||
chat: {
|
||||
chatURL: 'https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html',
|
||||
chatVariant: 'bubble',
|
||||
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',
|
||||
trialEndDate: new Date(),
|
||||
kbnVersion: '8.9.0',
|
||||
kbnBuildNum: 12345,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getCloudContextProvider: () => FC<PropsWithChildren<unknown>> =
|
||||
() =>
|
||||
({ children }) =>
|
||||
<ServicesProvider {...services}>{children}</ServicesProvider>;
|
||||
|
||||
export const getCloudContextDecorator: DecoratorFn = (storyFn) => {
|
||||
const CloudContextProvider = getCloudContextProvider();
|
||||
return <CloudContextProvider>{storyFn()}</CloudContextProvider>;
|
||||
};
|
|
@ -1,8 +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.
|
||||
*/
|
||||
|
||||
export { getCloudContextDecorator, getCloudContextProvider } from './decorator';
|
|
@ -1,10 +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.
|
||||
*/
|
||||
|
||||
import { defaultConfig } from '@kbn/storybook';
|
||||
|
||||
module.exports = defaultConfig;
|
|
@ -1,20 +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.
|
||||
*/
|
||||
|
||||
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,
|
||||
});
|
|
@ -1,11 +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.
|
||||
*/
|
||||
|
||||
import { addDecorator } from '@storybook/react';
|
||||
import { getCloudContextDecorator } from './decorator';
|
||||
|
||||
addDecorator(getCloudContextDecorator);
|
|
@ -1,3 +1,4 @@
|
|||
# Cloud Chat
|
||||
# Cloud Chat - Deprecated / Removed
|
||||
|
||||
Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud.
|
||||
The plugin was meant to integrate with DriftChat in order to provide live support to our Elastic Cloud users.
|
||||
It was removed, but the plugin was left behind to register no longer used config keys.
|
||||
|
|
|
@ -1,9 +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.
|
||||
*/
|
||||
|
||||
export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user';
|
||||
export const DEFAULT_TRIAL_BUFFER = 90;
|
|
@ -1,15 +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.
|
||||
*/
|
||||
|
||||
export type ChatVariant = 'header' | 'bubble';
|
||||
|
||||
export interface GetChatUserDataResponseBody {
|
||||
token: string;
|
||||
email: string;
|
||||
id: string;
|
||||
chatVariant: ChatVariant;
|
||||
}
|
|
@ -1,19 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns true if today's date is within the an end date + buffer, false otherwise.
|
||||
*
|
||||
* @param endDate The end date of the trial.
|
||||
* @param buffer The number of days to add to the end date.
|
||||
* @returns true if today's date is within the an end date + buffer, false otherwise.
|
||||
*/
|
||||
export const isTodayInDateWindow = (endDate: Date, buffer: number) => {
|
||||
const endDateWithBuffer = new Date(endDate);
|
||||
endDateWithBuffer.setDate(endDateWithBuffer.getDate() + buffer);
|
||||
return endDateWithBuffer > new Date();
|
||||
};
|
|
@ -1,18 +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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../',
|
||||
roots: ['<rootDir>/x-pack/plugins/cloud_integrations/cloud_chat'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_chat',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/cloud_integrations/cloud_chat/{common,public,server}/**/*.{ts,tsx}',
|
||||
],
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
"plugin": {
|
||||
"id": "cloudChat",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"browser": false,
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"cloud_integrations",
|
||||
|
|
|
@ -1,70 +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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { WhenIdle } from './when_idle';
|
||||
import { useChatConfig, ChatApi } from './use_chat_config';
|
||||
export type { ChatApi } from './use_chat_config';
|
||||
|
||||
export interface Props {
|
||||
/** Handler invoked when the chat widget signals it is ready. */
|
||||
onReady?: (chatApi: ChatApi) => void;
|
||||
/** Handler invoked when the chat widget signals to be resized. */
|
||||
onResize?: () => void;
|
||||
/** Handler invoked when the playbook is fired. */
|
||||
onPlaybookFired?: () => void;
|
||||
/** The offset from the top of the page to the chat widget. */
|
||||
topOffset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 const Chat = ({ onReady, onResize, onPlaybookFired, topOffset = 0 }: Props) => {
|
||||
const config = useChatConfig({
|
||||
onReady,
|
||||
onResize,
|
||||
onPlaybookFired,
|
||||
});
|
||||
|
||||
if (!config.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WhenIdle>
|
||||
<iframe
|
||||
data-test-subj="cloud-chat-frame"
|
||||
title={i18n.translate('xpack.cloudChat.chatFrameTitle', {
|
||||
defaultMessage: 'Chat',
|
||||
})}
|
||||
src={config.src}
|
||||
ref={config.ref}
|
||||
style={
|
||||
config.isReady
|
||||
? {
|
||||
position: 'fixed',
|
||||
...config.style,
|
||||
// reset default button positioning
|
||||
bottom: 'auto',
|
||||
inset: 'initial',
|
||||
// force position to the top and of the page
|
||||
top: topOffset,
|
||||
right: 0,
|
||||
// TODO: if the page height is smaller than widget height + topOffset,
|
||||
// the widget will be cut off from the bottom.
|
||||
// @ts-ignore - fixes white background on iframe in chrome/system dark mode
|
||||
colorScheme: 'light',
|
||||
}
|
||||
: { display: 'none' }
|
||||
}
|
||||
/>
|
||||
</WhenIdle>
|
||||
);
|
||||
};
|
|
@ -1,65 +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.
|
||||
*/
|
||||
|
||||
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('retrieve 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,36 +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.
|
||||
*/
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,28 +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.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import type { Props } from './chat';
|
||||
export type { ChatApi, Props } from './chat';
|
||||
|
||||
/**
|
||||
* 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 = (props: Props) => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<></>}>
|
||||
<LazyChat {...props} />
|
||||
</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
);
|
|
@ -1,213 +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.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState, CSSProperties } from 'react';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { useChat } from '../../services';
|
||||
import { getChatContext } from './get_chat_context';
|
||||
import { Props as ChatProps } from './chat';
|
||||
|
||||
type UseChatType =
|
||||
| { enabled: false }
|
||||
| {
|
||||
enabled: true;
|
||||
src: string;
|
||||
ref: React.MutableRefObject<HTMLIFrameElement | null>;
|
||||
style: CSSProperties;
|
||||
isReady: boolean;
|
||||
isResized: boolean;
|
||||
};
|
||||
|
||||
export interface ChatApi {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const MESSAGE_WIDGET_READY = 'driftWidgetReady';
|
||||
const MESSAGE_IFRAME_READY = 'driftIframeReady';
|
||||
const MESSAGE_RESIZE = 'driftIframeResize';
|
||||
const MESSAGE_SET_CONTEXT = 'driftSetContext';
|
||||
const MESSAGE_CHAT_CLOSED = 'driftChatClosed';
|
||||
const MESSAGE_PLAYBOOK_FIRED = 'driftPlaybookFired';
|
||||
|
||||
type ChatConfigParams = Exclude<ChatProps, 'onHide'> & {
|
||||
/** if the chat visibility is controlled from the outside */
|
||||
controlled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook which handles positioning and communication with the chat widget.
|
||||
*/
|
||||
export const useChatConfig = ({
|
||||
onReady = () => {},
|
||||
onResize = () => {},
|
||||
onPlaybookFired = () => {},
|
||||
controlled = true,
|
||||
}: ChatConfigParams): UseChatType => {
|
||||
const ref = useRef<HTMLIFrameElement>(null);
|
||||
const chat = useChat();
|
||||
const [style, setStyle] = useState<CSSProperties>({ height: 0, width: 0 });
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [isResized, setIsResized] = useState(false);
|
||||
const isChatOpenRef = useRef<boolean>(false);
|
||||
const [hasPlaybookFiredOnce, setPlaybookFiredOnce] = useLocalStorage(
|
||||
'cloudChatPlaybookFiredOnce',
|
||||
false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent): void => {
|
||||
const { current: chatIframe } = ref;
|
||||
|
||||
if (!chat || !chatIframe?.contentWindow || event.source !== chatIframe?.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getChatContext();
|
||||
const { data: message } = event;
|
||||
const { user: userConfig, chatVariant } = chat;
|
||||
const { id, email, jwt, trialEndDate, kbnVersion, kbnBuildNum } = userConfig;
|
||||
|
||||
const chatApi: ChatApi = {
|
||||
show: () => {
|
||||
ref.current?.contentWindow?.postMessage(
|
||||
{
|
||||
type: `driftShow`,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
ref.current?.contentWindow?.postMessage(
|
||||
{
|
||||
type: `driftOpenChat`,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
isChatOpenRef.current = true;
|
||||
},
|
||||
hide: () => {
|
||||
ref.current?.contentWindow?.postMessage(
|
||||
{
|
||||
type: `driftHide`,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
isChatOpenRef.current = false;
|
||||
},
|
||||
toggle: () => {
|
||||
if (isChatOpenRef.current) {
|
||||
chatApi.hide();
|
||||
} else {
|
||||
chatApi.show();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
switch (message.type) {
|
||||
// The IFRAME is ready to receive messages.
|
||||
case MESSAGE_IFRAME_READY: {
|
||||
const user = {
|
||||
id,
|
||||
attributes: {
|
||||
email,
|
||||
trial_end_date: trialEndDate,
|
||||
kbn_version: kbnVersion,
|
||||
kbn_build_num: kbnBuildNum,
|
||||
kbn_chat_variant: chatVariant,
|
||||
},
|
||||
jwt,
|
||||
};
|
||||
|
||||
chatIframe.contentWindow.postMessage(
|
||||
{
|
||||
type: MESSAGE_SET_CONTEXT,
|
||||
data: { context, user },
|
||||
},
|
||||
'*'
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Drift is attempting to resize the IFRAME based on interactions with
|
||||
// its interface.
|
||||
case MESSAGE_RESIZE: {
|
||||
const styles = message.data.styles || ({} as CSSProperties);
|
||||
// camelize to avoid style warnings from react
|
||||
const camelize = (s: string) => s.replace(/-./g, (x) => x[1].toUpperCase());
|
||||
const camelStyles = Object.keys(styles).reduce((acc, key) => {
|
||||
acc[camelize(key)] = styles[key];
|
||||
return acc;
|
||||
}, {} as Record<string, string>) as CSSProperties;
|
||||
setStyle({ ...style, ...camelStyles });
|
||||
|
||||
if (!isResized) {
|
||||
setIsResized(true);
|
||||
}
|
||||
|
||||
onResize();
|
||||
break;
|
||||
}
|
||||
|
||||
// The chat widget is ready.
|
||||
case MESSAGE_WIDGET_READY:
|
||||
if (controlled) chatApi.hide();
|
||||
|
||||
setIsReady(true);
|
||||
onReady(chatApi);
|
||||
|
||||
if (hasPlaybookFiredOnce) {
|
||||
// The `MESSAGE_PLAYBOOK_FIRED` event is only fired until the interaction,
|
||||
// so we need to manually trigger the callback if the event has already fired.
|
||||
// otherwise, users might have an ongoing conversion, but they can't get back to it
|
||||
onPlaybookFired();
|
||||
}
|
||||
break;
|
||||
|
||||
case MESSAGE_CHAT_CLOSED:
|
||||
if (controlled) chatApi.hide();
|
||||
break;
|
||||
|
||||
case MESSAGE_PLAYBOOK_FIRED:
|
||||
onPlaybookFired();
|
||||
setPlaybookFiredOnce(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [
|
||||
chat,
|
||||
style,
|
||||
onReady,
|
||||
onResize,
|
||||
isReady,
|
||||
isResized,
|
||||
controlled,
|
||||
hasPlaybookFiredOnce,
|
||||
onPlaybookFired,
|
||||
setPlaybookFiredOnce,
|
||||
]);
|
||||
|
||||
if (chat) {
|
||||
return {
|
||||
enabled: true,
|
||||
src: chat.chatURL,
|
||||
ref,
|
||||
style,
|
||||
isReady,
|
||||
isResized,
|
||||
};
|
||||
}
|
||||
|
||||
return { enabled: false };
|
||||
};
|
|
@ -1,34 +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.
|
||||
*/
|
||||
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
|
||||
export function whenIdle(doWork: () => void) {
|
||||
const requestIdleCallback = window.requestIdleCallback || window.setTimeout;
|
||||
if (document.readyState === 'complete') {
|
||||
requestIdleCallback(() => doWork());
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
requestIdleCallback(() => doWork());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Postpone rendering of children until the page is loaded and browser is idle.
|
||||
*/
|
||||
export const WhenIdle: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const [idleFired, setIdleFired] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
whenIdle(() => {
|
||||
setIdleFired(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return idleFired ? <>{children}</> : null;
|
||||
};
|
|
@ -1,40 +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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import type { ChatVariant } from '../../../common/types';
|
||||
import { ChatHeaderMenuItem } from '../chat_header_menu_item';
|
||||
import { Chat as ChatFloatingBubble } from '../chat_floating_bubble';
|
||||
|
||||
const BUBBLE_ALLOWED_LOCATIONS = [
|
||||
'app/home#/getting_started',
|
||||
'app/enterprise_search/overview',
|
||||
'app/observability/overview',
|
||||
'app/security/get_started',
|
||||
'app/integrations',
|
||||
];
|
||||
|
||||
export function ChatExperimentSwitcher(props: {
|
||||
location$: Observable<string>;
|
||||
variant: ChatVariant;
|
||||
}) {
|
||||
const location = useObservable(props.location$);
|
||||
if (!location) return null;
|
||||
|
||||
if (props.variant === 'bubble') {
|
||||
if (BUBBLE_ALLOWED_LOCATIONS.some((loc) => location.includes(loc))) {
|
||||
return <ChatFloatingBubble />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return <ChatHeaderMenuItem />;
|
||||
}
|
||||
}
|
|
@ -1,123 +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.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiGlobalToastList,
|
||||
EuiGlobalToastListProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { forceReRender } from '@storybook/react';
|
||||
|
||||
import { Chat } from './chat_floating_bubble';
|
||||
import { ServicesProvider } from '../../services';
|
||||
|
||||
export default {
|
||||
title: 'Chat Widget',
|
||||
description:
|
||||
'A Chat widget, enabled in Cloud, that allows a person to talk to Elastic about their deployment',
|
||||
};
|
||||
|
||||
const Toaster = () => {
|
||||
const [toasts, setToasts] = useState<EuiGlobalToastListProps['toasts']>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
setToasts([
|
||||
{
|
||||
id: 'toast',
|
||||
title: 'Download complete!',
|
||||
color: 'success',
|
||||
text: <p>Thanks for your patience!</p>,
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
Show Toast
|
||||
</EuiButton>
|
||||
<EuiGlobalToastList
|
||||
toasts={toasts}
|
||||
toastLifeTimeMs={3000}
|
||||
dismissToast={() => {
|
||||
setToasts([]);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface Params {
|
||||
id: string;
|
||||
email: string;
|
||||
chatURL: string;
|
||||
jwt: string;
|
||||
trialEndDate: string;
|
||||
kbnVersion: string;
|
||||
kbnBuildNum: number;
|
||||
}
|
||||
|
||||
export const Component = ({
|
||||
id,
|
||||
email,
|
||||
chatURL,
|
||||
jwt,
|
||||
kbnVersion,
|
||||
trialEndDate,
|
||||
kbnBuildNum,
|
||||
}: Params) => {
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
|
||||
return (
|
||||
<ServicesProvider
|
||||
chat={{
|
||||
chatURL,
|
||||
chatVariant: 'bubble',
|
||||
user: {
|
||||
jwt,
|
||||
id,
|
||||
email,
|
||||
trialEndDate: new Date(trialEndDate),
|
||||
kbnVersion,
|
||||
kbnBuildNum,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<Toaster />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
setIsHidden(false);
|
||||
forceReRender();
|
||||
}}
|
||||
disabled={!isHidden}
|
||||
>
|
||||
Reset
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{isHidden ? null : <Chat onHide={() => setIsHidden(true)} />}
|
||||
</ServicesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Component.args = {
|
||||
id: '1234567890',
|
||||
email: 'email.address@elastic.co',
|
||||
chatURL: 'https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html',
|
||||
jwt: 'abcdefghijklmnopqrstuvwxyz',
|
||||
trialEndDate: new Date().toISOString(),
|
||||
kbnVersion: '8.9.0',
|
||||
kbnBuildNum: 12345,
|
||||
};
|
|
@ -1,101 +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.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { useChatConfig, ChatApi } from '../chat/use_chat_config';
|
||||
export type { ChatApi } from '../chat/use_chat_config';
|
||||
|
||||
export interface Props {
|
||||
/** Handler invoked when chat is hidden by someone. */
|
||||
onHide?: () => void;
|
||||
/** Handler invoked when the chat widget signals it is ready. */
|
||||
onReady?: (chatApi: ChatApi) => void;
|
||||
/** Handler invoked when the chat widget signals to be resized. */
|
||||
onResize?: () => void;
|
||||
/** Handler invoked when the playbook is fired. */
|
||||
onPlaybookFired?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This Chat widget implementation uses the default floating chat bubble in the bottom right corner of the screen.
|
||||
* It automatically appears if the playbook fires and isn't controlled from the outside
|
||||
*/
|
||||
export const Chat = ({ onHide = () => {}, onReady, onResize, onPlaybookFired }: Props) => {
|
||||
const config = useChatConfig({
|
||||
onReady,
|
||||
onResize,
|
||||
onPlaybookFired,
|
||||
controlled: false /* makes this chat appear automatically */,
|
||||
});
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isClosed, setIsClosed] = useState(false);
|
||||
|
||||
if (!config.enabled || isClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isReady, isResized, style } = config;
|
||||
const { right } = style;
|
||||
|
||||
const buttonCSS = css`
|
||||
bottom: ${euiThemeVars.euiSizeXS};
|
||||
position: fixed;
|
||||
right: calc(${right} + ${euiThemeVars.euiSizeXS});
|
||||
visibility: ${isReady && isResized ? 'visible' : 'hidden'};
|
||||
`;
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
css={buttonCSS}
|
||||
data-test-subj="cloud-chat-hide"
|
||||
name="cloudChatHide"
|
||||
onClick={() => {
|
||||
onHide();
|
||||
setIsClosed(true);
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
{i18n.translate('xpack.cloudChat.hideChatButtonLabel', {
|
||||
defaultMessage: 'Hide chat',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const containerCSS = css`
|
||||
bottom: ${euiThemeVars.euiSizeXL};
|
||||
position: fixed;
|
||||
right: ${euiThemeVars.euiSizeXL};
|
||||
z-index: ${euiThemeVars.euiZMaskBelowHeader - 1};
|
||||
&:focus [name='cloudChatHide'],
|
||||
&:hover [name='cloudChatHide'] {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<div css={containerCSS} ref={ref} data-test-subj="cloud-chat">
|
||||
{button}
|
||||
<iframe
|
||||
data-test-subj="cloud-chat-frame"
|
||||
title={i18n.translate('xpack.cloudChat.chatFrameTitle', {
|
||||
defaultMessage: 'Chat',
|
||||
})}
|
||||
src={config.src}
|
||||
ref={config.ref}
|
||||
style={{
|
||||
...config.style,
|
||||
// @ts-ignore - fixes white background on iframe in chrome/system dark mode
|
||||
colorScheme: 'light',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,30 +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.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import type { Props } from './chat_floating_bubble';
|
||||
export type { ChatApi, Props } from './chat_floating_bubble';
|
||||
|
||||
/**
|
||||
* A suspense-compatible version of the Chat component.
|
||||
*/
|
||||
export const LazyChat = React.lazy(() =>
|
||||
import('./chat_floating_bubble').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 = (props: Props) => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<></>}>
|
||||
<LazyChat {...props} />
|
||||
</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
);
|
|
@ -1,109 +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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
useEuiTheme,
|
||||
useIsWithinMinBreakpoint,
|
||||
EuiTourStep,
|
||||
EuiText,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import chatIconDark from './chat_icon_dark.svg';
|
||||
import chatIconLight from './chat_icon_light.svg';
|
||||
import { Chat, ChatApi } from '../chat';
|
||||
|
||||
export function ChatHeaderMenuItem() {
|
||||
const [showChatButton, setChatButtonShow] = React.useState(false);
|
||||
const [chatApi, setChatApi] = React.useState<ChatApi | null>(null);
|
||||
const [showTour, setShowTour] = useLocalStorage('cloudChatTour', true);
|
||||
const { euiTheme, colorMode } = useEuiTheme();
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
// chat top offset is used to properly position the chat widget
|
||||
// it can't be static because of an edge case with banners
|
||||
const [chatTopOffset, setChatTopOffset] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
const chatButton = ref.current;
|
||||
if (!chatButton) return;
|
||||
const chatButtonClientRect = chatButton.getBoundingClientRect();
|
||||
setChatTopOffset(chatButtonClientRect.top + chatButtonClientRect.height - 8);
|
||||
}, [showChatButton]);
|
||||
|
||||
const isLargeScreen = useIsWithinMinBreakpoint('m');
|
||||
if (!isLargeScreen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showChatButton && (
|
||||
<EuiTourStep
|
||||
title={
|
||||
<>
|
||||
<EuiIcon
|
||||
type={colorMode === 'DARK' ? chatIconLight : chatIconDark}
|
||||
size={'l'}
|
||||
css={{ marginRight: euiTheme.size.s }}
|
||||
/>
|
||||
{i18n.translate('xpack.cloudChat.chatTourHeaderText', {
|
||||
defaultMessage: 'Live Chat Now',
|
||||
})}
|
||||
</>
|
||||
}
|
||||
content={
|
||||
<EuiText size={'s'}>
|
||||
<p>
|
||||
{i18n.translate('xpack.cloudChat.chatTourText', {
|
||||
defaultMessage:
|
||||
'Open chat for assistance with topics such as ingesting data, configuring your instance, and troubleshooting.',
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
}
|
||||
isStepOpen={showTour}
|
||||
onFinish={() => setShowTour(false)}
|
||||
minWidth={300}
|
||||
maxWidth={360}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
buttonRef={ref}
|
||||
css={{ color: euiTheme.colors.ghost, marginRight: euiTheme.size.m }}
|
||||
size="s"
|
||||
iconType={chatIconLight}
|
||||
data-test-subj="cloud-chat"
|
||||
onClick={() => {
|
||||
if (showTour) setShowTour(false);
|
||||
chatApi?.toggle();
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.cloudChat.chatButtonLabel', {
|
||||
defaultMessage: 'Live Chat',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiTourStep>
|
||||
)}
|
||||
{ReactDOM.createPortal(
|
||||
<Chat
|
||||
onReady={(_chatApi) => {
|
||||
setChatApi(_chatApi);
|
||||
}}
|
||||
onPlaybookFired={() => {
|
||||
setChatButtonShow(true);
|
||||
}}
|
||||
topOffset={chatTopOffset}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.96672 8.8637C5.29876 9.6512 6.05226 10.2009 6.92862 10.2009H10.6021L13.3572 13V10.2009C14.5407 10.2009 15.5001 9.1984 15.5001 7.9617V4.60283C15.5001 3.36613 14.5407 2.36359 13.3572 2.36359H12.2858V6.69704C12.2858 7.89366 11.3903 8.8637 10.2858 8.8637H4.96672Z" fill="black"/>
|
||||
<path d="M4.90517 6.71774H9.07143C9.66316 6.71774 10.1429 6.21646 10.1429 5.59811V2.23925C10.1429 1.62089 9.66316 1.11962 9.07143 1.11962H2.64286C2.05112 1.11962 1.57143 1.62089 1.57143 2.23925V5.59811C1.57143 6.21646 2.05112 6.71774 2.64286 6.71774H3.71429V8.1696L4.90517 6.71774ZM5.39796 7.83736L2.64286 10.6364V7.83736C1.45939 7.83736 0.5 6.83481 0.5 5.59811V2.23925C0.5 1.00254 1.45939 0 2.64286 0H9.07143C10.2549 0 11.2143 1.00254 11.2143 2.23925V5.59811C11.2143 6.83481 10.2549 7.83736 9.07143 7.83736H5.39796Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 929 B |
|
@ -1,5 +0,0 @@
|
|||
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.96672 8.8637C5.29876 9.6512 6.05226 10.2009 6.92862 10.2009H10.6021L13.3572 13V10.2009C14.5407 10.2009 15.5001 9.1984 15.5001 7.9617V4.60283C15.5001 3.36613 14.5407 2.36359 13.3572 2.36359H12.2858V6.69704C12.2858 7.89366 11.3903 8.8637 10.2858 8.8637H4.96672Z" fill="white"/>
|
||||
<path d="M4.90517 6.71774H9.07143C9.66316 6.71774 10.1429 6.21646 10.1429 5.59811V2.23925C10.1429 1.62089 9.66316 1.11962 9.07143 1.11962H2.64286C2.05112 1.11962 1.57143 1.62089 1.57143 2.23925V5.59811C1.57143 6.21646 2.05112 6.71774 2.64286 6.71774H3.71429V8.1696L4.90517 6.71774ZM5.39796 7.83736L2.64286 10.6364V7.83736C1.45939 7.83736 0.5 6.83481 0.5 5.59811V2.23925C0.5 1.00254 1.45939 0 2.64286 0H9.07143C10.2549 0 11.2143 1.00254 11.2143 2.23925V5.59811C11.2143 6.83481 10.2549 7.83736 9.07143 7.83736H5.39796Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 929 B |
|
@ -1,8 +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.
|
||||
*/
|
||||
|
||||
export { ChatHeaderMenuItem } from './chat_header_menu_items';
|
|
@ -1,9 +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.
|
||||
*/
|
||||
|
||||
export { Chat, type ChatApi, type Props as ChatProps } from './chat';
|
||||
export { ChatHeaderMenuItem } from './chat_header_menu_item';
|
|
@ -1,13 +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.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { CloudChatPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudChatPlugin(initializerContext);
|
||||
}
|
|
@ -1,103 +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.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import type { CloudChatConfigType } from '../server/config';
|
||||
import { CloudChatPlugin } from './plugin';
|
||||
import { type MockedLogger } from '@kbn/logging-mocks';
|
||||
|
||||
describe('Cloud Chat Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
describe('setupChat', () => {
|
||||
let newTrialEndDate: Date;
|
||||
let logger: MockedLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
newTrialEndDate = new Date();
|
||||
newTrialEndDate.setDate(new Date().getDate() + 14);
|
||||
});
|
||||
|
||||
const setupPlugin = async ({
|
||||
config = {},
|
||||
isCloudEnabled = true,
|
||||
failHttp = false,
|
||||
trialEndDate = newTrialEndDate,
|
||||
}: {
|
||||
config?: Partial<CloudChatConfigType>;
|
||||
isCloudEnabled?: boolean;
|
||||
failHttp?: boolean;
|
||||
trialEndDate?: Date;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext(config);
|
||||
logger = initContext.logger as MockedLogger;
|
||||
|
||||
const plugin = new CloudChatPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
if (failHttp) {
|
||||
coreSetup.http.get.mockImplementation(async () => {
|
||||
throw new Error('HTTP request failed');
|
||||
});
|
||||
}
|
||||
|
||||
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
|
||||
|
||||
const cloud = cloudMock.createSetup();
|
||||
|
||||
plugin.setup(coreSetup, {
|
||||
cloud: { ...cloud, isCloudEnabled, trialEndDate },
|
||||
});
|
||||
|
||||
// Wait for the async processes to complete
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
return { initContext, plugin, 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 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: { chatURL: 'http://chat.elastic.co', trialBuffer: 30 },
|
||||
failHttp: true,
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining(`Error setting up Chat`));
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if chat is enabled and url is provided but trial has expired', async () => {
|
||||
const date = new Date();
|
||||
date.setDate(new Date().getDate() - 44);
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chatURL: 'http://chat.elastic.co', trialBuffer: 30 },
|
||||
trialEndDate: date,
|
||||
});
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is retrieved if chat is enabled and url is provided and trial is active', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chatURL: 'http://chat.elastic.co', trialBuffer: 30 },
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,139 +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.
|
||||
*/
|
||||
|
||||
import React, { type FC, type PropsWithChildren } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { ReplaySubject, first } from 'rxjs';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import type { ChatVariant, GetChatUserDataResponseBody } from '../common/types';
|
||||
import { GET_CHAT_USER_DATA_ROUTE_PATH } from '../common/constants';
|
||||
import { ChatConfig, ServicesProvider } from './services';
|
||||
import { isTodayInDateWindow } from '../common/util';
|
||||
import { ChatExperimentSwitcher } from './components/chat_experiment_switcher';
|
||||
|
||||
interface CloudChatSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
}
|
||||
|
||||
interface CloudChatStartDeps {
|
||||
cloud: CloudStart;
|
||||
}
|
||||
|
||||
interface SetupChatDeps extends CloudChatSetupDeps {
|
||||
http: HttpSetup;
|
||||
}
|
||||
|
||||
interface CloudChatConfig {
|
||||
chatURL?: string;
|
||||
trialBuffer: number;
|
||||
}
|
||||
|
||||
export class CloudChatPlugin implements Plugin<void, void, CloudChatSetupDeps, CloudChatStartDeps> {
|
||||
private readonly config: CloudChatConfig;
|
||||
private readonly logger: Logger;
|
||||
private chatConfig$ = new ReplaySubject<ChatConfig>(1);
|
||||
private kbnVersion: string;
|
||||
private kbnBuildNum: number;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext<CloudChatConfig>) {
|
||||
this.kbnVersion = initializerContext.env.packageInfo.version;
|
||||
this.kbnBuildNum = initializerContext.env.packageInfo.buildNum;
|
||||
this.config = initializerContext.config.get();
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, { cloud }: CloudChatSetupDeps) {
|
||||
this.setupChat({ http: core.http, cloud }).catch((e) =>
|
||||
this.logger.debug(`Error setting up Chat: ${e.toString()}`)
|
||||
);
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
const CloudChatContextProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
// 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 chatConfig = useObservable(this.chatConfig$, undefined);
|
||||
return <ServicesProvider chat={chatConfig}>{children}</ServicesProvider>;
|
||||
};
|
||||
function ConnectedChat(props: { chatVariant: ChatVariant }) {
|
||||
return (
|
||||
<CloudChatContextProvider>
|
||||
<KibanaRenderContextProvider {...core}>
|
||||
<ChatExperimentSwitcher
|
||||
location$={core.application.currentLocation$}
|
||||
variant={props.chatVariant}
|
||||
/>
|
||||
</KibanaRenderContextProvider>
|
||||
</CloudChatContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
this.chatConfig$.pipe(first((config) => config != null)).subscribe((config) => {
|
||||
core.chrome.navControls.registerExtension({
|
||||
order: 50,
|
||||
mount: (e) => {
|
||||
ReactDOM.render(<ConnectedChat chatVariant={config.chatVariant} />, e);
|
||||
return () => {
|
||||
ReactDOM.unmountComponentAtNode(e);
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private async setupChat({ cloud, http }: SetupChatDeps) {
|
||||
const { isCloudEnabled, trialEndDate } = cloud;
|
||||
const { chatURL, trialBuffer } = this.config;
|
||||
|
||||
if (
|
||||
!isCloudEnabled ||
|
||||
!chatURL ||
|
||||
!trialEndDate ||
|
||||
!isTodayInDateWindow(trialEndDate, trialBuffer)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
id,
|
||||
token: jwt,
|
||||
chatVariant,
|
||||
} = await http.get<GetChatUserDataResponseBody>(GET_CHAT_USER_DATA_ROUTE_PATH);
|
||||
|
||||
if (!email || !id || !jwt) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatConfig$.next({
|
||||
chatURL,
|
||||
chatVariant,
|
||||
user: {
|
||||
email,
|
||||
id,
|
||||
jwt,
|
||||
trialEndDate: trialEndDate!,
|
||||
kbnVersion: this.kbnVersion,
|
||||
kbnBuildNum: this.kbnBuildNum,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.debug(
|
||||
`[cloud.chat] Could not retrieve chat config: ${e.response.status} ${e.message}`,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +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.
|
||||
*/
|
||||
|
||||
import React, { FC, createContext, useContext, PropsWithChildren } from 'react';
|
||||
import type { ChatVariant } from '../../common/types';
|
||||
|
||||
export interface ChatConfig {
|
||||
chatURL: string;
|
||||
chatVariant: ChatVariant;
|
||||
user: {
|
||||
jwt: string;
|
||||
id: string;
|
||||
email: string;
|
||||
trialEndDate: Date;
|
||||
kbnVersion: string;
|
||||
kbnBuildNum: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CloudChatServices {
|
||||
chat?: ChatConfig;
|
||||
}
|
||||
|
||||
const ServicesContext = createContext<CloudChatServices>({});
|
||||
|
||||
export const ServicesProvider: FC<PropsWithChildren<CloudChatServices>> = ({
|
||||
children,
|
||||
...services
|
||||
}) => <ServicesContext.Provider value={services}>{children}</ServicesContext.Provider>;
|
||||
|
||||
/**
|
||||
* React hook for accessing the pre-wired `CloudChatServices`.
|
||||
*/
|
||||
export function useServices() {
|
||||
return useContext(ServicesContext);
|
||||
}
|
||||
|
||||
export function useChat(): ChatConfig | undefined {
|
||||
const { chat } = useServices();
|
||||
return chat;
|
||||
}
|
|
@ -5,74 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get, has } from 'lodash';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
|
||||
import { DEFAULT_TRIAL_BUFFER } from '../common/constants';
|
||||
|
||||
const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
chatURL: schema.maybe(schema.string()),
|
||||
chatIdentitySecret: schema.maybe(schema.string()),
|
||||
trialBuffer: schema.number({ defaultValue: DEFAULT_TRIAL_BUFFER }),
|
||||
});
|
||||
|
||||
export type CloudChatConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<CloudChatConfigType> = {
|
||||
exposeToBrowser: {
|
||||
chatURL: true,
|
||||
trialBuffer: true,
|
||||
},
|
||||
export const config: PluginConfigDescriptor = {
|
||||
schema: configSchema,
|
||||
deprecations: () => [
|
||||
// Silently move the chat configuration from `xpack.cloud` to `xpack.cloud_integrations.chat`.
|
||||
// No need to emit a deprecation log because it's an internal restructure
|
||||
(cfg) => {
|
||||
return {
|
||||
set: [
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.chat.enabled',
|
||||
toKey: 'xpack.cloud_integrations.chat.enabled',
|
||||
}),
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.chat.chatURL',
|
||||
toKey: 'xpack.cloud_integrations.chat.chatURL',
|
||||
}),
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.chatIdentitySecret',
|
||||
toKey: 'xpack.cloud_integrations.chat.chatIdentitySecret',
|
||||
}),
|
||||
],
|
||||
unset: [
|
||||
{ path: 'xpack.cloud.chat.enabled' },
|
||||
{ path: 'xpack.cloud.chat.chatURL' },
|
||||
{ path: 'xpack.cloud.chatIdentitySecret' },
|
||||
],
|
||||
};
|
||||
},
|
||||
deprecations: ({ unusedFromRoot }) => [
|
||||
// Deprecate the old chat configuration keys
|
||||
unusedFromRoot('xpack.cloud.chat.enabled', { silent: true, level: 'warning' }),
|
||||
unusedFromRoot('xpack.cloud.chat.chatURL', { silent: true, level: 'warning' }),
|
||||
unusedFromRoot('xpack.cloud.chatIdentitySecret', { silent: true, level: 'warning' }),
|
||||
|
||||
// Deprecate the latest chat configuration keys
|
||||
unusedFromRoot('xpack.cloud_integrations.chat.enabled', { silent: true, level: 'warning' }),
|
||||
unusedFromRoot('xpack.cloud_integrations.chat.chatURL', { silent: true, level: 'warning' }),
|
||||
unusedFromRoot('xpack.cloud_integrations.chat.chatIdentitySecret', {
|
||||
silent: true,
|
||||
level: 'warning',
|
||||
}),
|
||||
unusedFromRoot('xpack.cloud_integrations.chat.trialBuffer', {
|
||||
silent: true,
|
||||
level: 'warning',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the `set` action only if the key exists in the `fromKey` value.
|
||||
* This is to avoid overwriting actual values with undefined.
|
||||
* @param cfg The config object
|
||||
* @param fromKey The key to copy from.
|
||||
* @param toKey The key where the value should be copied to.
|
||||
*/
|
||||
function copyIfExists({
|
||||
cfg,
|
||||
fromKey,
|
||||
toKey,
|
||||
}: {
|
||||
cfg: Readonly<{ [p: string]: unknown }>;
|
||||
fromKey: string;
|
||||
toKey: string;
|
||||
}) {
|
||||
return has(cfg, fromKey) ? [{ path: toKey, value: get(cfg, fromKey) }] : [];
|
||||
}
|
||||
|
|
|
@ -5,11 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext } from '@kbn/core/server';
|
||||
import { Plugin } from '@kbn/core-plugins-server';
|
||||
|
||||
export { config } from './config';
|
||||
|
||||
export async function plugin(initializerContext: PluginInitializerContext) {
|
||||
const { CloudChatPlugin } = await import('./plugin');
|
||||
return new CloudChatPlugin(initializerContext);
|
||||
// The plugin exists only to register the deprecated config keys and to be cleaned up in the future
|
||||
export async function plugin() {
|
||||
class CloudChatPlugin implements Plugin {
|
||||
constructor() {}
|
||||
|
||||
public setup() {}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
return new CloudChatPlugin();
|
||||
}
|
||||
|
|
|
@ -1,45 +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.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext, CoreSetup, Plugin } from '@kbn/core/server';
|
||||
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import { registerChatRoute } from './routes';
|
||||
import type { CloudChatConfigType } from './config';
|
||||
|
||||
interface CloudChatSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
}
|
||||
|
||||
export class CloudChatPlugin implements Plugin<void, void, CloudChatSetupDeps> {
|
||||
private readonly config: CloudChatConfigType;
|
||||
private readonly isDev: boolean;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config = initializerContext.config.get();
|
||||
this.isDev = initializerContext.env.mode.dev;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, { cloud }: CloudChatSetupDeps) {
|
||||
const { chatIdentitySecret, trialBuffer } = this.config;
|
||||
const { isCloudEnabled, trialEndDate } = cloud;
|
||||
|
||||
if (isCloudEnabled && chatIdentitySecret) {
|
||||
registerChatRoute({
|
||||
router: core.http.createRouter(),
|
||||
chatIdentitySecret,
|
||||
trialEndDate,
|
||||
trialBuffer,
|
||||
isDev: this.isDev,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -1,369 +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.
|
||||
*/
|
||||
|
||||
jest.mock('jsonwebtoken', () => ({
|
||||
sign: () => {
|
||||
return 'json-web-token';
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
httpServiceMock,
|
||||
httpServerMock,
|
||||
coreMock,
|
||||
securityServiceMock,
|
||||
coreFeatureFlagsMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { kibanaResponseFactory } from '@kbn/core/server';
|
||||
import { type MetaWithSaml, registerChatRoute } from './chat';
|
||||
|
||||
describe('chat route', () => {
|
||||
let security: ReturnType<typeof securityServiceMock.createRequestHandlerContext>;
|
||||
let requestHandlerContextMock: ReturnType<typeof coreMock.createCustomRequestHandlerContext>;
|
||||
let featureFlags: ReturnType<typeof coreFeatureFlagsMock.createRequestHandlerContext>;
|
||||
|
||||
beforeEach(() => {
|
||||
const core = coreMock.createRequestHandlerContext();
|
||||
security = core.security;
|
||||
featureFlags = core.featureFlags;
|
||||
featureFlags.getStringValue.mockResolvedValue('header');
|
||||
featureFlags.getBooleanValue.mockResolvedValue(true);
|
||||
requestHandlerContextMock = coreMock.createCustomRequestHandlerContext({ core });
|
||||
});
|
||||
|
||||
test('error if no user', async () => {
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(null);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
httpServerMock.createKibanaRequest(),
|
||||
kibanaResponseFactory
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {},
|
||||
"payload": "Not Found",
|
||||
"status": 404,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('error if no user is missing any details', async () => {
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(
|
||||
securityServiceMock.createMockAuthenticatedUser({
|
||||
username: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
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('error if no trial end date specified', async () => {
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(
|
||||
securityServiceMock.createMockAuthenticatedUser({
|
||||
username,
|
||||
metadata: {
|
||||
saml_email: [email],
|
||||
} as MetaWithSaml,
|
||||
})
|
||||
);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 2,
|
||||
});
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
httpServerMock.createKibanaRequest(),
|
||||
kibanaResponseFactory
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": "Chat can only be started if a trial end date is specified",
|
||||
},
|
||||
"payload": "Chat can only be started if a trial end date is specified",
|
||||
"status": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('error if not in trial window', async () => {
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(
|
||||
securityServiceMock.createMockAuthenticatedUser({
|
||||
username,
|
||||
metadata: {
|
||||
saml_email: [email],
|
||||
} as MetaWithSaml,
|
||||
})
|
||||
);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
const trialEndDate = new Date();
|
||||
trialEndDate.setDate(trialEndDate.getDate() - 30);
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 2,
|
||||
trialEndDate,
|
||||
});
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
httpServerMock.createKibanaRequest(),
|
||||
kibanaResponseFactory
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": "Chat can only be started during trial and trial chat buffer",
|
||||
},
|
||||
"payload": "Chat can only be started during trial and trial chat buffer",
|
||||
"status": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('error if disabled in experiments', async () => {
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(
|
||||
securityServiceMock.createMockAuthenticatedUser({
|
||||
username,
|
||||
metadata: {
|
||||
saml_email: [email],
|
||||
} as MetaWithSaml,
|
||||
})
|
||||
);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
featureFlags.getBooleanValue.mockResolvedValueOnce(false);
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
httpServerMock.createKibanaRequest(),
|
||||
kibanaResponseFactory
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": "Chat is disabled through experiments",
|
||||
},
|
||||
"payload": "Chat is disabled through experiments",
|
||||
"status": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns user information taken from saml metadata and a token', async () => {
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(
|
||||
securityServiceMock.createMockAuthenticatedUser({
|
||||
username,
|
||||
metadata: {
|
||||
saml_email: [email],
|
||||
} as MetaWithSaml,
|
||||
})
|
||||
);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
httpServerMock.createKibanaRequest(),
|
||||
kibanaResponseFactory
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": Object {
|
||||
"chatVariant": "header",
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
},
|
||||
"payload": Object {
|
||||
"chatVariant": "header",
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
"status": 200,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns placeholder user information and a token in dev mode', async () => {
|
||||
const username = 'first.last';
|
||||
const email = 'test+first.last@elasticsearch.com';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(
|
||||
securityServiceMock.createMockAuthenticatedUser({
|
||||
username: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: true,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
httpServerMock.createKibanaRequest(),
|
||||
kibanaResponseFactory
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": Object {
|
||||
"chatVariant": "header",
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
},
|
||||
"payload": Object {
|
||||
"chatVariant": "header",
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
"status": 200,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns chat variant', async () => {
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce(
|
||||
securityServiceMock.createMockAuthenticatedUser({
|
||||
username,
|
||||
metadata: {
|
||||
saml_email: [email],
|
||||
} as MetaWithSaml,
|
||||
})
|
||||
);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
featureFlags.getStringValue.mockResolvedValueOnce('bubble');
|
||||
registerChatRoute({
|
||||
router,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(
|
||||
handler(
|
||||
requestHandlerContextMock,
|
||||
httpServerMock.createKibanaRequest(),
|
||||
kibanaResponseFactory
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": Object {
|
||||
"chatVariant": "bubble",
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
},
|
||||
"payload": Object {
|
||||
"chatVariant": "bubble",
|
||||
"email": "${email}",
|
||||
"id": "${username}",
|
||||
"token": "json-web-token",
|
||||
},
|
||||
"status": 200,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -1,100 +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.
|
||||
*/
|
||||
|
||||
import type { AuthenticatedUser, IRouter } from '@kbn/core/server';
|
||||
import { GET_CHAT_USER_DATA_ROUTE_PATH } from '../../common/constants';
|
||||
import type { GetChatUserDataResponseBody, ChatVariant } from '../../common/types';
|
||||
import { generateSignedJwt } from '../util/generate_jwt';
|
||||
import { isTodayInDateWindow } from '../../common/util';
|
||||
|
||||
export type MetaWithSaml = AuthenticatedUser['metadata'] & {
|
||||
saml_name: [string];
|
||||
saml_email: [string];
|
||||
saml_roles: [string];
|
||||
saml_principal: [string];
|
||||
};
|
||||
|
||||
export const registerChatRoute = ({
|
||||
router,
|
||||
chatIdentitySecret,
|
||||
trialEndDate,
|
||||
trialBuffer,
|
||||
isDev,
|
||||
}: {
|
||||
router: IRouter;
|
||||
chatIdentitySecret: string;
|
||||
trialEndDate?: Date;
|
||||
trialBuffer: number;
|
||||
isDev: boolean;
|
||||
}) => {
|
||||
router.get(
|
||||
{
|
||||
path: GET_CHAT_USER_DATA_ROUTE_PATH,
|
||||
validate: {},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { security, featureFlags } = await context.core;
|
||||
const user = security.authc.getCurrentUser();
|
||||
|
||||
if (!user) {
|
||||
// Hide the API from unauthenticated users
|
||||
return response.notFound();
|
||||
}
|
||||
|
||||
let userId = user.username;
|
||||
let [userEmail] = (user.metadata as MetaWithSaml)?.saml_email || [];
|
||||
|
||||
// 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',
|
||||
});
|
||||
}
|
||||
|
||||
if (!trialEndDate) {
|
||||
return response.badRequest({
|
||||
body: 'Chat can only be started if a trial end date is specified',
|
||||
});
|
||||
}
|
||||
|
||||
if (!trialEndDate || !isTodayInDateWindow(trialEndDate, trialBuffer)) {
|
||||
return response.badRequest({
|
||||
body: 'Chat can only be started during trial and trial chat buffer',
|
||||
});
|
||||
}
|
||||
|
||||
// Meant to be used as a runtime kill switch via LaunchDarkly
|
||||
if (!(await featureFlags.getBooleanValue('cloud-chat.enabled', true).catch(() => false))) {
|
||||
return response.badRequest({
|
||||
body: 'Chat is disabled through experiments',
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateSignedJwt(userId, chatIdentitySecret);
|
||||
const body: GetChatUserDataResponseBody = {
|
||||
token,
|
||||
email: userEmail,
|
||||
id: userId,
|
||||
chatVariant: await featureFlags.getStringValue<ChatVariant>(
|
||||
'cloud-chat.chat-variant',
|
||||
'header'
|
||||
),
|
||||
};
|
||||
return response.ok({ body });
|
||||
}
|
||||
);
|
||||
};
|
|
@ -1,8 +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.
|
||||
*/
|
||||
|
||||
export { registerChatRoute } from './chat';
|
|
@ -1,23 +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.
|
||||
*/
|
||||
|
||||
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}'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,24 +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.
|
||||
*/
|
||||
|
||||
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);
|
||||
};
|
|
@ -12,15 +12,8 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/storybook",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/i18n",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/logging",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/core-plugins-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -13498,11 +13498,6 @@
|
|||
"xpack.cases.userProfiles.modifySearch": "Modifiez votre recherche ou vérifiez les privilèges de l'utilisateur.",
|
||||
"xpack.cases.userProfiles.userDoesNotExist": "L'utilisateur n'existe pas ou n'est pas disponible",
|
||||
"xpack.cases.visualizationActions.addToExistingCaseSuccessContent": "Visualisation correctement ajoutée au cas",
|
||||
"xpack.cloudChat.chatButtonLabel": "Chat en live",
|
||||
"xpack.cloudChat.chatFrameTitle": "Chat",
|
||||
"xpack.cloudChat.chatTourHeaderText": "Chat en live maintenant",
|
||||
"xpack.cloudChat.chatTourText": "Lancer le chat pour bénéficier d'une aide sur l'ingestion des données, la configuration de votre instance et la résolution de problèmes.",
|
||||
"xpack.cloudChat.hideChatButtonLabel": "Masquer le chat",
|
||||
"xpack.cloudDataMigration.breadcrumb.label": "Migrer vers Elastic Cloud",
|
||||
"xpack.cloudDataMigration.deployInSeconds.text": "Déployer et scaler une suite Elastic en quelques minutes",
|
||||
"xpack.cloudDataMigration.helpMenuMoveDataTitle": "Déplacer les données vers Elastic Cloud",
|
||||
|
|
|
@ -13247,11 +13247,6 @@
|
|||
"xpack.cases.userProfiles.modifySearch": "検索を変更するか、ユーザーの権限を確認してください。",
|
||||
"xpack.cases.userProfiles.userDoesNotExist": "ユーザーが存在しないか、使用できません",
|
||||
"xpack.cases.visualizationActions.addToExistingCaseSuccessContent": "ビジュアライゼーションが正常にケースに追加されました",
|
||||
"xpack.cloudChat.chatButtonLabel": "ライブチャット",
|
||||
"xpack.cloudChat.chatFrameTitle": "チャット",
|
||||
"xpack.cloudChat.chatTourHeaderText": "今すぐライブチャット",
|
||||
"xpack.cloudChat.chatTourText": "データのインジェスト、インスタンスの設定、トラブルシューティングなどのトピックに関するサポートを受けるためのチャットを開きます。",
|
||||
"xpack.cloudChat.hideChatButtonLabel": "グラフを非表示",
|
||||
"xpack.cloudDataMigration.breadcrumb.label": "Elastic Cloudに移行する",
|
||||
"xpack.cloudDataMigration.deployInSeconds.text": "数分で安全なElastic Stackをデプロイしてスケール",
|
||||
"xpack.cloudDataMigration.helpMenuMoveDataTitle": "データをElastic Cloudに移動",
|
||||
|
|
|
@ -13272,11 +13272,6 @@
|
|||
"xpack.cases.userProfiles.modifySearch": "请修改您的搜索或检查用户的权限。",
|
||||
"xpack.cases.userProfiles.userDoesNotExist": "用户不存在或不可用",
|
||||
"xpack.cases.visualizationActions.addToExistingCaseSuccessContent": "已成功将可视化添加到案例",
|
||||
"xpack.cloudChat.chatButtonLabel": "实时聊天",
|
||||
"xpack.cloudChat.chatFrameTitle": "聊天",
|
||||
"xpack.cloudChat.chatTourHeaderText": "立即实时聊天",
|
||||
"xpack.cloudChat.chatTourText": "打开聊天以获取有关主题的帮助,如采集数据、配置实例和故障排除。",
|
||||
"xpack.cloudChat.hideChatButtonLabel": "隐藏聊天",
|
||||
"xpack.cloudDataMigration.breadcrumb.label": "迁移到 Elastic Cloud",
|
||||
"xpack.cloudDataMigration.deployInSeconds.text": "在数分钟内部署并扩展安全的 Elastic Stack",
|
||||
"xpack.cloudDataMigration.helpMenuMoveDataTitle": "转移数据到 Elastic Cloud",
|
||||
|
|
|
@ -14,11 +14,6 @@ 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 CLOUD_TRIAL_END_DATE = new Date().toISOString(); // needed for chat to appear
|
||||
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) {
|
||||
|
@ -34,11 +29,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
const samlIdPPlugin = resolve(__dirname, './plugins/saml_provider');
|
||||
|
||||
return {
|
||||
testFiles: [
|
||||
...(RUN_FULLSTORY_TESTS ? [resolve(__dirname, './tests/fullstory')] : []),
|
||||
...(RUN_CHAT_TESTS ? [resolve(__dirname, './tests/chat')] : []),
|
||||
...(!RUN_CHAT_TESTS ? [resolve(__dirname, './tests/chat_disabled')] : []),
|
||||
],
|
||||
testFiles: [...(RUN_FULLSTORY_TESTS ? [resolve(__dirname, './tests/fullstory')] : [])],
|
||||
|
||||
services,
|
||||
pageObjects,
|
||||
|
@ -78,15 +69,6 @@ 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}`,
|
||||
`--xpack.cloud.trial_end_date="${CLOUD_TRIAL_END_DATE}"`,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
uiSettings: {
|
||||
|
|
|
@ -1,31 +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.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
|
||||
// TODO: the whole suite doesn't run on ci https://github.com/elastic/kibana/issues/159401
|
||||
describe('Cloud Chat integration', function () {
|
||||
it('chat widget is present in header', async () => {
|
||||
await PageObjects.common.navigateToUrl('home');
|
||||
|
||||
// button is visible
|
||||
await testSubjects.existOrFail('cloud-chat');
|
||||
|
||||
// the chat widget is not visible (but in DOM)
|
||||
await testSubjects.missingOrFail('cloud-chat-frame', {
|
||||
allowHidden: true,
|
||||
});
|
||||
|
||||
await testSubjects.click('cloud-chat');
|
||||
await testSubjects.existOrFail('cloud-chat-frame');
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue