Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant committed Sep 20, 2023
1 parent 83f8d4e commit 4e7290a
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React from 'react';
// import { css } from '@emotion/react';

import { i18n } from '@kbn/i18n';
import { WhenIdle } from './when_idle';
// import { EuiButtonEmpty } from '@elastic/eui';
// import { euiThemeVars } from '@kbn/ui-theme';

Expand Down Expand Up @@ -39,31 +40,30 @@ export const Chat = ({ onHide = () => {}, onReady, onResize, onPlaybookFired }:
return null;
}

const { isReady } = config;

return (
<iframe
loading="lazy"
data-test-subj="cloud-chat-frame"
title={i18n.translate('xpack.cloudChat.chatFrameTitle', {
defaultMessage: 'Chat',
})}
src={config.src}
ref={config.ref}
style={
isReady
? {
...config.style,
// reset
bottom: 'auto',
inset: 'initial',
// position
top: 32,
right: 0,
}
: { position: 'absolute' }
}
/>
<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
? {
...config.style,
// reset
bottom: 'auto',
inset: 'initial',
// position
top: 32,
right: 0,
}
: { position: 'absolute' }
}
/>
</WhenIdle>
);

// const buttonCSS = css`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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>
);
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,13 @@ export const useChatConfig = ({
// its interface.
case MESSAGE_RESIZE: {
const styles = message.data.styles || ({} as CSSProperties);
setStyle({ ...style, ...styles });
// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import React from 'react';

export function whenIdle(doWork: () => void) {
const requestIdleCallback = window.requestIdleCallback || window.setTimeout;
if (document.readyState === 'complete') {
Expand All @@ -15,3 +17,18 @@ export function whenIdle(doWork: () => void) {
});
}
}

/**
* Postpone rendering of children until the page is loaded and browser is idle.
*/
export const WhenIdle: React.FC = ({ children }) => {
const [idleFired, setIdleFired] = React.useState(false);

React.useEffect(() => {
whenIdle(() => {
setIdleFired(true);
});
}, []);

return idleFired ? <>{children}</> : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 } = useEuiTheme();

const isLargeScreen = useIsWithinMinBreakpoint('m');
if (!isLargeScreen) return null;

return (
<>
{showChatButton && (
<EuiTourStep
title={
<>
<EuiIcon type={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
css={{ color: euiTheme.colors.ghost, marginRight: euiTheme.size.m }}
size="s"
iconType={chatIconLight}
data-test-subj="cloudChat"
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);
}}
/>,
document.body
)}
</>
);
}
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 @@ -4,9 +4,5 @@
* 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';

export { ChatHeaderMenuItem } from './chat_header_menu_items';
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,5 @@
* 2.0.
*/

import React, { Suspense } from 'react';
import { EuiErrorBoundary } from '@elastic/eui';
import type { 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>
);
export { Chat, type ChatApi, type Props as ChatProps } from './chat';
export { ChatHeaderMenuItem } from './chat_header_menu_item';
2 changes: 0 additions & 2 deletions x-pack/plugins/cloud_integrations/cloud_chat/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,3 @@ import { CloudChatPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new CloudChatPlugin(initializerContext);
}

export { Chat } from './components';
80 changes: 20 additions & 60 deletions x-pack/plugins/cloud_integrations/cloud_chat/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,14 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb
import type { HttpSetup } from '@kbn/core-http-browser';
import type { SecurityPluginSetup } from '@kbn/security-plugin/public';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import { ReplaySubject } from 'rxjs';
import { ReplaySubject, first } from 'rxjs';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
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';
import { ChatHeaderMenuItem } from './components';

interface CloudChatSetupDeps {
cloud: CloudSetup;
Expand Down Expand Up @@ -60,74 +56,38 @@ export class CloudChatPlugin implements Plugin<void, void, CloudChatSetupDeps, C
// eslint-disable-next-line no-console
console.debug(`Error setting up Chat: ${e.toString()}`)
);
}

public start(core: CoreStart, { cloud }: CloudChatStartDeps) {
const CloudChatContextProvider: FC = ({ 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>;
};
cloud.registerCloudService(CloudChatContextProvider);
}

public start(core: CoreStart, { cloud }: CloudChatStartDeps) {
const CloudContextProvider = cloud.CloudContextProvider;

// core.chrome.setChatComponent(() => (
// <CloudContextProvider>
// <Chat />
// </CloudContextProvider>
// ));

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

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

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

return () => {
ReactDOM.unmountComponentAtNode(e);
};
},
this.chatConfig$.pipe(first((config) => config != null)).subscribe(() => {
core.chrome.navControls.registerExtension({
order: 50,
mount: (e) => {
ReactDOM.render(<ConnectedChatHeaderMenuItem />, e);

return () => {
ReactDOM.unmountComponentAtNode(e);
};
},
});
});
}

Expand Down

0 comments on commit 4e7290a

Please sign in to comment.