Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .buildkite/scripts/steps/storybooks/build_and_upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const STORYBOOKS = [
'apm',
'canvas',
'ci_composite',
'cloud',
'codeeditor',
'custom_integrations',
'dashboard_enhanced',
Expand Down
1 change: 1 addition & 0 deletions src/dev/storybook/aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
33 changes: 33 additions & 0 deletions x-pack/plugins/cloud/.storybook/decorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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',
userID: 'user-id',
userEmail: '[email protected]',
// this doesn't affect chat appearance,
// but a user identity in Drift only
identityJWT: 'identity-jwt',
},
};

export const getEngagementContextDecorator: DecoratorFn = (storyFn) => {
const EngagementProvider = getEngagementContextProvider();
return <EngagementProvider>{storyFn()}</EngagementProvider>;
};

export const getEngagementContextProvider: () => React.FC =
() =>
({ children }) =>
<ServicesProvider {...services}>{children}</ServicesProvider>;
8 changes: 8 additions & 0 deletions x-pack/plugins/cloud/.storybook/index.ts
Original file line number Diff line number Diff line change
@@ -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 { getEngagementContextDecorator, getEngagementContextProvider } from './decorator';
8 changes: 8 additions & 0 deletions x-pack/plugins/cloud/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -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 { defaultConfig } from '@kbn/storybook';
20 changes: 20 additions & 0 deletions x-pack/plugins/cloud/.storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { addons } from '@storybook/addons';
import { create } from '@storybook/theming';
import { PANEL_ID } from '@storybook/addon-actions';

addons.setConfig({
theme: create({
base: 'light',
brandTitle: 'Cloud Storybook',
brandUrl: 'https://github.com/elastic/kibana/tree/main/x-pack/plugins/cloud',
}),
showPanel: true.valueOf,
selectedPanel: PANEL_ID,
});
11 changes: 11 additions & 0 deletions x-pack/plugins/cloud/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -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 { getEngagementContextDecorator } from './decorator';

addDecorator(getEngagementContextDecorator);
1 change: 1 addition & 0 deletions x-pack/plugins/cloud/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions x-pack/plugins/cloud/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export interface GetChatUserDataResponseBody {
token: string;
email: string;
id: string;
}
20 changes: 20 additions & 0 deletions x-pack/plugins/cloud/public/components/chat/iframe.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 './iframe';

export default {
title: 'IFRAME Chat',
description: '',
parameters: {},
};

export const Component = () => {
return <Chat />;
};
135 changes: 135 additions & 0 deletions x-pack/plugins/cloud/public/components/chat/iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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';

type UseChatType =
| { enabled: false }
| {
enabled: true;
src: string;
ref: React.MutableRefObject<HTMLIFrameElement | null>;
style: CSSProperties;
};

const MESSAGE_READY = 'driftIframeReady';
const MESSAGE_RESIZE = 'driftIframeResize';
const MESSAGE_SET_CONTEXT = 'driftSetContext';

const iframeStyle = css`
position: fixed;
botton: 30px;
right: 30px;
display: block;
`;

// We're sending a lot of information to the frame, so this method puts together the specific
// properties, 1/ to avoid leaking too much, and 2/ to enumerate precisely what we're sending.
const getContext = () => {
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,
},
};
};

const useFrame = (): UseChatType => {
const ref = useRef<HTMLIFrameElement>(null);
const chat = useChat();
const [style, setStyle] = useState<CSSProperties>({});

useEffect(() => {
const handleMessage = (event: MessageEvent): void => {
const { current: chatIframe } = ref;

if (
!chat.enabled ||
!chatIframe?.contentWindow ||
event.source !== chatIframe?.contentWindow
) {
return;
}

const { data: message } = event;
const context = getContext();

switch (message.type) {
case MESSAGE_READY: {
const user = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should add a layer of abstraction between a user / chat and the actual chat provider used, so that we have a user with id and email, and then a Drift user with jwt. Could be an overkill at this point, but just thinking out loud.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Believe me, that's the goal, (see #123339 )

id: chat.userID,
attributes: {
email: chat.userEmail,
},
jwt: chat.identityJWT,
};

chatIframe.contentWindow.postMessage(
{
type: MESSAGE_SET_CONTEXT,
data: { context, user },
},
'*'
);
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 };
}

return { enabled: false };
};

export const Chat = () => {
const frameProps = useFrame();

if (!frameProps.enabled) {
return null;
}

return <iframe css={iframeStyle} data-test-id="iframe-chat" title="engagement" {...frameProps} />;
};
16 changes: 16 additions & 0 deletions x-pack/plugins/cloud/public/components/chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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.
*/

/* eslint-disable import/no-default-export */

import { Chat } from './iframe';
export { Chat } from './iframe';

/**
* Exporting the Chat component as a default export so it can be loaded by React.lazy.
*/
export default Chat;
18 changes: 18 additions & 0 deletions x-pack/plugins/cloud/public/components/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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';

export const LazyChat = React.lazy(() => import('./chat'));
export const Chat = () => (
<EuiErrorBoundary>
<Suspense fallback={<div />}>
<LazyChat />
</Suspense>
</EuiErrorBoundary>
);
5 changes: 4 additions & 1 deletion x-pack/plugins/cloud/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import { PluginInitializerContext } from '../../../../src/core/public';
import { CloudPlugin } from './plugin';

export type { CloudSetup, CloudConfigType } from './plugin';
export type { CloudSetup, CloudConfigType, CloudStart } from './plugin';

export function plugin(initializerContext: PluginInitializerContext) {
return new CloudPlugin(initializerContext);
}

export { Chat } from './components';
22 changes: 0 additions & 22 deletions x-pack/plugins/cloud/public/mocks.ts

This file was deleted.

47 changes: 47 additions & 0 deletions x-pack/plugins/cloud/public/mocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { CloudStart } from '.';
import { ServicesProvider } from '../public/services';

function createSetupMock() {
return {
cloudId: 'mock-cloud-id',
isCloudEnabled: true,
cname: 'cname',
baseUrl: 'base-url',
deploymentUrl: 'deployment-url',
profileUrl: 'profile-url',
organizationUrl: 'organization-url',
};
}

const config = {
chat: {
enabled: true,
chatURL: 'chat-url',
userID: 'user-id',
userEmail: '[email protected]',
identityJWT: 'identity-jwt',
},
};

const getContextProvider: () => React.FC =
() =>
({ children }) =>
<ServicesProvider {...config}>{children}</ServicesProvider>;

const createStartMock = (): jest.Mocked<CloudStart> => ({
CloudContextProvider: jest.fn(getContextProvider()),
});

export const cloudMock = {
createSetup: createSetupMock,
createStart: createStartMock,
};
Loading