[Drift] Add chat to management and home getting started (#159121)

## Summary

Partially address https://github.com/elastic/kibana/issues/158835, add
cloud chat (drift) to more places: all management pages and
home/getting_started page

I hit an issue that both management and home couldn't depend directly on
`cloudChat` plugin. Here is the issue with more details
https://github.com/elastic/kibana/issues/159008. I worked around with
creating an intermediate `cloudChatProvider` plugin.


![Screenshot 2023-06-05 at 15 46
44](a051be0c-b5f0-437d-9e52-507643c14aba)

![Screenshot 2023-06-05 at 16 03
06](b3b705da-c8c7-4bb6-9e85-b4adefa583a6)

How do I run drift locally? 

Add this to kibana.yml


```
xpack.cloud.id: "some-id"
xpack.cloud.trial_end_date: "2023-06-21T00:00:00.000Z"

xpack.cloud_integrations.chat.enabled: true
xpack.cloud_integrations.chat.chatURL: "https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html"
xpack.cloud_integrations.chat.chatIdentitySecret: "some-secret" (get it from drift console)
```

You need to have access to our drift account. But I tested with a custom
account. To change account id I had to point
`xpack.cloud_integrations.chat.chatURL` to a script with custom drift
id.
This commit is contained in:
Anton Dosov 2023-06-13 14:10:49 +02:00 committed by GitHub
parent bf6848888b
commit d082d78e60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 228 additions and 24 deletions

1
.github/CODEOWNERS vendored
View file

@ -67,6 +67,7 @@ packages/kbn-ci-stats-reporter @elastic/kibana-operations
packages/kbn-ci-stats-shipper-cli @elastic/kibana-operations
packages/kbn-cli-dev-mode @elastic/kibana-operations
x-pack/plugins/cloud_integrations/cloud_chat @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_chat_provider @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_data_migration @elastic/platform-onboarding
x-pack/plugins/cloud_defend @elastic/sec-cloudnative-integrations
x-pack/plugins/cloud_integrations/cloud_experiments @elastic/kibana-core

View file

@ -467,6 +467,10 @@ for inventory and topology purposes.
|Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud.
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_chat_provider/README.md[cloudChatProvider]
|This plugin exists as a workaround for using cloudChat plugin in plugins which can't have a direct dependency on security plugin.
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_data_migration/README.md[cloudDataMigration]
|Static migration page where self-managed users can see text/copy about migrating to Elastic Cloud

View file

@ -168,6 +168,7 @@
"@kbn/chart-icons": "link:packages/kbn-chart-icons",
"@kbn/charts-plugin": "link:src/plugins/charts",
"@kbn/cloud-chat-plugin": "link:x-pack/plugins/cloud_integrations/cloud_chat",
"@kbn/cloud-chat-provider-plugin": "link:x-pack/plugins/cloud_integrations/cloud_chat_provider",
"@kbn/cloud-data-migration-plugin": "link:x-pack/plugins/cloud_integrations/cloud_data_migration",
"@kbn/cloud-defend-plugin": "link:x-pack/plugins/cloud_defend",
"@kbn/cloud-experiments-plugin": "link:x-pack/plugins/cloud_integrations/cloud_experiments",

View file

@ -11,6 +11,7 @@ pageLoadAssetSize:
charts: 55000
cloud: 21076
cloudChat: 19894
cloudChatProvider: 17114
cloudDataMigration: 19170
cloudDefend: 18697
cloudExperiments: 59358

View file

@ -15,7 +15,8 @@
"usageCollection",
"customIntegrations",
"cloud",
"guidedOnboarding"
"guidedOnboarding",
"cloudChatProvider"
],
"requiredBundles": [
"kibanaReact"

View file

@ -43,7 +43,9 @@ const skipText = i18n.translate('home.guidedOnboarding.gettingStarted.skip.butto
});
export const GettingStarted = () => {
const { application, trackUiMetric, chrome, guidedOnboardingService, cloud } = getServices();
const { application, trackUiMetric, chrome, guidedOnboardingService, cloud, cloudChat } =
getServices();
const [guidesState, setGuidesState] = useState<GuideState[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
@ -226,6 +228,7 @@ export const GettingStarted = () => {
{skipText}
</EuiLink>
</div>
{cloudChat?.Chat && <cloudChat.Chat />}
</EuiPageTemplate.Section>
</KibanaPageTemplate>
);

View file

@ -22,6 +22,7 @@ import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { SharePluginSetup } from '@kbn/share-plugin/public';
import { GuidedOnboardingApi } from '@kbn/guided-onboarding-plugin/public';
import { CloudSetup } from '@kbn/cloud-plugin/public';
import { CloudChatProviderPluginStart } from '@kbn/cloud-chat-provider-plugin/public';
import { TutorialService } from '../services/tutorials';
import { AddDataService } from '../services/add_data';
import { FeatureCatalogueRegistry } from '../services/feature_catalogue';
@ -53,6 +54,7 @@ export interface HomeKibanaServices {
welcomeService: WelcomeService;
guidedOnboardingService?: GuidedOnboardingApi;
cloud?: CloudSetup;
cloudChat?: CloudChatProviderPluginStart;
}
let services: HomeKibanaServices | null = null;

View file

@ -22,6 +22,7 @@ import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/
import { AppNavLinkStatus } from '@kbn/core/public';
import { SharePluginSetup } from '@kbn/share-plugin/public';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { CloudChatProviderPluginStart } from '@kbn/cloud-chat-provider-plugin/public';
import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants';
import { setServices } from './application/kibana_services';
import { ConfigSchema } from '../config';
@ -42,6 +43,7 @@ export interface HomePluginStartDependencies {
dataViews: DataViewsPublicPluginStart;
urlForwarding: UrlForwardingStart;
guidedOnboarding: GuidedOnboardingPluginStart;
cloudChatProvider?: CloudChatProviderPluginStart;
}
export interface HomePluginSetupDependencies {
@ -80,8 +82,10 @@ export class HomePublicPlugin
const trackUiMetric = usageCollection
? usageCollection.reportUiCounter.bind(usageCollection, 'Kibana_home')
: () => {};
const [coreStart, { dataViews, urlForwarding: urlForwardingStart, guidedOnboarding }] =
await core.getStartServices();
const [
coreStart,
{ dataViews, urlForwarding: urlForwardingStart, guidedOnboarding, cloudChatProvider },
] = await core.getStartServices();
setServices({
share,
trackUiMetric,
@ -106,6 +110,7 @@ export class HomePublicPlugin
welcomeService: this.welcomeService,
guidedOnboardingService: guidedOnboarding.guidedOnboardingApi,
cloud,
cloudChat: cloudChatProvider,
});
coreStart.chrome.docTitle.change(
i18n.translate('home.pageTitle', { defaultMessage: 'Home' })

View file

@ -30,6 +30,7 @@
"@kbn/ebt-tools",
"@kbn/core-analytics-server",
"@kbn/storybook",
"@kbn/cloud-chat-provider-plugin",
],
"exclude": [
"target/**/*",

View file

@ -10,7 +10,8 @@
"share"
],
"optionalPlugins": [
"home"
"home",
"cloudChatProvider"
],
"requiredBundles": [
"kibanaReact",

View file

@ -16,6 +16,7 @@ import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from '@kbn/core/p
import { reactRouterNavigate, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaPageTemplate, KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import type { CloudChatProviderPluginStart } from '@kbn/cloud-chat-provider-plugin/public';
import useObservable from 'react-use/lib/useObservable';
import {
ManagementSection,
@ -38,10 +39,11 @@ export interface ManagementAppDependencies {
kibanaVersion: string;
setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void;
isSidebarEnabled$: BehaviorSubject<boolean>;
cloudChat?: CloudChatProviderPluginStart;
}
export const ManagementApp = ({ dependencies, history, theme$ }: ManagementAppProps) => {
const { setBreadcrumbs, isSidebarEnabled$ } = dependencies;
const { setBreadcrumbs, isSidebarEnabled$, cloudChat } = dependencies;
const [selectedId, setSelectedId] = useState<string>('');
const [sections, setSections] = useState<ManagementSection[]>();
const isSidebarEnabled = useObservable(isSidebarEnabled$);
@ -114,6 +116,7 @@ export const ManagementApp = ({ dependencies, history, theme$ }: ManagementAppPr
dependencies={dependencies}
/>
</KibanaPageTemplate>
{cloudChat?.Chat ? <cloudChat.Chat /> : null}
</KibanaThemeProvider>
</I18nProvider>
);

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { BehaviorSubject } from 'rxjs';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { CloudChatProviderPluginStart } from '@kbn/cloud-chat-provider-plugin/public';
import {
CoreSetup,
CoreStart,
@ -39,6 +40,7 @@ interface ManagementSetupDependencies {
interface ManagementStartDependencies {
share: SharePluginStart;
cloudChatProvider?: CloudChatProviderPluginStart;
}
export class ManagementPlugin
@ -75,7 +77,10 @@ export class ManagementPlugin
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { home, share }: ManagementSetupDependencies) {
public setup(
core: CoreSetup<ManagementStartDependencies>,
{ home, share }: ManagementSetupDependencies
) {
const kibanaVersion = this.initializerContext.env.packageInfo.version;
const locator = share.url.locators.create(new ManagementAppLocatorDefinition());
const managementPlugin = this;
@ -108,13 +113,14 @@ export class ManagementPlugin
updater$: this.appUpdater,
async mount(params: AppMountParameters) {
const { renderApp } = await import('./application');
const [coreStart] = await core.getStartServices();
const [coreStart, plugins] = await core.getStartServices();
return renderApp(params, {
sections: getSectionsServiceStartPrivate(),
kibanaVersion,
setBreadcrumbs: coreStart.chrome.setBreadcrumbs,
isSidebarEnabled$: managementPlugin.isSidebarEnabled$,
cloudChat: plugins.cloudChatProvider,
});
},
});

View file

@ -20,6 +20,7 @@
"@kbn/i18n-react",
"@kbn/shared-ux-page-kibana-template",
"@kbn/shared-ux-router",
"@kbn/cloud-chat-provider-plugin",
],
"exclude": [
"target/**/*",

View file

@ -128,6 +128,8 @@
"@kbn/cli-dev-mode/*": ["packages/kbn-cli-dev-mode/*"],
"@kbn/cloud-chat-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat"],
"@kbn/cloud-chat-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat/*"],
"@kbn/cloud-chat-provider-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat_provider"],
"@kbn/cloud-chat-provider-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat_provider/*"],
"@kbn/cloud-data-migration-plugin": ["x-pack/plugins/cloud_integrations/cloud_data_migration"],
"@kbn/cloud-data-migration-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_data_migration/*"],
"@kbn/cloud-defend-plugin": ["x-pack/plugins/cloud_defend"],

View file

@ -13,7 +13,8 @@
"chat"
],
"requiredPlugins": [
"cloud"
"cloud",
"cloudChatProvider"
],
"optionalPlugins": [
"security"

View file

@ -83,7 +83,9 @@ export const Chat = ({ onHide = () => {}, onReady, onResize }: Props) => {
title={i18n.translate('xpack.cloudChat.chatFrameTitle', {
defaultMessage: 'Chat',
})}
{...config}
src={config.src}
ref={config.ref}
style={config.style}
/>
</div>
);

View file

@ -13,3 +13,4 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
export { Chat } from './components';
export type { CloudChatPluginStart } from './plugin';

View file

@ -64,12 +64,17 @@ describe('Cloud Chat Plugin', () => {
const cloud = cloudMock.createSetup();
const cloudChatProvider = {
registerChatProvider: jest.fn(),
};
plugin.setup(coreSetup, {
cloud: { ...cloud, isCloudEnabled, trialEndDate },
...(securityEnabled ? { security: securitySetup } : {}),
cloudChatProvider,
});
return { initContext, plugin, coreSetup };
return { initContext, plugin, coreSetup, cloudChatProvider };
};
it('chatConfig is not retrieved if cloud is not enabled', async () => {
@ -114,6 +119,11 @@ describe('Cloud Chat Plugin', () => {
});
expect(coreSetup.http.get).toHaveBeenCalled();
});
it('Chat component is registered with chatProvider plugin', async () => {
const { cloudChatProvider } = await setupPlugin({});
expect(cloudChatProvider.registerChatProvider).toBeCalled();
});
});
});
});

View file

@ -7,19 +7,26 @@
import React, { type FC } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { SecurityPluginSetup } from '@kbn/security-plugin/public';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import { CloudChatProviderPluginSetup } from '@kbn/cloud-chat-provider-plugin/public';
import { ReplaySubject } from 'rxjs';
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';
interface CloudChatSetupDeps {
cloud: CloudSetup;
security?: SecurityPluginSetup;
cloudChatProvider: CloudChatProviderPluginSetup;
}
interface CloudChatStartDeps {
cloud: CloudStart;
}
interface SetupChatDeps extends CloudChatSetupDeps {
@ -31,20 +38,26 @@ interface CloudChatConfig {
trialBuffer: number;
}
export class CloudChatPlugin implements Plugin {
export interface CloudChatPluginStart {
Chat: React.ComponentType;
}
export class CloudChatPlugin
implements Plugin<void, CloudChatPluginStart, CloudChatSetupDeps, CloudChatStartDeps>
{
private readonly config: CloudChatConfig;
private chatConfig$ = new ReplaySubject<ChatConfig>(1);
private Chat: React.ComponentType | undefined;
constructor(initializerContext: PluginInitializerContext<CloudChatConfig>) {
this.config = initializerContext.config.get();
}
public setup(core: CoreSetup, { cloud, security }: CloudChatSetupDeps) {
this.setupChat({ http: core.http, cloud, security }).catch((e) =>
public setup(core: CoreSetup, { cloud, security, cloudChatProvider }: CloudChatSetupDeps) {
this.setupChat({ http: core.http, cloud, security, cloudChatProvider }).catch((e) =>
// eslint-disable-next-line no-console
console.debug(`Error setting up Chat: ${e.toString()}`)
);
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.
@ -52,9 +65,20 @@ export class CloudChatPlugin implements Plugin {
return <ServicesProvider chat={chatConfig}>{children}</ServicesProvider>;
};
cloud.registerCloudService(CloudChatContextProvider);
cloudChatProvider.registerChatProvider(() => this.Chat);
}
public start() {}
public start(core: CoreStart, { cloud }: CloudChatStartDeps) {
const CloudContextProvider = cloud.CloudContextProvider;
this.Chat = () => (
<CloudContextProvider>
<Chat />
</CloudContextProvider>
);
return {
Chat: this.Chat,
};
}
public stop() {}

View file

@ -19,6 +19,7 @@
"@kbn/i18n",
"@kbn/ui-theme",
"@kbn/config-schema",
"@kbn/cloud-chat-provider-plugin",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,5 @@
# Cloud Chat Provider
This plugin exists as a workaround for using `cloudChat` plugin in plugins which can't have a direct dependency on security plugin.
Ideally we'd remove this plugin and used `cloudChat` directly https://github.com/elastic/kibana/issues/159008

View file

@ -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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../',
roots: ['<rootDir>/x-pack/plugins/cloud_integrations/cloud_chat_provider'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_chat_provider',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/x-pack/plugins/cloud_integrations/cloud_chat_provider/{common,public,server}/**/*.{ts,tsx}',
],
};

View file

@ -0,0 +1,11 @@
{
"type": "plugin",
"id": "@kbn/cloud-chat-provider-plugin",
"owner": "@elastic/kibana-core",
"description": "This plugin exists as a workaround for using `cloudChat` plugin in plugins which can't have a direct dependency on security plugin.",
"plugin": {
"id": "cloudChatProvider",
"server": false,
"browser": true
}
}

View file

@ -0,0 +1,15 @@
/*
* 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 { CloudChatProviderPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new CloudChatProviderPlugin(initializerContext);
}
export type { CloudChatProviderPluginSetup, CloudChatProviderPluginStart } from './plugin';

View file

@ -0,0 +1,49 @@
/*
* 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 type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import type { PluginInitializerContext } from '@kbn/core/public';
export interface CloudChatProviderPluginSetup {
registerChatProvider: (getChat: () => React.ComponentType | undefined) => void;
}
export interface CloudChatProviderPluginStart {
Chat?: React.ComponentType;
}
export class CloudChatProviderPlugin
implements Plugin<CloudChatProviderPluginSetup, CloudChatProviderPluginStart>
{
private getChat: (() => React.ComponentType | undefined) | undefined;
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup) {
return {
registerChatProvider: (getChat: () => React.ComponentType | undefined) => {
if (this.getChat) {
throw new Error('Chat component has already been provided');
}
this.getChat = getChat;
},
};
}
public start(core: CoreStart) {
return {
Chat: () => {
const Chat = this.getChat?.();
return Chat ? <Chat /> : <></>;
},
};
}
public stop() {}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": [
".storybook/**/*",
"common/**/*",
"public/**/*",
"server/**/*",
"../../../typings/**/*"
],
"kbn_references": ["@kbn/core"],
"exclude": ["target/**/*"]
}

View file

@ -16,6 +16,7 @@ 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
@ -83,6 +84,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--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}"`,
]
: []),
],

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('Cloud Chat integration', function () {
@ -21,10 +20,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
.expect(200);
});
it('chat widget is present when enabled', async () => {
PageObjects.common.navigateToUrl('integrations', 'browse', { useActualUrl: true });
const chat = await find.byCssSelector('[data-test-subj="floatingChatTrigger"]', 20000);
expect(chat).to.not.be(null);
it('chat widget is present on integrations page', async () => {
PageObjects.common.navigateToUrl('integrations', 'browse', {
useActualUrl: true,
shouldUseHashForSubUrl: false,
});
await testSubjects.existOrFail('cloud-chat');
});
it('chat widget is present on home getting_started page', async () => {
PageObjects.common.navigateToUrl('home', '/getting_started', {
useActualUrl: true,
shouldUseHashForSubUrl: true,
});
await testSubjects.existOrFail('cloud-chat');
});
it('chat widget is present on management page', async () => {
PageObjects.common.navigateToApp('management');
await testSubjects.existOrFail('cloud-chat');
});
});
}

View file

@ -3096,6 +3096,10 @@
version "0.0.0"
uid ""
"@kbn/cloud-chat-provider-plugin@link:x-pack/plugins/cloud_integrations/cloud_chat_provider":
version "0.0.0"
uid ""
"@kbn/cloud-data-migration-plugin@link:x-pack/plugins/cloud_integrations/cloud_data_migration":
version "0.0.0"
uid ""