Skip to content

Commit

Permalink
wip global drift
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant committed Sep 19, 2023
1 parent ab4fcb1 commit 46cc24b
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 56 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,38 @@ import { i18n } from '@kbn/i18n';
// import { EuiButtonEmpty } from '@elastic/eui';
// import { euiThemeVars } from '@kbn/ui-theme';

import { useChatConfig } from './use_chat_config';
import { useChatConfig, ChatApi } from './use_chat_config';
export type { ChatApi } from './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?: () => void;
onReady?: (chatApi: ChatApi) => void;
/** Handler invoked when the chat widget signals to be resized. */
onResize?: () => void;

onPlaybookFired?: () => void;
}

/**
* 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 = ({ onHide = () => {}, onReady, onResize }: Props) => {
const config = useChatConfig({ onReady, onResize });
export const Chat = ({ onHide = () => {}, onReady, onResize, onPlaybookFired }: Props) => {
const config = useChatConfig({ onReady, onResize, onPlaybookFired });
const ref = useRef<HTMLDivElement>(null);
const [isClosed, setIsClosed] = useState(false);

if (!config.enabled || isClosed) {
return null;
}

const { isReady, isResized, style } = config;
const { right } = style;

// clipPath: 'inset(40px 4px 72px 30px)',
const { isReady } = config;

return (
<iframe
loading="lazy"
data-test-subj="cloud-chat-frame"
title={i18n.translate('xpack.cloudChat.chatFrameTitle', {
defaultMessage: 'Chat',
Expand All @@ -59,8 +60,6 @@ export const Chat = ({ onHide = () => {}, onReady, onResize }: Props) => {
// position
top: 32,
right: 0,
// trim
clipPath: 'inset(24px 4px 76px 30px)',
}
: { position: 'absolute' }
}
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

/**
* A component that will display a trigger that will allow the user to chat with a human operator,
* when the service is enabled; otherwise, it renders nothing.
*/
export { Chat } from './chat';
export type { ChatApi, Props } from './chat';
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ type UseChatType =
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';
Expand All @@ -35,13 +41,14 @@ type ChatConfigParams = Exclude<ChatProps, 'onHide'>;
export const useChatConfig = ({
onReady = () => {},
onResize = () => {},
onPlaybookFired = () => {},
}: ChatConfigParams): UseChatType => {
const ref = useRef<HTMLIFrameElement>(null);
const chat = useChat();
const history = useHistory();
const [style, setStyle] = useState<CSSProperties>({ height: 0, width: 0 });
const [isReady, setIsReady] = useState(false);
const [isResized, setIsResized] = useState(false);
const isChatOpenRef = useRef<boolean>(false);

useEffect(() => {
const handleMessage = (event: MessageEvent): void => {
Expand All @@ -56,6 +63,40 @@ export const useChatConfig = ({
const { user: userConfig } = 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: {
Expand Down Expand Up @@ -97,8 +138,19 @@ export const useChatConfig = ({

// The chat widget is ready.
case MESSAGE_WIDGET_READY:
chatApi.hide();
setIsReady(true);
onReady();
onReady(chatApi);
break;

case 'driftChatClosed':
chatApi.hide();
break;

case 'driftPlaybookFired':
onPlaybookFired();
break;

default:
break;
}
Expand All @@ -107,31 +159,17 @@ export const useChatConfig = ({
window.addEventListener('message', handleMessage);

return () => window.removeEventListener('message', handleMessage);
}, [chat, style, onReady, onResize, isReady, isResized]);

useEffect(() => {
if (!isReady) return;
if (!history) return;

const unlisten = history.listen(() => {
// eslint-disable-next-line no-console
console.log('drift: posting update', getChatContext().window.location.href);
ref.current?.contentWindow?.postMessage(
{
type: MESSAGE_UPDATE_CONTEXT,
data: { context: getChatContext() },
},
'*'
);
});

return () => {
unlisten();
};
}, [history, isReady]);
}, [chat, style, onReady, onResize, isReady, isResized, onPlaybookFired]);

if (chat) {
return { enabled: true, src: chat.chatURL, ref, style, isReady, isResized };
return {
enabled: true,
src: chat.chatURL,
ref,
style,
isReady,
isResized,
};
}

return { enabled: false };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 function whenIdle(doWork: () => void) {
const requestIdleCallback = window.requestIdleCallback || window.setTimeout;
if (document.readyState === 'complete') {
requestIdleCallback(() => doWork());
} else {
window.addEventListener('load', () => {
requestIdleCallback(() => doWork());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React, { Suspense } from 'react';
import { EuiErrorBoundary } from '@elastic/eui';
import type { Props } from './chat';

/**
* A suspense-compatible version of the Chat component.
Expand All @@ -17,10 +18,10 @@ export const LazyChat = React.lazy(() => import('./chat').then(({ 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 = () => (
export const Chat = (props: Props) => (
<EuiErrorBoundary>
<Suspense fallback={<div />}>
<LazyChat />
<Suspense fallback={<></>}>
<LazyChat {...props} />
</Suspense>
</EuiErrorBoundary>
);
46 changes: 34 additions & 12 deletions x-pack/plugins/cloud_integrations/cloud_chat/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { ReplaySubject } from 'rxjs';
import { InternalApplicationSetup } from '@kbn/core-application-browser-internal';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
import { EuiHeaderSectionItemButton, EuiIcon } from '@elastic/eui';
import { EuiButtonEmpty } from '@elastic/eui';
import type { 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 { Chat } from './components';
import chatIcon from './chat_icon.svg';
import type { ChatApi } from './components/chat';
import { whenIdle } from './components/chat/when_idle';

interface CloudChatSetupDeps {
cloud: CloudSetup;
Expand Down Expand Up @@ -86,29 +88,49 @@ export class CloudChatPlugin implements Plugin<void, void, CloudChatSetupDeps, C

function ChatHeaderMenuItem() {
const [show, setShow] = React.useState(false);
const [chatApi, setChatApi] = React.useState<ChatApi | null>(null);

return (
<CloudContextProvider>
<KibanaThemeProvider theme$={core.theme.theme$}>
<I18nProvider>
<EuiHeaderSectionItemButton
data-test-subj="cloudChat"
notification={true}
onClick={() => setShow(!show)}
>
<EuiIcon type={chatIcon} size="m" />
</EuiHeaderSectionItemButton>
{show && (
<EuiButtonEmpty
css={{ color: '#fff', marginRight: 12 }}
size="s"
iconType={chatIcon}
data-test-subj="cloudChat"
onClick={() => {
chatApi?.toggle();
}}
>
Live Chat
</EuiButtonEmpty>
)}
</I18nProvider>
</KibanaThemeProvider>
{show && <Chat />}
{ReactDOM.createPortal(
<Chat
onReady={(_chatApi) => {
setChatApi(_chatApi);
}}
onPlaybookFired={() => {
setShow(true);
}}
/>,
document.body
)}
</CloudContextProvider>
);
}

core.chrome.navControls.registerRight({
order: 1000,
core.chrome.navControls.registerExtension({
order: 50,
mount: (e) => {
ReactDOM.render(<ChatHeaderMenuItem />, e);
// postpone rendering to avoid slowing the page load
whenIdle(() => {
ReactDOM.render(<ChatHeaderMenuItem />, e);
});

return () => {
ReactDOM.unmountComponentAtNode(e);
Expand Down

0 comments on commit 46cc24b

Please sign in to comment.