diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index 328c769d81c66..0af75e72de78a 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -15,6 +15,7 @@ const STORYBOOKS = [ 'apm', 'canvas', 'ci_composite', + 'cloud', 'codeeditor', 'custom_integrations', 'dashboard_enhanced', diff --git a/api_docs/cloud.devdocs.json b/api_docs/cloud.devdocs.json index 6c6dbf6f48003..ce0ded5fa875b 100644 --- a/api_docs/cloud.devdocs.json +++ b/api_docs/cloud.devdocs.json @@ -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 } ], diff --git a/package.json b/package.json index 501204174fb15..a9b4023adeb00 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 896ed6b0bb536..d6526781e7373 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -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', diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts index abeb14de4c43b..ed6497573a362 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts @@ -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'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts index 9f5102b336eda..79230a99a3822 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts @@ -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'; diff --git a/x-pack/plugins/cloud/.storybook/decorator.tsx b/x-pack/plugins/cloud/.storybook/decorator.tsx new file mode 100644 index 0000000000000..4489b58f75759 --- /dev/null +++ b/x-pack/plugins/cloud/.storybook/decorator.tsx @@ -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 }) => + {children}; + +export const getCloudContextDecorator: DecoratorFn = (storyFn) => { + const CloudContextProvider = getCloudContextProvider(); + return {storyFn()}; +}; diff --git a/x-pack/plugins/cloud/.storybook/index.ts b/x-pack/plugins/cloud/.storybook/index.ts new file mode 100644 index 0000000000000..321df983cb20d --- /dev/null +++ b/x-pack/plugins/cloud/.storybook/index.ts @@ -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'; diff --git a/x-pack/plugins/cloud/.storybook/main.ts b/x-pack/plugins/cloud/.storybook/main.ts new file mode 100644 index 0000000000000..bf63e08d64c32 --- /dev/null +++ b/x-pack/plugins/cloud/.storybook/main.ts @@ -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; diff --git a/x-pack/plugins/cloud/.storybook/manager.ts b/x-pack/plugins/cloud/.storybook/manager.ts new file mode 100644 index 0000000000000..54c3d31c2002f --- /dev/null +++ b/x-pack/plugins/cloud/.storybook/manager.ts @@ -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, +}); diff --git a/x-pack/plugins/cloud/.storybook/preview.ts b/x-pack/plugins/cloud/.storybook/preview.ts new file mode 100644 index 0000000000000..83c512e516d5a --- /dev/null +++ b/x-pack/plugins/cloud/.storybook/preview.ts @@ -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); diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts index fc37906299d14..09333e3773fe9 100644 --- a/x-pack/plugins/cloud/common/constants.ts +++ b/x-pack/plugins/cloud/common/constants.ts @@ -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. diff --git a/x-pack/plugins/cloud/common/types.ts b/x-pack/plugins/cloud/common/types.ts new file mode 100644 index 0000000000000..38ebeaf5f467c --- /dev/null +++ b/x-pack/plugins/cloud/common/types.ts @@ -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; +} diff --git a/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx b/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx new file mode 100644 index 0000000000000..668017e134e75 --- /dev/null +++ b/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx @@ -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 ; +}; diff --git a/x-pack/plugins/cloud/public/components/chat/chat.tsx b/x-pack/plugins/cloud/public/components/chat/chat.tsx new file mode 100644 index 0000000000000..99b53f553e75f --- /dev/null +++ b/x-pack/plugins/cloud/public/components/chat/chat.tsx @@ -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; + style: CSSProperties; + isReady: boolean; + }; + +const MESSAGE_READY = 'driftIframeReady'; +const MESSAGE_RESIZE = 'driftIframeResize'; +const MESSAGE_SET_CONTEXT = 'driftSetContext'; + +const useChatConfig = (): UseChatType => { + const ref = useRef(null); + const chat = useChat(); + const [style, setStyle] = useState({}); + 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