mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Move Cloud Integrations out of the cloud
plugin (#141103)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
10884e6a5f
commit
74f30dcf8e
118 changed files with 1914 additions and 1270 deletions
|
@ -15,7 +15,7 @@ const STORYBOOKS = [
|
|||
'apm',
|
||||
'canvas',
|
||||
'ci_composite',
|
||||
'cloud',
|
||||
'cloud_chat',
|
||||
'coloring',
|
||||
'chart_icons',
|
||||
'controls',
|
||||
|
|
|
@ -424,10 +424,22 @@ The plugin exposes the static DefaultEditorController class to consume.
|
|||
|The cloud plugin adds Cloud-specific features to Kibana.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_chat/README.md[cloudChat]
|
||||
|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_experiments/README.mdx[cloudExperiments]
|
||||
|The Cloud Experiments Service provides the necessary APIs to implement A/B testing scenarios, fetching the variations in configuration and reporting back metrics to track conversion rates of the experiments.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_full_story/README.md[cloudFullStory]
|
||||
|Integrates with FullStory in order to provide better product analytics, so we can understand how our users make use of Kibana. This plugin should only run on Elastic Cloud.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_links/README.md[cloudLinks]
|
||||
|Adds all the links to the Elastic Cloud console.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_security_posture/README.md[cloudSecurityPosture]
|
||||
|Cloud Posture automates the identification and remediation of risks across cloud infrastructures
|
||||
|
||||
|
|
|
@ -10,7 +10,10 @@ pageLoadAssetSize:
|
|||
cases: 144442
|
||||
charts: 55000
|
||||
cloud: 21076
|
||||
cloudChat: 19894
|
||||
cloudExperiments: 59358
|
||||
cloudFullStory: 18493
|
||||
cloudLinks: 17629
|
||||
cloudSecurityPosture: 19109
|
||||
console: 46091
|
||||
controls: 40000
|
||||
|
|
|
@ -11,7 +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',
|
||||
cloud_chat: 'x-pack/plugins/cloud_integrations/cloud_chat/.storybook',
|
||||
coloring: 'packages/kbn-coloring/.storybook',
|
||||
chart_icons: 'packages/kbn-chart-icons/.storybook',
|
||||
content_management: 'packages/content-management/.storybook',
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["dataViews", "share", "urlForwarding"],
|
||||
"optionalPlugins": ["usageCollection", "customIntegrations"],
|
||||
"optionalPlugins": ["usageCollection", "customIntegrations", "cloud"],
|
||||
"requiredBundles": ["kibanaReact"]
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { HomePublicPlugin } from './plugin';
|
|||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks';
|
||||
import { SharePluginSetup } from '@kbn/share-plugin/public';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
|
||||
const mockInitializerContext = coreMock.createPluginInitializerContext();
|
||||
const mockShare = {} as SharePluginSetup;
|
||||
|
@ -24,14 +25,11 @@ describe('HomePublicPlugin', () => {
|
|||
});
|
||||
|
||||
describe('setup', () => {
|
||||
test('registers tutorial directory to feature catalogue', async () => {
|
||||
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
|
||||
coreMock.createSetup() as any,
|
||||
{
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
}
|
||||
);
|
||||
test('registers tutorial directory to feature catalogue', () => {
|
||||
const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(setup).toHaveProperty('featureCatalogue');
|
||||
expect(setup.featureCatalogue.register).toHaveBeenCalledTimes(1);
|
||||
expect(setup.featureCatalogue.register).toHaveBeenCalledWith(
|
||||
|
@ -44,53 +42,73 @@ describe('HomePublicPlugin', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('wires up and returns registry', async () => {
|
||||
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
|
||||
coreMock.createSetup() as any,
|
||||
{
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
}
|
||||
);
|
||||
test('wires up and returns registry', () => {
|
||||
const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(setup).toHaveProperty('featureCatalogue');
|
||||
expect(setup.featureCatalogue).toHaveProperty('register');
|
||||
});
|
||||
|
||||
test('wires up and returns environment service', async () => {
|
||||
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
|
||||
coreMock.createSetup() as any,
|
||||
{
|
||||
share: {} as SharePluginSetup,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
}
|
||||
);
|
||||
test('wires up and returns environment service', () => {
|
||||
const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
|
||||
share: {} as SharePluginSetup,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(setup).toHaveProperty('environment');
|
||||
expect(setup.environment).toHaveProperty('update');
|
||||
});
|
||||
|
||||
test('wires up and returns tutorial service', async () => {
|
||||
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
|
||||
coreMock.createSetup() as any,
|
||||
{
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
}
|
||||
);
|
||||
test('wires up and returns tutorial service', () => {
|
||||
const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(setup).toHaveProperty('tutorials');
|
||||
expect(setup.tutorials).toHaveProperty('setVariable');
|
||||
});
|
||||
|
||||
test('wires up and returns welcome service', async () => {
|
||||
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
|
||||
coreMock.createSetup() as any,
|
||||
{
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
}
|
||||
);
|
||||
test('wires up and returns welcome service', () => {
|
||||
const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(setup).toHaveProperty('welcomeScreen');
|
||||
expect(setup.welcomeScreen).toHaveProperty('registerOnRendered');
|
||||
expect(setup.welcomeScreen).toHaveProperty('registerTelemetryNoticeRenderer');
|
||||
});
|
||||
|
||||
test('sets the cloud environment variable when the cloud plugin is present but isCloudEnabled: false', () => {
|
||||
const cloud = { ...cloudMock.createSetup(), isCloudEnabled: false };
|
||||
const plugin = new HomePublicPlugin(mockInitializerContext);
|
||||
const setup = plugin.setup(coreMock.createSetup(), {
|
||||
cloud,
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(setup.environment.update).toHaveBeenCalledTimes(1);
|
||||
expect(setup.environment.update).toHaveBeenCalledWith({ cloud: false });
|
||||
expect(setup.tutorials.setVariable).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('when cloud is enabled, it sets the cloud environment and the tutorials variable "cloud"', () => {
|
||||
const cloud = { ...cloudMock.createSetup(), isCloudEnabled: true };
|
||||
const plugin = new HomePublicPlugin(mockInitializerContext);
|
||||
const setup = plugin.setup(coreMock.createSetup(), {
|
||||
cloud,
|
||||
share: mockShare,
|
||||
urlForwarding: urlForwardingPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(setup.environment.update).toHaveBeenCalledTimes(1);
|
||||
expect(setup.environment.update).toHaveBeenCalledWith({ cloud: true });
|
||||
expect(setup.tutorials.setVariable).toHaveBeenCalledTimes(1);
|
||||
expect(setup.tutorials.setVariable).toHaveBeenCalledWith('cloud', {
|
||||
id: 'mock-cloud-id',
|
||||
baseUrl: 'base-url',
|
||||
deploymentUrl: 'deployment-url',
|
||||
profileUrl: 'profile-url',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
|||
import { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
|
||||
import { AppNavLinkStatus } from '@kbn/core/public';
|
||||
import { SharePluginSetup } from '@kbn/share-plugin/public';
|
||||
import type { CloudSetup } from '@kbn/cloud-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 {
|
|||
}
|
||||
|
||||
export interface HomePluginSetupDependencies {
|
||||
cloud?: CloudSetup;
|
||||
share: SharePluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
urlForwarding: UrlForwardingSetup;
|
||||
|
@ -66,7 +68,7 @@ export class HomePublicPlugin
|
|||
|
||||
public setup(
|
||||
core: CoreSetup<HomePluginStartDependencies>,
|
||||
{ share, urlForwarding, usageCollection }: HomePluginSetupDependencies
|
||||
{ cloud, share, urlForwarding, usageCollection }: HomePluginSetupDependencies
|
||||
): HomePublicPluginSetup {
|
||||
core.application.register({
|
||||
id: PLUGIN_ID,
|
||||
|
@ -127,10 +129,25 @@ export class HomePublicPlugin
|
|||
order: 500,
|
||||
});
|
||||
|
||||
const environment = { ...this.environmentService.setup() };
|
||||
const tutorials = { ...this.tutorialService.setup() };
|
||||
if (cloud) {
|
||||
environment.update({ cloud: cloud.isCloudEnabled });
|
||||
if (cloud.isCloudEnabled) {
|
||||
tutorials.setVariable('cloud', {
|
||||
id: cloud.cloudId,
|
||||
baseUrl: cloud.baseUrl,
|
||||
// Cloud's API already provides the full URLs
|
||||
profileUrl: cloud.profileUrl?.replace(cloud.baseUrl ?? '', ''),
|
||||
deploymentUrl: cloud.deploymentUrl?.replace(cloud.baseUrl ?? '', ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
featureCatalogue,
|
||||
environment: { ...this.environmentService.setup() },
|
||||
tutorials: { ...this.tutorialService.setup() },
|
||||
environment,
|
||||
tutorials,
|
||||
addData: { ...this.addDataService.setup() },
|
||||
welcomeScreen: { ...this.welcomeService.setup() },
|
||||
};
|
||||
|
|
|
@ -18,14 +18,13 @@ const createSetupMock = (): jest.Mocked<EnvironmentServiceSetup> => {
|
|||
|
||||
const createMock = (): jest.Mocked<PublicMethodsOf<EnvironmentService>> => {
|
||||
const service = {
|
||||
setup: jest.fn(),
|
||||
setup: jest.fn(createSetupMock),
|
||||
getEnvironment: jest.fn(() => ({
|
||||
cloud: false,
|
||||
apmUi: false,
|
||||
ml: false,
|
||||
})),
|
||||
};
|
||||
service.setup.mockImplementation(createSetupMock);
|
||||
return service;
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
{ "path": "../kibana_react/tsconfig.json" },
|
||||
{ "path": "../share/tsconfig.json" },
|
||||
{ "path": "../url_forwarding/tsconfig.json" },
|
||||
{ "path": "../usage_collection/tsconfig.json" }
|
||||
{ "path": "../usage_collection/tsconfig.json" },
|
||||
{ "path": "../../../x-pack/plugins/cloud/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -60,6 +60,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
'--corePluginDeprecations.noLongerUsed=still_using',
|
||||
// for testing set buffer duration to 0 to immediately flush counters into saved objects.
|
||||
'--usageCollection.usageCounters.bufferDuration=0',
|
||||
// explicitly enable the cloud integration plugins to validate the rendered config keys
|
||||
'--xpack.cloud_integrations.chat.enabled=true',
|
||||
'--xpack.cloud_integrations.chat.chatURL=a_string',
|
||||
'--xpack.cloud_integrations.experiments.enabled=true',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.sdk_key=a_string',
|
||||
'--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string',
|
||||
'--xpack.cloud_integrations.full_story.enabled=true',
|
||||
'--xpack.cloud_integrations.full_story.org_id=a_string',
|
||||
...plugins.map(
|
||||
(pluginDir) => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`
|
||||
),
|
||||
|
|
|
@ -171,14 +171,17 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cases.markdownPlugins.lens (boolean)',
|
||||
'xpack.ccr.ui.enabled (boolean)',
|
||||
'xpack.cloud.base_url (string)',
|
||||
'xpack.cloud.chat.chatURL (string)',
|
||||
'xpack.cloud.chat.enabled (boolean)',
|
||||
'xpack.cloud.cname (string)',
|
||||
'xpack.cloud.deployment_url (string)',
|
||||
'xpack.cloud.full_story.enabled (boolean)',
|
||||
'xpack.cloud.full_story.org_id (any)',
|
||||
'xpack.cloud_integrations.chat.chatURL (string)',
|
||||
// No PII. This is an escape patch to override LaunchDarkly's flag resolution mechanism for testing or quick fix.
|
||||
'xpack.cloud_integrations.experiments.flag_overrides (record)',
|
||||
// Commented because it's inside a schema conditional, and the test is not able to resolve it. But it's shared.
|
||||
// Added here for documentation purposes.
|
||||
// 'xpack.cloud_integrations.experiments.launch_darkly.client_id (string)',
|
||||
'xpack.cloud_integrations.full_story.org_id (any)',
|
||||
// No PII. Just the list of event types we want to forward to FullStory.
|
||||
'xpack.cloud.full_story.eventTypesAllowlist (array)',
|
||||
'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)',
|
||||
'xpack.cloud.id (string)',
|
||||
'xpack.cloud.organization_url (string)',
|
||||
'xpack.cloud.profile_url (string)',
|
||||
|
|
|
@ -313,8 +313,14 @@
|
|||
"@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"],
|
||||
"@kbn/cases-plugin": ["x-pack/plugins/cases"],
|
||||
"@kbn/cases-plugin/*": ["x-pack/plugins/cases/*"],
|
||||
"@kbn/cloud-chat-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat"],
|
||||
"@kbn/cloud-chat-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat/*"],
|
||||
"@kbn/cloud-experiments-plugin": ["x-pack/plugins/cloud_integrations/cloud_experiments"],
|
||||
"@kbn/cloud-experiments-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_experiments/*"],
|
||||
"@kbn/cloud-full-story-plugin": ["x-pack/plugins/cloud_integrations/cloud_full_story"],
|
||||
"@kbn/cloud-full-story-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_full_story/*"],
|
||||
"@kbn/cloud-links-plugin": ["x-pack/plugins/cloud_integrations/cloud_links"],
|
||||
"@kbn/cloud-links-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_links/*"],
|
||||
"@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"],
|
||||
"@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"],
|
||||
"@kbn/cloud-plugin": ["x-pack/plugins/cloud"],
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
"xpack.canvas": "plugins/canvas",
|
||||
"xpack.cases": "plugins/cases",
|
||||
"xpack.cloud": "plugins/cloud",
|
||||
"xpack.cloudChat": "plugins/cloud_integrations/cloud_chat",
|
||||
"xpack.cloudLinks": "plugins/cloud_integrations/cloud_links",
|
||||
"xpack.csp": "plugins/cloud_security_posture",
|
||||
"xpack.dashboard": "plugins/dashboard_enhanced",
|
||||
"xpack.discover": "plugins/discover_enhanced",
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
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.
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "cloud"],
|
||||
"optionalPlugins": ["cloudExperiments", "usageCollection", "home", "security"],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"server": true,
|
||||
"ui": true
|
||||
}
|
||||
|
|
|
@ -13,5 +13,3 @@ export type { CloudSetup, CloudConfigType, CloudStart } from './plugin';
|
|||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export { Chat } from './components';
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { CloudStart } from '.';
|
||||
import { ServicesProvider } from './services';
|
||||
|
||||
function createSetupMock() {
|
||||
return {
|
||||
|
@ -19,28 +18,22 @@ function createSetupMock() {
|
|||
deploymentUrl: 'deployment-url',
|
||||
profileUrl: 'profile-url',
|
||||
organizationUrl: 'organization-url',
|
||||
registerCloudService: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
const config = {
|
||||
chat: {
|
||||
enabled: true,
|
||||
chatURL: 'chat-url',
|
||||
user: {
|
||||
id: 'user-id',
|
||||
email: 'test-user@elastic.co',
|
||||
jwt: 'identity-jwt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getContextProvider: () => React.FC =
|
||||
() =>
|
||||
({ children }) =>
|
||||
<ServicesProvider {...config}>{children}</ServicesProvider>;
|
||||
<>{children}</>;
|
||||
|
||||
const createStartMock = (): jest.Mocked<CloudStart> => ({
|
||||
CloudContextProvider: jest.fn(getContextProvider()),
|
||||
cloudId: 'mock-cloud-id',
|
||||
isCloudEnabled: true,
|
||||
deploymentUrl: 'deployment-url',
|
||||
profileUrl: 'profile-url',
|
||||
organizationUrl: 'organization-url',
|
||||
});
|
||||
|
||||
export const cloudMock = {
|
||||
|
|
|
@ -5,308 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Sha256 } from '@kbn/crypto-browser';
|
||||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { homePluginMock } from '@kbn/home-plugin/public/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
import { CloudPlugin, type CloudConfigType } from './plugin';
|
||||
import { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
|
||||
import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
|
||||
import { CloudPlugin } from './plugin';
|
||||
|
||||
const baseConfig = {
|
||||
base_url: 'https://cloud.elastic.co',
|
||||
deployment_url: '/abc123',
|
||||
profile_url: '/user/settings/',
|
||||
organization_url: '/account/',
|
||||
full_story: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe('Cloud Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
describe('setupFullStory', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupPlugin = async ({ config = {} }: { config?: Partial<CloudConfigType> }) => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
...baseConfig,
|
||||
id: 'cloudId',
|
||||
...config,
|
||||
});
|
||||
|
||||
const plugin = new CloudPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
||||
const setup = plugin.setup(coreSetup, {});
|
||||
|
||||
// Wait for FullStory dynamic import to resolve
|
||||
await new Promise((r) => setImmediate(r));
|
||||
|
||||
return { initContext, plugin, setup, coreSetup };
|
||||
};
|
||||
|
||||
test('register the shipper FullStory with correct args when enabled and org_id are set', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { full_story: { enabled: true, org_id: 'foo' } },
|
||||
});
|
||||
|
||||
expect(coreSetup.analytics.registerShipper).toHaveBeenCalled();
|
||||
expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
|
||||
fullStoryOrgId: 'foo',
|
||||
scriptUrl: '/internal/cloud/100/fullstory.js',
|
||||
namespace: 'FSKibana',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call initializeFullStory when enabled=false', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { full_story: { enabled: false, org_id: 'foo' } },
|
||||
});
|
||||
expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call initializeFullStory when org_id is undefined', async () => {
|
||||
const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } });
|
||||
expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupTelemetryContext', () => {
|
||||
const username = '1234';
|
||||
const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupPlugin = async ({
|
||||
config = {},
|
||||
securityEnabled = true,
|
||||
currentUserProps = {},
|
||||
}: {
|
||||
config?: Partial<CloudConfigType>;
|
||||
securityEnabled?: boolean;
|
||||
currentUserProps?: Record<string, any> | Error;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
...baseConfig,
|
||||
...config,
|
||||
});
|
||||
|
||||
const plugin = new CloudPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const securitySetup = securityMock.createSetup();
|
||||
if (currentUserProps instanceof Error) {
|
||||
securitySetup.authc.getCurrentUser.mockRejectedValue(currentUserProps);
|
||||
} else {
|
||||
securitySetup.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser(currentUserProps)
|
||||
);
|
||||
}
|
||||
|
||||
const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {});
|
||||
|
||||
return { initContext, plugin, setup, coreSetup };
|
||||
};
|
||||
|
||||
test('register the context provider for the cloud user with hashed user ID when security is available', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { id: 'cloudId' },
|
||||
currentUserProps: { username },
|
||||
});
|
||||
|
||||
expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
|
||||
|
||||
const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
|
||||
([{ name }]) => name === 'cloud_user_id'
|
||||
)!;
|
||||
|
||||
await expect(firstValueFrom(context$)).resolves.toEqual({
|
||||
userId: '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041',
|
||||
isElasticCloudUser: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('user hash includes cloud id', async () => {
|
||||
const { coreSetup: coreSetup1 } = await setupPlugin({
|
||||
config: { id: 'esOrg1' },
|
||||
currentUserProps: { username },
|
||||
});
|
||||
|
||||
const [{ context$: context1$ }] =
|
||||
coreSetup1.analytics.registerContextProvider.mock.calls.find(
|
||||
([{ name }]) => name === 'cloud_user_id'
|
||||
)!;
|
||||
|
||||
const { userId: hashId1 } = (await firstValueFrom(context1$)) as { userId: string };
|
||||
expect(hashId1).not.toEqual(expectedHashedPlainUsername);
|
||||
|
||||
const { coreSetup: coreSetup2 } = await setupPlugin({
|
||||
config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' },
|
||||
currentUserProps: { username },
|
||||
});
|
||||
|
||||
const [{ context$: context2$ }] =
|
||||
coreSetup2.analytics.registerContextProvider.mock.calls.find(
|
||||
([{ name }]) => name === 'cloud_user_id'
|
||||
)!;
|
||||
|
||||
const { userId: hashId2 } = (await firstValueFrom(context2$)) as { userId: string };
|
||||
expect(hashId2).not.toEqual(expectedHashedPlainUsername);
|
||||
|
||||
expect(hashId1).not.toEqual(hashId2);
|
||||
});
|
||||
|
||||
test('user hash does not include cloudId when user is an Elastic Cloud user', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { id: 'cloudDeploymentId' },
|
||||
currentUserProps: { username, elastic_cloud_user: true },
|
||||
});
|
||||
|
||||
expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
|
||||
|
||||
const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
|
||||
([{ name }]) => name === 'cloud_user_id'
|
||||
)!;
|
||||
|
||||
await expect(firstValueFrom(context$)).resolves.toEqual({
|
||||
userId: expectedHashedPlainUsername,
|
||||
isElasticCloudUser: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('user hash does not include cloudId when not provided', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: {},
|
||||
currentUserProps: { username },
|
||||
});
|
||||
|
||||
expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
|
||||
|
||||
const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
|
||||
([{ name }]) => name === 'cloud_user_id'
|
||||
)!;
|
||||
|
||||
await expect(firstValueFrom(context$)).resolves.toEqual({
|
||||
userId: expectedHashedPlainUsername,
|
||||
isElasticCloudUser: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('user hash is undefined when failed to fetch a user', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
currentUserProps: new Error('failed to fetch a user'),
|
||||
});
|
||||
|
||||
expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
|
||||
|
||||
const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
|
||||
([{ name }]) => name === 'cloud_user_id'
|
||||
)!;
|
||||
|
||||
await expect(firstValueFrom(context$)).resolves.toEqual({
|
||||
userId: undefined,
|
||||
isElasticCloudUser: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupChat', () => {
|
||||
let consoleMock: jest.SpyInstance<void, [message?: any, ...optionalParams: any[]]>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleMock.mockRestore();
|
||||
});
|
||||
|
||||
const setupPlugin = async ({
|
||||
config = {},
|
||||
securityEnabled = true,
|
||||
currentUserProps = {},
|
||||
isCloudEnabled = true,
|
||||
failHttp = false,
|
||||
}: {
|
||||
config?: Partial<CloudConfigType>;
|
||||
securityEnabled?: boolean;
|
||||
currentUserProps?: Record<string, any>;
|
||||
isCloudEnabled?: boolean;
|
||||
failHttp?: boolean;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
...baseConfig,
|
||||
id: isCloudEnabled ? 'cloud-id' : null,
|
||||
...config,
|
||||
});
|
||||
|
||||
const plugin = new CloudPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
if (failHttp) {
|
||||
coreSetup.http.get.mockImplementation(() => {
|
||||
throw new Error('HTTP request failed');
|
||||
});
|
||||
}
|
||||
|
||||
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
|
||||
|
||||
const securitySetup = securityMock.createSetup();
|
||||
securitySetup.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser(currentUserProps)
|
||||
);
|
||||
|
||||
const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {});
|
||||
|
||||
return { initContext, plugin, setup, coreSetup };
|
||||
};
|
||||
|
||||
it('chatConfig is not retrieved if cloud is not enabled', async () => {
|
||||
const { coreSetup } = await setupPlugin({ isCloudEnabled: false });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if security is not enabled', async () => {
|
||||
const { coreSetup } = await setupPlugin({ securityEnabled: false });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if chat is enabled but url is not provided', async () => {
|
||||
// @ts-expect-error 2741
|
||||
const { coreSetup } = await setupPlugin({ config: { chat: { enabled: true } } });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if internal API fails', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
|
||||
failHttp: true,
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
expect(consoleMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is retrieved if chat is enabled and url is provided', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interface', () => {
|
||||
const setupPlugin = () => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
|
@ -317,7 +27,7 @@ describe('Cloud Plugin', () => {
|
|||
const plugin = new CloudPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const setup = plugin.setup(coreSetup, {});
|
||||
const setup = plugin.setup(coreSetup);
|
||||
|
||||
return { setup };
|
||||
};
|
||||
|
@ -361,49 +71,10 @@ describe('Cloud Plugin', () => {
|
|||
const { setup } = setupPlugin();
|
||||
expect(setup.cname).toBe('cloud.elastic.co');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set up cloudExperiments', () => {
|
||||
describe('when cloud ID is not provided in the config', () => {
|
||||
let cloudExperiments: jest.Mocked<CloudExperimentsPluginSetup>;
|
||||
beforeEach(() => {
|
||||
const plugin = new CloudPlugin(coreMock.createPluginInitializerContext(baseConfig));
|
||||
cloudExperiments = cloudExperimentsMock.createSetupMock();
|
||||
plugin.setup(coreMock.createSetup(), { cloudExperiments });
|
||||
});
|
||||
|
||||
test('does not call cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cloud ID is provided in the config', () => {
|
||||
let cloudExperiments: jest.Mocked<CloudExperimentsPluginSetup>;
|
||||
beforeEach(() => {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext({ ...baseConfig, id: 'cloud test' })
|
||||
);
|
||||
cloudExperiments = cloudExperimentsMock.createSetupMock();
|
||||
plugin.setup(coreMock.createSetup(), { cloudExperiments });
|
||||
});
|
||||
|
||||
test('calls cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('the cloud ID is hashed when calling cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser.mock.calls[0][0]).toEqual(
|
||||
'1acb4a1cc1c3d672a8d826055d897c2623ceb1d4fb07e46d97986751a36b06cf'
|
||||
);
|
||||
});
|
||||
|
||||
test('specifies the Kibana version when calling cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser.mock.calls[0][1]).toEqual(
|
||||
expect.objectContaining({
|
||||
kibanaVersion: 'version',
|
||||
})
|
||||
);
|
||||
});
|
||||
it('exposes registerCloudService', () => {
|
||||
const { setup } = setupPlugin();
|
||||
expect(setup.registerCloudService).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -426,9 +97,8 @@ describe('Cloud Plugin', () => {
|
|||
})
|
||||
);
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const homeSetup = homePluginMock.createSetupContract();
|
||||
|
||||
plugin.setup(coreSetup, { home: homeSetup });
|
||||
plugin.setup(coreSetup);
|
||||
|
||||
return { coreSetup, plugin };
|
||||
};
|
||||
|
@ -437,8 +107,7 @@ describe('Cloud Plugin', () => {
|
|||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
plugin.start(coreStart);
|
||||
|
||||
expect(coreStart.chrome.setHelpSupportUrl).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.chrome.setHelpSupportUrl.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
|
@ -447,177 +116,5 @@ describe('Cloud Plugin', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not register custom nav links on anonymous pages', async () => {
|
||||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
|
||||
|
||||
const securityStart = securityMock.createStart();
|
||||
securityStart.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({
|
||||
elastic_cloud_user: true,
|
||||
})
|
||||
);
|
||||
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled();
|
||||
expect(securityStart.authc.getCurrentUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registers a custom nav link for cloud users', async () => {
|
||||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
|
||||
securityStart.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({
|
||||
elastic_cloud_user: true,
|
||||
})
|
||||
);
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"euiIconType": "logoCloud",
|
||||
"href": "https://cloud.elastic.co/abc123",
|
||||
"title": "Manage this deployment",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers a custom nav link when there is an error retrieving the current user', async () => {
|
||||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
securityStart.authc.getCurrentUser.mockRejectedValue(new Error('something happened'));
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"euiIconType": "logoCloud",
|
||||
"href": "https://cloud.elastic.co/abc123",
|
||||
"title": "Manage this deployment",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not register a custom nav link for non-cloud users', async () => {
|
||||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
securityStart.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({
|
||||
elastic_cloud_user: false,
|
||||
})
|
||||
);
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registers user profile links for cloud users', async () => {
|
||||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
securityStart.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({
|
||||
elastic_cloud_user: true,
|
||||
})
|
||||
);
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/profile/alice",
|
||||
"iconType": "user",
|
||||
"label": "Edit profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/org/myOrg",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers profile links when there is an error retrieving the current user', async () => {
|
||||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
securityStart.authc.getCurrentUser.mockRejectedValue(new Error('something happened'));
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/profile/alice",
|
||||
"iconType": "user",
|
||||
"label": "Edit profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/org/myOrg",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not register profile links for non-cloud users', async () => {
|
||||
const { plugin } = startPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
securityStart.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({
|
||||
elastic_cloud_user: false,
|
||||
})
|
||||
);
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,34 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import type {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
HttpStart,
|
||||
IBasePath,
|
||||
AnalyticsServiceSetup,
|
||||
} from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { BehaviorSubject, catchError, from, map, of } from 'rxjs';
|
||||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
|
||||
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
||||
import { Sha256 } from '@kbn/crypto-browser';
|
||||
import type { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
|
||||
import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context';
|
||||
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
|
||||
import {
|
||||
ELASTIC_SUPPORT_LINK,
|
||||
CLOUD_SNAPSHOTS_PATH,
|
||||
GET_CHAT_USER_DATA_ROUTE_PATH,
|
||||
} from '../common/constants';
|
||||
import type { GetChatUserDataResponseBody } from '../common/types';
|
||||
import { createUserMenuLinks } from './user_menu_links';
|
||||
import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants';
|
||||
import { getFullCloudUrl } from './utils';
|
||||
import { ChatConfig, ServicesProvider } from './services';
|
||||
|
||||
export interface CloudConfigType {
|
||||
id?: string;
|
||||
|
@ -47,23 +25,6 @@ export interface CloudConfigType {
|
|||
org_id?: string;
|
||||
eventTypesAllowlist?: string[];
|
||||
};
|
||||
/** Configuration to enable live chat in Cloud-enabled instances of Kibana. */
|
||||
chat: {
|
||||
/** Determines if chat is enabled. */
|
||||
enabled: boolean;
|
||||
/** The URL to the remotely-hosted chat application. */
|
||||
chatURL: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CloudSetupDependencies {
|
||||
home?: HomePublicPluginSetup;
|
||||
security?: Pick<SecurityPluginSetup, 'authc'>;
|
||||
cloudExperiments?: CloudExperimentsPluginSetup;
|
||||
}
|
||||
|
||||
interface CloudStartDependencies {
|
||||
security?: SecurityPluginStart;
|
||||
}
|
||||
|
||||
export interface CloudStart {
|
||||
|
@ -71,6 +32,26 @@ export interface CloudStart {
|
|||
* A React component that provides a pre-wired `React.Context` which connects components to Cloud services.
|
||||
*/
|
||||
CloudContextProvider: FC<{}>;
|
||||
/**
|
||||
* `true` when Kibana is running on Elastic Cloud.
|
||||
*/
|
||||
isCloudEnabled: boolean;
|
||||
/**
|
||||
* Cloud ID. Undefined if not running on Cloud.
|
||||
*/
|
||||
cloudId?: string;
|
||||
/**
|
||||
* The full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
deploymentUrl?: string;
|
||||
/**
|
||||
* The full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
profileUrl?: string;
|
||||
/**
|
||||
* The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
organizationUrl?: string;
|
||||
}
|
||||
|
||||
export interface CloudSetup {
|
||||
|
@ -82,268 +63,93 @@ export interface CloudSetup {
|
|||
organizationUrl?: string;
|
||||
snapshotsUrl?: string;
|
||||
isCloudEnabled: boolean;
|
||||
registerCloudService: (contextProvider: FC) => void;
|
||||
}
|
||||
|
||||
interface SetupFullStoryDeps {
|
||||
analytics: AnalyticsServiceSetup;
|
||||
basePath: IBasePath;
|
||||
}
|
||||
|
||||
interface SetupChatDeps extends Pick<CloudSetupDependencies, 'security'> {
|
||||
http: CoreSetup['http'];
|
||||
interface CloudUrls {
|
||||
deploymentUrl?: string;
|
||||
profileUrl?: string;
|
||||
organizationUrl?: string;
|
||||
snapshotsUrl?: string;
|
||||
}
|
||||
|
||||
export class CloudPlugin implements Plugin<CloudSetup> {
|
||||
private readonly config: CloudConfigType;
|
||||
private readonly isCloudEnabled: boolean;
|
||||
private chatConfig$ = new BehaviorSubject<ChatConfig>({ enabled: false });
|
||||
private readonly contextProviders: FC[] = [];
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<CloudConfigType>();
|
||||
this.isCloudEnabled = getIsCloudEnabled(this.config.id);
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, { cloudExperiments, home, security }: CloudSetupDependencies) {
|
||||
this.setupTelemetryContext(core.analytics, security, this.config.id);
|
||||
public setup(core: CoreSetup): CloudSetup {
|
||||
registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id);
|
||||
|
||||
this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error setting up FullStory: ${e.toString()}`)
|
||||
);
|
||||
const { id, cname, base_url: baseUrl } = this.config;
|
||||
|
||||
const {
|
||||
id,
|
||||
return {
|
||||
cloudId: id,
|
||||
cname,
|
||||
baseUrl,
|
||||
...this.getCloudUrls(),
|
||||
isCloudEnabled: this.isCloudEnabled,
|
||||
registerCloudService: (contextProvider) => {
|
||||
this.contextProviders.push(contextProvider);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start(coreStart: CoreStart): CloudStart {
|
||||
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
|
||||
|
||||
// Nest all the registered context providers under the Cloud Services Provider.
|
||||
// This way, plugins only need to require Cloud's context provider to have all the enriched Cloud services.
|
||||
const CloudContextProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{this.contextProviders.reduce(
|
||||
(acc, ContextProvider) => (
|
||||
<ContextProvider> {acc} </ContextProvider>
|
||||
),
|
||||
children
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const { deploymentUrl, profileUrl, organizationUrl } = this.getCloudUrls();
|
||||
|
||||
return {
|
||||
CloudContextProvider,
|
||||
isCloudEnabled: this.isCloudEnabled,
|
||||
cloudId: this.config.id,
|
||||
deploymentUrl,
|
||||
profileUrl,
|
||||
organizationUrl,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private getCloudUrls(): CloudUrls {
|
||||
const {
|
||||
profile_url: profileUrl,
|
||||
organization_url: organizationUrl,
|
||||
deployment_url: deploymentUrl,
|
||||
base_url: baseUrl,
|
||||
} = this.config;
|
||||
|
||||
if (this.isCloudEnabled && id) {
|
||||
// We use the Hashed Cloud Deployment ID as the userId in the Cloud Experiments
|
||||
cloudExperiments?.identifyUser(sha256(id), {
|
||||
kibanaVersion: this.initializerContext.env.packageInfo.version,
|
||||
});
|
||||
}
|
||||
|
||||
this.setupChat({ http: core.http, security }).catch((e) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error setting up Chat: ${e.toString()}`)
|
||||
);
|
||||
|
||||
if (home) {
|
||||
home.environment.update({ cloud: this.isCloudEnabled });
|
||||
if (this.isCloudEnabled) {
|
||||
home.tutorials.setVariable('cloud', { id, baseUrl, profileUrl, deploymentUrl });
|
||||
}
|
||||
}
|
||||
|
||||
const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl);
|
||||
const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl);
|
||||
const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl);
|
||||
const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`;
|
||||
|
||||
return {
|
||||
cloudId: id,
|
||||
cname,
|
||||
baseUrl,
|
||||
deploymentUrl: fullCloudDeploymentUrl,
|
||||
profileUrl: fullCloudProfileUrl,
|
||||
organizationUrl: fullCloudOrganizationUrl,
|
||||
snapshotsUrl: fullCloudSnapshotsUrl,
|
||||
isCloudEnabled: this.isCloudEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
public start(coreStart: CoreStart, { security }: CloudStartDependencies): CloudStart {
|
||||
const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config;
|
||||
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
|
||||
|
||||
const setLinks = (authorized: boolean) => {
|
||||
if (!authorized) return;
|
||||
|
||||
if (baseUrl && deploymentUrl) {
|
||||
coreStart.chrome.setCustomNavLink({
|
||||
title: i18n.translate('xpack.cloud.deploymentLinkLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
}),
|
||||
euiIconType: 'logoCloud',
|
||||
href: getFullCloudUrl(baseUrl, deploymentUrl),
|
||||
});
|
||||
}
|
||||
|
||||
if (security && this.isCloudEnabled) {
|
||||
const userMenuLinks = createUserMenuLinks(this.config);
|
||||
security.navControlService.addUserMenuLinks(userMenuLinks);
|
||||
}
|
||||
};
|
||||
|
||||
this.checkIfAuthorizedForLinks({ http: coreStart.http, security })
|
||||
.then(setLinks)
|
||||
// In the event of an unexpected error, fail *open*.
|
||||
// Cloud admin console will always perform the actual authorization checks.
|
||||
.catch(() => setLinks(true));
|
||||
|
||||
// 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 CloudContextProvider: FC = ({ children }) => {
|
||||
const chatConfig = useObservable(this.chatConfig$, { enabled: false });
|
||||
return <ServicesProvider chat={chatConfig}>{children}</ServicesProvider>;
|
||||
};
|
||||
|
||||
return {
|
||||
CloudContextProvider,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
||||
/**
|
||||
* Determines if the current user should see links back to Cloud.
|
||||
* This isn't a true authorization check, but rather a heuristic to
|
||||
* see if the current user is *likely* a cloud deployment administrator.
|
||||
*
|
||||
* At this point, we do not have enough information to reliably make this determination,
|
||||
* but we do know that all cloud deployment admins are superusers by default.
|
||||
*/
|
||||
private async checkIfAuthorizedForLinks({
|
||||
http,
|
||||
security,
|
||||
}: {
|
||||
http: HttpStart;
|
||||
security?: SecurityPluginStart;
|
||||
}) {
|
||||
if (http.anonymousPaths.isAnonymous(window.location.pathname)) {
|
||||
return false;
|
||||
}
|
||||
// Security plugin is disabled
|
||||
if (!security) return true;
|
||||
|
||||
// Otherwise check if user is a cloud user.
|
||||
// If user is not defined due to an unexpected error, then fail *open*.
|
||||
// Cloud admin console will always perform the actual authorization checks.
|
||||
const user = await security.authc.getCurrentUser().catch(() => null);
|
||||
return user?.elastic_cloud_user ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the right config is provided, register the FullStory shipper to the analytics client.
|
||||
* @param analytics Core's Analytics service's setup contract.
|
||||
* @param basePath Core's http.basePath helper.
|
||||
* @private
|
||||
*/
|
||||
private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) {
|
||||
const { enabled, org_id: fullStoryOrgId, eventTypesAllowlist } = this.config.full_story;
|
||||
if (!enabled || !fullStoryOrgId) {
|
||||
return; // do not load any FullStory code in the browser if not enabled
|
||||
}
|
||||
|
||||
// Keep this import async so that we do not load any FullStory code into the browser when it is disabled.
|
||||
const { FullStoryShipper } = await import('@kbn/analytics-shippers-fullstory');
|
||||
analytics.registerShipper(FullStoryShipper, {
|
||||
eventTypesAllowlist,
|
||||
fullStoryOrgId,
|
||||
// Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN.
|
||||
scriptUrl: basePath.prepend(
|
||||
`/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js`
|
||||
),
|
||||
namespace: 'FSKibana',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the Analytics context providers.
|
||||
* @param analytics Core's Analytics service. The Setup contract.
|
||||
* @param security The security plugin.
|
||||
* @param cloudId The Cloud Org ID.
|
||||
* @private
|
||||
*/
|
||||
private setupTelemetryContext(
|
||||
analytics: AnalyticsServiceSetup,
|
||||
security?: Pick<SecurityPluginSetup, 'authc'>,
|
||||
cloudId?: string
|
||||
) {
|
||||
registerCloudDeploymentIdAnalyticsContext(analytics, cloudId);
|
||||
|
||||
if (security) {
|
||||
analytics.registerContextProvider({
|
||||
name: 'cloud_user_id',
|
||||
context$: from(security.authc.getCurrentUser()).pipe(
|
||||
map((user) => {
|
||||
if (user.elastic_cloud_user) {
|
||||
// If the user is managed by ESS, use the plain username as the user ID:
|
||||
// The username is expected to be unique for these users,
|
||||
// and it matches how users are identified in the Cloud UI, so it allows us to correlate them.
|
||||
return { userId: user.username, isElasticCloudUser: true };
|
||||
}
|
||||
|
||||
return {
|
||||
// For the rest of the authentication providers, we want to add the cloud deployment ID to make it unique.
|
||||
// Especially in the case of Elasticsearch-backed authentication, where users are commonly repeated
|
||||
// across multiple deployments (i.e.: `elastic` superuser).
|
||||
userId: cloudId ? `${cloudId}:${user.username}` : user.username,
|
||||
isElasticCloudUser: false,
|
||||
};
|
||||
}),
|
||||
// The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs
|
||||
map(({ userId, isElasticCloudUser }) => ({ userId: sha256(userId), isElasticCloudUser })),
|
||||
catchError(() => of({ userId: undefined, isElasticCloudUser: false }))
|
||||
),
|
||||
schema: {
|
||||
userId: {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'The user id scoped as seen by Cloud (hashed)' },
|
||||
},
|
||||
isElasticCloudUser: {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: '`true` if the user is managed by ESS.',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async setupChat({ http, security }: SetupChatDeps) {
|
||||
if (!this.isCloudEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { enabled, chatURL } = this.config.chat;
|
||||
|
||||
if (!security || !enabled || !chatURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
id,
|
||||
token: jwt,
|
||||
} = await http.get<GetChatUserDataResponseBody>(GET_CHAT_USER_DATA_ROUTE_PATH);
|
||||
|
||||
if (!email || !id || !jwt) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatConfig$.next({
|
||||
enabled,
|
||||
chatURL,
|
||||
user: {
|
||||
email,
|
||||
id,
|
||||
jwt,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`[cloud.chat] Could not retrieve chat config: ${e.res.status} ${e.message}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sha256(str: string) {
|
||||
return new Sha256().update(str, 'utf8').digest('hex');
|
||||
}
|
||||
|
|
|
@ -18,32 +18,11 @@ const apmConfigSchema = schema.object({
|
|||
),
|
||||
});
|
||||
|
||||
const fullStoryConfigSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
org_id: schema.conditional(
|
||||
schema.siblingRef('enabled'),
|
||||
true,
|
||||
schema.string({ minLength: 1 }),
|
||||
schema.maybe(schema.string())
|
||||
),
|
||||
eventTypesAllowlist: schema.arrayOf(schema.string(), {
|
||||
defaultValue: ['Loaded Kibana'],
|
||||
}),
|
||||
});
|
||||
|
||||
const chatConfigSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
chatURL: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
const configSchema = schema.object({
|
||||
apm: schema.maybe(apmConfigSchema),
|
||||
base_url: schema.maybe(schema.string()),
|
||||
chat: chatConfigSchema,
|
||||
chatIdentitySecret: schema.maybe(schema.string()),
|
||||
cname: schema.maybe(schema.string()),
|
||||
deployment_url: schema.maybe(schema.string()),
|
||||
full_story: fullStoryConfigSchema,
|
||||
id: schema.maybe(schema.string()),
|
||||
organization_url: schema.maybe(schema.string()),
|
||||
profile_url: schema.maybe(schema.string()),
|
||||
|
@ -54,10 +33,8 @@ export type CloudConfigType = TypeOf<typeof configSchema>;
|
|||
export const config: PluginConfigDescriptor<CloudConfigType> = {
|
||||
exposeToBrowser: {
|
||||
base_url: true,
|
||||
chat: true,
|
||||
cname: true,
|
||||
deployment_url: true,
|
||||
full_story: true,
|
||||
id: true,
|
||||
organization_url: true,
|
||||
profile_url: true,
|
||||
|
|
25
x-pack/plugins/cloud/server/mocks.ts
Normal file
25
x-pack/plugins/cloud/server/mocks.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { CloudSetup } from '.';
|
||||
|
||||
function createSetupMock(): jest.Mocked<CloudSetup> {
|
||||
return {
|
||||
cloudId: 'mock-cloud-id',
|
||||
instanceSizeMb: 1234,
|
||||
deploymentId: 'deployment-id',
|
||||
isCloudEnabled: true,
|
||||
apm: {
|
||||
url: undefined,
|
||||
secretToken: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const cloudMock = {
|
||||
createSetup: createSetupMock,
|
||||
};
|
|
@ -7,111 +7,54 @@
|
|||
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import { CloudPlugin } from './plugin';
|
||||
import { config } from './config';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';
|
||||
import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
|
||||
import { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
|
||||
|
||||
const baseConfig = {
|
||||
base_url: 'https://cloud.elastic.co',
|
||||
deployment_url: '/abc123',
|
||||
profile_url: '/user/settings/',
|
||||
organization_url: '/account/',
|
||||
};
|
||||
|
||||
describe('Cloud Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
describe('setupSecurity', () => {
|
||||
it('properly handles missing optional Security dependency if Cloud ID is NOT set.', async () => {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext(config.schema.validate({}))
|
||||
);
|
||||
describe('interface', () => {
|
||||
const setupPlugin = () => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
...baseConfig,
|
||||
id: 'cloudId',
|
||||
cname: 'cloud.elastic.co',
|
||||
});
|
||||
const plugin = new CloudPlugin(initContext);
|
||||
|
||||
expect(() =>
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
})
|
||||
).not.toThrow();
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const setup = plugin.setup(coreSetup, {});
|
||||
|
||||
return { setup };
|
||||
};
|
||||
|
||||
it('exposes isCloudEnabled', () => {
|
||||
const { setup } = setupPlugin();
|
||||
expect(setup.isCloudEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('properly handles missing optional Security dependency if Cloud ID is set.', async () => {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' }))
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
})
|
||||
).not.toThrow();
|
||||
it('exposes cloudId', () => {
|
||||
const { setup } = setupPlugin();
|
||||
expect(setup.cloudId).toBe('cloudId');
|
||||
});
|
||||
|
||||
it('does not notify Security plugin about Cloud environment if Cloud ID is NOT set.', async () => {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext(config.schema.validate({}))
|
||||
);
|
||||
|
||||
const securityDependencyMock = securityMock.createSetup();
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
security: securityDependencyMock,
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
});
|
||||
|
||||
expect(securityDependencyMock.setIsElasticCloudDeployment).not.toHaveBeenCalled();
|
||||
it('exposes instanceSizeMb', () => {
|
||||
const { setup } = setupPlugin();
|
||||
expect(setup.instanceSizeMb).toBeUndefined();
|
||||
});
|
||||
|
||||
it('properly notifies Security plugin about Cloud environment if Cloud ID is set.', async () => {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' }))
|
||||
);
|
||||
|
||||
const securityDependencyMock = securityMock.createSetup();
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
security: securityDependencyMock,
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
});
|
||||
|
||||
expect(securityDependencyMock.setIsElasticCloudDeployment).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set up cloudExperiments', () => {
|
||||
describe('when cloud ID is not provided in the config', () => {
|
||||
let cloudExperiments: jest.Mocked<CloudExperimentsPluginSetup>;
|
||||
beforeEach(() => {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext(config.schema.validate({}))
|
||||
);
|
||||
cloudExperiments = cloudExperimentsMock.createSetupMock();
|
||||
plugin.setup(coreMock.createSetup(), { cloudExperiments });
|
||||
});
|
||||
|
||||
test('does not call cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser).not.toHaveBeenCalled();
|
||||
});
|
||||
it('exposes deploymentId', () => {
|
||||
const { setup } = setupPlugin();
|
||||
expect(setup.deploymentId).toBe('abc123');
|
||||
});
|
||||
|
||||
describe('when cloud ID is provided in the config', () => {
|
||||
let cloudExperiments: jest.Mocked<CloudExperimentsPluginSetup>;
|
||||
beforeEach(() => {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext(config.schema.validate({ id: 'cloud test' }))
|
||||
);
|
||||
cloudExperiments = cloudExperimentsMock.createSetupMock();
|
||||
plugin.setup(coreMock.createSetup(), { cloudExperiments });
|
||||
});
|
||||
|
||||
test('calls cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('the cloud ID is hashed when calling cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser.mock.calls[0][0]).toEqual(
|
||||
'1acb4a1cc1c3d672a8d826055d897c2623ceb1d4fb07e46d97986751a36b06cf'
|
||||
);
|
||||
});
|
||||
|
||||
test('specifies the Kibana version when calling cloudExperiments.identifyUser', async () => {
|
||||
expect(cloudExperiments.identifyUser.mock.calls[0][1]).toEqual(
|
||||
expect.objectContaining({
|
||||
kibanaVersion: 'version',
|
||||
})
|
||||
);
|
||||
});
|
||||
it('exposes apm', () => {
|
||||
const { setup } = setupPlugin();
|
||||
expect(setup.apm).toStrictEqual({ url: undefined, secretToken: undefined });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,24 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||
import type { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
|
||||
import { createSHA256Hash } from '@kbn/crypto';
|
||||
import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context';
|
||||
import { CloudConfigType } from './config';
|
||||
import type { CloudConfigType } from './config';
|
||||
import { registerCloudUsageCollector } from './collectors';
|
||||
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
|
||||
import { parseDeploymentIdFromDeploymentUrl } from './utils';
|
||||
import { registerFullstoryRoute } from './routes/fullstory';
|
||||
import { registerChatRoute } from './routes/chat';
|
||||
import { readInstanceSizeMb } from './env';
|
||||
|
||||
interface PluginsSetup {
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
cloudExperiments?: CloudExperimentsPluginSetup;
|
||||
}
|
||||
|
||||
export interface CloudSetup {
|
||||
|
@ -37,52 +30,17 @@ export interface CloudSetup {
|
|||
}
|
||||
|
||||
export class CloudPlugin implements Plugin<CloudSetup> {
|
||||
private readonly logger: Logger;
|
||||
private readonly config: CloudConfigType;
|
||||
private readonly isDev: boolean;
|
||||
|
||||
constructor(private readonly context: PluginInitializerContext) {
|
||||
this.logger = this.context.logger.get();
|
||||
this.config = this.context.config.get<CloudConfigType>();
|
||||
this.isDev = this.context.env.mode.dev;
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
{ cloudExperiments, usageCollection, security }: PluginsSetup
|
||||
): CloudSetup {
|
||||
this.logger.debug('Setting up Cloud plugin');
|
||||
public setup(core: CoreSetup, { usageCollection }: PluginsSetup): CloudSetup {
|
||||
const isCloudEnabled = getIsCloudEnabled(this.config.id);
|
||||
registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id);
|
||||
registerCloudUsageCollector(usageCollection, { isCloudEnabled });
|
||||
|
||||
if (isCloudEnabled) {
|
||||
security?.setIsElasticCloudDeployment();
|
||||
}
|
||||
|
||||
if (isCloudEnabled && this.config.id) {
|
||||
// We use the Cloud ID as the userId in the Cloud Experiments
|
||||
cloudExperiments?.identifyUser(createSHA256Hash(this.config.id), {
|
||||
kibanaVersion: this.context.env.packageInfo.version,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.full_story.enabled) {
|
||||
registerFullstoryRoute({
|
||||
httpResources: core.http.resources,
|
||||
packageInfo: this.context.env.packageInfo,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.chat.enabled && this.config.chatIdentitySecret) {
|
||||
registerChatRoute({
|
||||
router: core.http.createRouter(),
|
||||
chatIdentitySecret: this.config.chatIdentitySecret,
|
||||
security,
|
||||
isDev: this.isDev,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cloudId: this.config.id,
|
||||
instanceSizeMb: readInstanceSizeMb(),
|
||||
|
|
|
@ -16,8 +16,5 @@
|
|||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/home/tsconfig.json" },
|
||||
{ "path": "../cloud_integrations/cloud_experiments/tsconfig.json" },
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
|
||||
import React from 'react';
|
||||
import { DecoratorFn } from '@storybook/react';
|
||||
import { ServicesProvider, CloudServices } from '../public/services';
|
||||
import { ServicesProvider, CloudChatServices } from '../public/services';
|
||||
|
||||
// TODO: move to a storybook implementation of the service using parameters.
|
||||
const services: CloudServices = {
|
||||
const services: CloudChatServices = {
|
||||
chat: {
|
||||
enabled: true,
|
||||
chatURL: 'https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html',
|
||||
user: {
|
||||
id: 'user-id',
|
3
x-pack/plugins/cloud_integrations/cloud_chat/README.md
Executable file
3
x-pack/plugins/cloud_integrations/cloud_chat/README.md
Executable file
|
@ -0,0 +1,3 @@
|
|||
# Cloud Chat
|
||||
|
||||
Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud.
|
8
x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts
Executable file
8
x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts
Executable file
|
@ -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 const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user';
|
18
x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js
Normal file
18
x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js
Normal 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'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_chat',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/cloud_integrations/cloud_chat/{common,public,server}/**/*.{ts,tsx}',
|
||||
],
|
||||
};
|
15
x-pack/plugins/cloud_integrations/cloud_chat/kibana.json
Executable file
15
x-pack/plugins/cloud_integrations/cloud_chat/kibana.json
Executable file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "cloudChat",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"owner": {
|
||||
"name": "Kibana Core",
|
||||
"githubTeam": "kibana-core"
|
||||
},
|
||||
"description": "Chat available on Elastic Cloud deployments for quicker assistance.",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"configPath": ["xpack", "cloud_integrations", "chat"],
|
||||
"requiredPlugins": ["cloud"],
|
||||
"optionalPlugins": ["security"]
|
||||
}
|
|
@ -68,7 +68,6 @@ export const Component = ({ id, email, chatURL, jwt }: Params) => {
|
|||
return (
|
||||
<ServicesProvider
|
||||
chat={{
|
||||
enabled: true,
|
||||
chatURL,
|
||||
user: {
|
||||
jwt,
|
|
@ -57,7 +57,7 @@ export const Chat = ({ onHide = () => {}, onReady, onResize }: Props) => {
|
|||
}}
|
||||
size="xs"
|
||||
>
|
||||
{i18n.translate('xpack.cloud.chat.hideChatButtonLabel', {
|
||||
{i18n.translate('xpack.cloudChat.hideChatButtonLabel', {
|
||||
defaultMessage: 'Hide chat',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
|
@ -80,7 +80,7 @@ export const Chat = ({ onHide = () => {}, onReady, onResize }: Props) => {
|
|||
{button}
|
||||
<iframe
|
||||
data-test-subj="cloud-chat-frame"
|
||||
title={i18n.translate('xpack.cloud.chat.chatFrameTitle', {
|
||||
title={i18n.translate('xpack.cloudChat.chatFrameTitle', {
|
||||
defaultMessage: 'Chat',
|
||||
})}
|
||||
{...config}
|
|
@ -23,7 +23,7 @@ const REFERRER = 'referrer';
|
|||
describe('getChatContext', () => {
|
||||
const url = new URL(HREF);
|
||||
|
||||
test('retreive the context', () => {
|
||||
test('retrieve the context', () => {
|
||||
Object.defineProperty(window, 'location', { value: url });
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: {
|
|
@ -45,11 +45,7 @@ export const useChatConfig = ({
|
|||
const handleMessage = (event: MessageEvent): void => {
|
||||
const { current: chatIframe } = ref;
|
||||
|
||||
if (
|
||||
!chat.enabled ||
|
||||
!chatIframe?.contentWindow ||
|
||||
event.source !== chatIframe?.contentWindow
|
||||
) {
|
||||
if (!chat || !chatIframe?.contentWindow || event.source !== chatIframe?.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -108,7 +104,7 @@ export const useChatConfig = ({
|
|||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [chat, style, onReady, onResize, isReady, isResized]);
|
||||
|
||||
if (chat.enabled) {
|
||||
if (chat) {
|
||||
return { enabled: true, src: chat.chatURL, ref, style, isReady, isResized };
|
||||
}
|
||||
|
15
x-pack/plugins/cloud_integrations/cloud_chat/public/index.ts
Executable file
15
x-pack/plugins/cloud_integrations/cloud_chat/public/index.ts
Executable 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 { CloudChatPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudChatPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export { Chat } from './components';
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { coreMock } from '@kbn/core/public/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import type { CloudChatConfigType } from '../server/config';
|
||||
import { CloudChatPlugin } from './plugin';
|
||||
|
||||
describe('Cloud Chat Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
describe('setupChat', () => {
|
||||
let consoleMock: jest.SpyInstance<void, [message?: any, ...optionalParams: any[]]>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleMock.mockRestore();
|
||||
});
|
||||
|
||||
const setupPlugin = async ({
|
||||
config = {},
|
||||
securityEnabled = true,
|
||||
currentUserProps = {},
|
||||
isCloudEnabled = true,
|
||||
failHttp = false,
|
||||
}: {
|
||||
config?: Partial<CloudChatConfigType>;
|
||||
securityEnabled?: boolean;
|
||||
currentUserProps?: Record<string, any>;
|
||||
isCloudEnabled?: boolean;
|
||||
failHttp?: boolean;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext(config);
|
||||
|
||||
const plugin = new CloudChatPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
if (failHttp) {
|
||||
coreSetup.http.get.mockImplementation(() => {
|
||||
throw new Error('HTTP request failed');
|
||||
});
|
||||
}
|
||||
|
||||
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
|
||||
|
||||
const securitySetup = securityMock.createSetup();
|
||||
securitySetup.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser(currentUserProps)
|
||||
);
|
||||
|
||||
const cloud = cloudMock.createSetup();
|
||||
|
||||
plugin.setup(coreSetup, {
|
||||
cloud: { ...cloud, isCloudEnabled },
|
||||
...(securityEnabled ? { security: securitySetup } : {}),
|
||||
});
|
||||
|
||||
return { initContext, plugin, coreSetup };
|
||||
};
|
||||
|
||||
it('chatConfig is not retrieved if cloud is not enabled', async () => {
|
||||
const { coreSetup } = await setupPlugin({ isCloudEnabled: false });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if security is not enabled', async () => {
|
||||
const { coreSetup } = await setupPlugin({ securityEnabled: false });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if chat is enabled but url is not provided', async () => {
|
||||
// @ts-expect-error 2741
|
||||
const { coreSetup } = await setupPlugin({ config: { chat: { enabled: true } } });
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is not retrieved if internal API fails', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chatURL: 'http://chat.elastic.co' },
|
||||
failHttp: true,
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
expect(consoleMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is retrieved if chat is enabled and url is provided', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chatURL: 'http://chat.elastic.co' },
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
88
x-pack/plugins/cloud_integrations/cloud_chat/public/plugin.tsx
Executable file
88
x-pack/plugins/cloud_integrations/cloud_chat/public/plugin.tsx
Executable file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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, { type FC } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { CoreSetup, 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 { ReplaySubject } from 'rxjs';
|
||||
import type { GetChatUserDataResponseBody } from '../common/types';
|
||||
import { GET_CHAT_USER_DATA_ROUTE_PATH } from '../common/constants';
|
||||
import { ChatConfig, ServicesProvider } from './services';
|
||||
|
||||
interface CloudChatSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
interface SetupChatDeps extends CloudChatSetupDeps {
|
||||
http: HttpSetup;
|
||||
}
|
||||
|
||||
interface CloudChatConfig {
|
||||
chatURL?: string;
|
||||
}
|
||||
|
||||
export class CloudChatPlugin implements Plugin {
|
||||
private readonly config: CloudChatConfig;
|
||||
private chatConfig$ = new ReplaySubject<ChatConfig>(1);
|
||||
|
||||
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) =>
|
||||
// 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.
|
||||
const chatConfig = useObservable(this.chatConfig$, undefined);
|
||||
return <ServicesProvider chat={chatConfig}>{children}</ServicesProvider>;
|
||||
};
|
||||
cloud.registerCloudService(CloudChatContextProvider);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private async setupChat({ cloud, http, security }: SetupChatDeps) {
|
||||
if (!cloud.isCloudEnabled || !security || !this.config.chatURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
id,
|
||||
token: jwt,
|
||||
} = await http.get<GetChatUserDataResponseBody>(GET_CHAT_USER_DATA_ROUTE_PATH);
|
||||
|
||||
if (!email || !id || !jwt) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatConfig$.next({
|
||||
chatURL: this.config.chatURL,
|
||||
user: {
|
||||
email,
|
||||
id,
|
||||
jwt,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`[cloud.chat] Could not retrieve chat config: ${e.res.status} ${e.message}`, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,12 +7,7 @@
|
|||
|
||||
import React, { FC, createContext, useContext } from 'react';
|
||||
|
||||
interface WithoutChat {
|
||||
enabled: false;
|
||||
}
|
||||
|
||||
interface WithChat {
|
||||
enabled: true;
|
||||
export interface ChatConfig {
|
||||
chatURL: string;
|
||||
user: {
|
||||
jwt: string;
|
||||
|
@ -21,26 +16,24 @@ interface WithChat {
|
|||
};
|
||||
}
|
||||
|
||||
export type ChatConfig = WithChat | WithoutChat;
|
||||
|
||||
export interface CloudServices {
|
||||
chat: ChatConfig;
|
||||
export interface CloudChatServices {
|
||||
chat?: ChatConfig;
|
||||
}
|
||||
|
||||
const ServicesContext = createContext<CloudServices>({ chat: { enabled: false } });
|
||||
const ServicesContext = createContext<CloudChatServices>({});
|
||||
|
||||
export const ServicesProvider: FC<CloudServices> = ({ children, ...services }) => (
|
||||
export const ServicesProvider: FC<CloudChatServices> = ({ children, ...services }) => (
|
||||
<ServicesContext.Provider value={services}>{children}</ServicesContext.Provider>
|
||||
);
|
||||
|
||||
/**
|
||||
* React hook for accessing the pre-wired `CloudServices`.
|
||||
* React hook for accessing the pre-wired `CloudChatServices`.
|
||||
*/
|
||||
export function useServices() {
|
||||
return useContext(ServicesContext);
|
||||
}
|
||||
|
||||
export function useChat(): ChatConfig {
|
||||
export function useChat(): ChatConfig | undefined {
|
||||
const { chat } = useServices();
|
||||
return chat;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { get, has } from 'lodash';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
|
||||
const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
chatURL: schema.maybe(schema.string()),
|
||||
chatIdentitySecret: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export type CloudChatConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<CloudChatConfigType> = {
|
||||
exposeToBrowser: {
|
||||
chatURL: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
deprecations: () => [
|
||||
// Silently move the chat configuration from `xpack.cloud` to `xpack.cloud_integrations.chat`.
|
||||
// No need to emit a deprecation log because it's an internal restructure
|
||||
(cfg) => {
|
||||
return {
|
||||
set: [
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.chat.enabled',
|
||||
toKey: 'xpack.cloud_integrations.chat.enabled',
|
||||
}),
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.chat.chatURL',
|
||||
toKey: 'xpack.cloud_integrations.chat.chatURL',
|
||||
}),
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.chatIdentitySecret',
|
||||
toKey: 'xpack.cloud_integrations.chat.chatIdentitySecret',
|
||||
}),
|
||||
],
|
||||
unset: [
|
||||
{ path: 'xpack.cloud.chat.enabled' },
|
||||
{ path: 'xpack.cloud.chat.chatURL' },
|
||||
{ path: 'xpack.cloud.chatIdentitySecret' },
|
||||
],
|
||||
};
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the `set` action only if the key exists in the `fromKey` value.
|
||||
* This is to avoid overwriting actual values with undefined.
|
||||
* @param cfg The config object
|
||||
* @param fromKey The key to copy from.
|
||||
* @param toKey The key where the value should be copied to.
|
||||
*/
|
||||
function copyIfExists({
|
||||
cfg,
|
||||
fromKey,
|
||||
toKey,
|
||||
}: {
|
||||
cfg: Readonly<{ [p: string]: unknown }>;
|
||||
fromKey: string;
|
||||
toKey: string;
|
||||
}) {
|
||||
return has(cfg, fromKey) ? [{ path: toKey, value: get(cfg, fromKey) }] : [];
|
||||
}
|
15
x-pack/plugins/cloud_integrations/cloud_chat/server/index.ts
Executable file
15
x-pack/plugins/cloud_integrations/cloud_chat/server/index.ts
Executable 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/server';
|
||||
import { CloudChatPlugin } from './plugin';
|
||||
|
||||
export { config } from './config';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudChatPlugin(initializerContext);
|
||||
}
|
43
x-pack/plugins/cloud_integrations/cloud_chat/server/plugin.ts
Executable file
43
x-pack/plugins/cloud_integrations/cloud_chat/server/plugin.ts
Executable file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext, CoreSetup, Plugin } from '@kbn/core/server';
|
||||
|
||||
import { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||
import { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import { registerChatRoute } from './routes';
|
||||
import { CloudChatConfigType } from './config';
|
||||
|
||||
interface CloudChatSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
export class CloudChatPlugin implements Plugin<void, void, CloudChatSetupDeps> {
|
||||
private readonly config: CloudChatConfigType;
|
||||
private readonly isDev: boolean;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config = initializerContext.config.get();
|
||||
this.isDev = initializerContext.env.mode.dev;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, { cloud, security }: CloudChatSetupDeps) {
|
||||
if (cloud.isCloudEnabled && this.config.chatIdentitySecret) {
|
||||
registerChatRoute({
|
||||
router: core.http.createRouter(),
|
||||
chatIdentitySecret: this.config.chatIdentitySecret,
|
||||
security,
|
||||
isDev: this.isDev,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
8
x-pack/plugins/cloud_integrations/cloud_chat/server/routes/index.ts
Executable file
8
x-pack/plugins/cloud_integrations/cloud_chat/server/routes/index.ts
Executable file
|
@ -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 { registerChatRoute } from './chat';
|
21
x-pack/plugins/cloud_integrations/cloud_chat/tsconfig.json
Normal file
21
x-pack/plugins/cloud_integrations/cloud_chat/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
},
|
||||
"include": [
|
||||
".storybook/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../cloud/tsconfig.json" },
|
||||
{ "path": "../../security/tsconfig.json" },
|
||||
]
|
||||
}
|
|
@ -9,6 +9,5 @@ export type {
|
|||
CloudExperimentsMetric,
|
||||
CloudExperimentsMetricNames,
|
||||
CloudExperimentsPluginStart,
|
||||
CloudExperimentsPluginSetup,
|
||||
CloudExperimentsFeatureFlagNames,
|
||||
} from './types';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CloudExperimentsPluginSetup, CloudExperimentsPluginStart } from './types';
|
||||
import type { CloudExperimentsPluginStart } from './types';
|
||||
|
||||
function createStartMock(): jest.Mocked<CloudExperimentsPluginStart> {
|
||||
return {
|
||||
|
@ -14,13 +14,6 @@ function createStartMock(): jest.Mocked<CloudExperimentsPluginStart> {
|
|||
};
|
||||
}
|
||||
|
||||
function createSetupMock(): jest.Mocked<CloudExperimentsPluginSetup> {
|
||||
return {
|
||||
identifyUser: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export const cloudExperimentsMock = {
|
||||
createSetupMock,
|
||||
createStartMock,
|
||||
};
|
||||
|
|
|
@ -7,27 +7,6 @@
|
|||
|
||||
import { FEATURE_FLAG_NAMES, METRIC_NAMES } from './constants';
|
||||
|
||||
/**
|
||||
* The contract of the setup lifecycle method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CloudExperimentsPluginSetup {
|
||||
/**
|
||||
* Identifies the user in the A/B testing service.
|
||||
* For now, we only rely on the user ID. In the future, we may request further details for more targeted experiments.
|
||||
* @param userId The unique identifier of the user in the experiment.
|
||||
* @param userMetadata Additional attributes to the user. Take care to ensure these values do not contain PII.
|
||||
*
|
||||
* @deprecated This API will become internal as soon as we reduce the dependency graph of the `cloud` plugin,
|
||||
* and this plugin depends on it to fetch the data.
|
||||
*/
|
||||
identifyUser: (
|
||||
userId: string,
|
||||
userMetadata?: Record<string, string | boolean | number | Array<string | boolean | number>>
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The names of the feature flags declared in Kibana.
|
||||
* Valid keys are defined in {@link FEATURE_FLAG_NAMES}. When using a new feature flag, add the name to the list.
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
"server": true,
|
||||
"ui": true,
|
||||
"configPath": ["xpack", "cloud_integrations", "experiments"],
|
||||
"requiredPlugins": [],
|
||||
"requiredPlugins": ["cloud"],
|
||||
"optionalPlugins": ["usageCollection"]
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import { ldClientMock } from './plugin.test.mock';
|
||||
import { CloudExperimentsPlugin } from './plugin';
|
||||
import { FEATURE_FLAG_NAMES } from '../common/constants';
|
||||
|
@ -61,13 +62,12 @@ describe('Cloud Experiments public plugin', () => {
|
|||
plugin = new CloudExperimentsPlugin(initializerContext);
|
||||
});
|
||||
|
||||
test('returns the contract', () => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup());
|
||||
expect(setupContract).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
identifyUser: expect.any(Function),
|
||||
test('returns no contract', () => {
|
||||
expect(
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: cloudMock.createSetup(),
|
||||
})
|
||||
);
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('identifyUser', () => {
|
||||
|
@ -76,44 +76,29 @@ describe('Cloud Experiments public plugin', () => {
|
|||
flag_overrides: { my_flag: '1234' },
|
||||
});
|
||||
const customPlugin = new CloudExperimentsPlugin(initializerContext);
|
||||
const setupContract = customPlugin.setup(coreMock.createSetup());
|
||||
expect(customPlugin).toHaveProperty('launchDarklyClient', undefined);
|
||||
setupContract.identifyUser('user-id', {});
|
||||
customPlugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
expect(customPlugin).toHaveProperty('launchDarklyClient', undefined);
|
||||
});
|
||||
|
||||
test('it initializes the LaunchDarkly client', () => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup());
|
||||
test('it skips creating the client if cloud is not enabled', () => {
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', undefined);
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: false },
|
||||
});
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', undefined);
|
||||
setupContract.identifyUser('user-id', {});
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', ldClientMock);
|
||||
expect(ldClientMock.identify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it calls identify if the client already exists', () => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup());
|
||||
test('it initializes the LaunchDarkly client', async () => {
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', undefined);
|
||||
setupContract.identifyUser('user-id', {});
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', ldClientMock);
|
||||
expect(ldClientMock.identify).not.toHaveBeenCalled();
|
||||
ldClientMock.identify.mockResolvedValue({}); // ensure it's a promise
|
||||
setupContract.identifyUser('user-id', {});
|
||||
expect(ldClientMock.identify).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it handles identify rejections', async () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
const setupContract = plugin.setup(coreMock.createSetup());
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', undefined);
|
||||
setupContract.identifyUser('user-id', {});
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', ldClientMock);
|
||||
expect(ldClientMock.identify).not.toHaveBeenCalled();
|
||||
const error = new Error('Something went terribly wrong');
|
||||
ldClientMock.identify.mockRejectedValue(error);
|
||||
setupContract.identifyUser('user-id', {});
|
||||
expect(ldClientMock.identify).toHaveBeenCalledTimes(1);
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
// await the lazy import
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(error);
|
||||
expect(plugin).toHaveProperty('launchDarklyClient', ldClientMock);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -132,7 +117,7 @@ describe('Cloud Experiments public plugin', () => {
|
|||
});
|
||||
|
||||
test('returns the contract', () => {
|
||||
plugin.setup(coreMock.createSetup());
|
||||
plugin.setup(coreMock.createSetup(), { cloud: cloudMock.createSetup() });
|
||||
const startContract = plugin.start(coreMock.createStart());
|
||||
expect(startContract).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -145,8 +130,9 @@ describe('Cloud Experiments public plugin', () => {
|
|||
describe('getVariation', () => {
|
||||
describe('with the user identified', () => {
|
||||
beforeEach(() => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup());
|
||||
setupContract.identifyUser('user-id', {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('uses the flag overrides to respond early', async () => {
|
||||
|
@ -175,7 +161,9 @@ describe('Cloud Experiments public plugin', () => {
|
|||
|
||||
describe('with the user not identified', () => {
|
||||
beforeEach(() => {
|
||||
plugin.setup(coreMock.createSetup());
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('uses the flag overrides to respond early', async () => {
|
||||
|
@ -202,8 +190,9 @@ describe('Cloud Experiments public plugin', () => {
|
|||
describe('reportMetric', () => {
|
||||
describe('with the user identified', () => {
|
||||
beforeEach(() => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup());
|
||||
setupContract.identifyUser('user-id', {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the track API', () => {
|
||||
|
@ -224,7 +213,9 @@ describe('Cloud Experiments public plugin', () => {
|
|||
|
||||
describe('with the user not identified', () => {
|
||||
beforeEach(() => {
|
||||
plugin.setup(coreMock.createSetup());
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the track API', () => {
|
||||
|
@ -250,8 +241,9 @@ describe('Cloud Experiments public plugin', () => {
|
|||
flag_overrides: { my_flag: '1234' },
|
||||
});
|
||||
plugin = new CloudExperimentsPlugin(initializerContext);
|
||||
const setupContract = plugin.setup(coreMock.createSetup());
|
||||
setupContract.identifyUser('user-id', {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
plugin.start(coreMock.createStart());
|
||||
});
|
||||
|
||||
|
|
|
@ -6,21 +6,26 @@
|
|||
*/
|
||||
|
||||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import LaunchDarkly, { type LDClient } from 'launchdarkly-js-client-sdk';
|
||||
import type { LDClient } from 'launchdarkly-js-client-sdk';
|
||||
import { get, has } from 'lodash';
|
||||
import { Sha256 } from '@kbn/crypto-browser';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import type {
|
||||
CloudExperimentsFeatureFlagNames,
|
||||
CloudExperimentsMetric,
|
||||
CloudExperimentsPluginSetup,
|
||||
CloudExperimentsPluginStart,
|
||||
} from '../common';
|
||||
import { FEATURE_FLAG_NAMES, METRIC_NAMES } from '../common/constants';
|
||||
|
||||
interface CloudExperimentsPluginSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser-side implementation of the Cloud Experiments plugin
|
||||
*/
|
||||
export class CloudExperimentsPlugin
|
||||
implements Plugin<CloudExperimentsPluginSetup, CloudExperimentsPluginStart>
|
||||
implements Plugin<void, CloudExperimentsPluginStart, CloudExperimentsPluginSetupDeps>
|
||||
{
|
||||
private launchDarklyClient?: LDClient;
|
||||
private readonly clientId?: string;
|
||||
|
@ -53,30 +58,32 @@ export class CloudExperimentsPlugin
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the contract {@link CloudExperimentsPluginSetup}
|
||||
* Sets up the A/B testing client only if cloud is enabled
|
||||
* @param core {@link CoreSetup}
|
||||
* @param deps {@link CloudExperimentsPluginSetupDeps}
|
||||
*/
|
||||
public setup(core: CoreSetup): CloudExperimentsPluginSetup {
|
||||
return {
|
||||
identifyUser: (userId, userMetadata) => {
|
||||
if (!this.clientId) return; // Only applies in dev mode.
|
||||
|
||||
if (!this.launchDarklyClient) {
|
||||
// If the client has not been initialized, create it with the user data..
|
||||
public setup(core: CoreSetup, deps: CloudExperimentsPluginSetupDeps) {
|
||||
if (deps.cloud.isCloudEnabled && deps.cloud.cloudId && this.clientId) {
|
||||
import('launchdarkly-js-client-sdk').then(
|
||||
(LaunchDarkly) => {
|
||||
this.launchDarklyClient = LaunchDarkly.initialize(
|
||||
this.clientId,
|
||||
{ key: userId, custom: userMetadata },
|
||||
this.clientId!,
|
||||
{
|
||||
// We use the Hashed Cloud Deployment ID as the userId in the Cloud Experiments
|
||||
key: sha256(deps.cloud.cloudId!),
|
||||
custom: {
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
},
|
||||
},
|
||||
{ application: { id: 'kibana-browser', version: this.kibanaVersion } }
|
||||
);
|
||||
} else {
|
||||
// Otherwise, call the `identify` method.
|
||||
this.launchDarklyClient
|
||||
.identify({ key: userId, custom: userMetadata })
|
||||
// eslint-disable-next-line no-console
|
||||
.catch((err) => console.warn(err));
|
||||
},
|
||||
(err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error setting up LaunchDarkly: ${err.toString()}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,3 +132,7 @@ export class CloudExperimentsPlugin
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
function sha256(str: string) {
|
||||
return new Sha256().update(str, 'utf8').digest('hex');
|
||||
}
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/server/mocks';
|
||||
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';
|
||||
import { ldClientMock } from './plugin.test.mock';
|
||||
import { CloudExperimentsPlugin } from './plugin';
|
||||
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';
|
||||
import { FEATURE_FLAG_NAMES } from '../common/constants';
|
||||
|
||||
describe('Cloud Experiments server plugin', () => {
|
||||
|
@ -16,6 +17,13 @@ describe('Cloud Experiments server plugin', () => {
|
|||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const ldUser = {
|
||||
key: '1c2412b751f056aef6e340efa5637d137442d489a4b1e3117071e7c87f8523f2',
|
||||
custom: {
|
||||
kibanaVersion: coreMock.createPluginInitializerContext().env.packageInfo.version,
|
||||
},
|
||||
};
|
||||
|
||||
describe('constructor', () => {
|
||||
test('successfully creates a new plugin if provided an empty configuration', () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext();
|
||||
|
@ -68,17 +76,19 @@ describe('Cloud Experiments server plugin', () => {
|
|||
});
|
||||
|
||||
test('returns the contract', () => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup(), {});
|
||||
expect(setupContract).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
identifyUser: expect.any(Function),
|
||||
expect(
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: cloudMock.createSetup(),
|
||||
})
|
||||
);
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('registers the usage collector when available', () => {
|
||||
const usageCollection = usageCollectionPluginMock.createSetupContract();
|
||||
plugin.setup(coreMock.createSetup(), { usageCollection });
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: cloudMock.createSetup(),
|
||||
usageCollection,
|
||||
});
|
||||
expect(usageCollection.makeUsageCollector).toHaveBeenCalledTimes(1);
|
||||
expect(usageCollection.registerCollector).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
@ -86,9 +96,9 @@ describe('Cloud Experiments server plugin', () => {
|
|||
describe('identifyUser', () => {
|
||||
test('sets launchDarklyUser and calls identify', () => {
|
||||
expect(plugin).toHaveProperty('launchDarklyUser', undefined);
|
||||
const setupContract = plugin.setup(coreMock.createSetup(), {});
|
||||
setupContract.identifyUser('user-id', {});
|
||||
const ldUser = { key: 'user-id', custom: {} };
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
expect(plugin).toHaveProperty('launchDarklyUser', ldUser);
|
||||
expect(ldClientMock.identify).toHaveBeenCalledWith(ldUser);
|
||||
});
|
||||
|
@ -110,7 +120,7 @@ describe('Cloud Experiments server plugin', () => {
|
|||
});
|
||||
|
||||
test('returns the contract', () => {
|
||||
plugin.setup(coreMock.createSetup(), {});
|
||||
plugin.setup(coreMock.createSetup(), { cloud: cloudMock.createSetup() });
|
||||
const startContract = plugin.start(coreMock.createStart());
|
||||
expect(startContract).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -123,8 +133,9 @@ describe('Cloud Experiments server plugin', () => {
|
|||
describe('getVariation', () => {
|
||||
describe('with the user identified', () => {
|
||||
beforeEach(() => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup(), {});
|
||||
setupContract.identifyUser('user-id', {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('uses the flag overrides to respond early', async () => {
|
||||
|
@ -146,7 +157,7 @@ describe('Cloud Experiments server plugin', () => {
|
|||
).resolves.toStrictEqual('12345');
|
||||
expect(ldClientMock.variation).toHaveBeenCalledWith(
|
||||
undefined, // it couldn't find it in FEATURE_FLAG_NAMES
|
||||
{ key: 'user-id', custom: {} },
|
||||
ldUser,
|
||||
123
|
||||
);
|
||||
});
|
||||
|
@ -154,7 +165,9 @@ describe('Cloud Experiments server plugin', () => {
|
|||
|
||||
describe('with the user not identified', () => {
|
||||
beforeEach(() => {
|
||||
plugin.setup(coreMock.createSetup(), {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('uses the flag overrides to respond early', async () => {
|
||||
|
@ -181,8 +194,9 @@ describe('Cloud Experiments server plugin', () => {
|
|||
describe('reportMetric', () => {
|
||||
describe('with the user identified', () => {
|
||||
beforeEach(() => {
|
||||
const setupContract = plugin.setup(coreMock.createSetup(), {});
|
||||
setupContract.identifyUser('user-id', {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the track API', () => {
|
||||
|
@ -195,7 +209,7 @@ describe('Cloud Experiments server plugin', () => {
|
|||
});
|
||||
expect(ldClientMock.track).toHaveBeenCalledWith(
|
||||
undefined, // it couldn't find it in METRIC_NAMES
|
||||
{ key: 'user-id', custom: {} },
|
||||
ldUser,
|
||||
{},
|
||||
1
|
||||
);
|
||||
|
@ -204,7 +218,9 @@ describe('Cloud Experiments server plugin', () => {
|
|||
|
||||
describe('with the user not identified', () => {
|
||||
beforeEach(() => {
|
||||
plugin.setup(coreMock.createSetup(), {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the track API', () => {
|
||||
|
@ -231,7 +247,9 @@ describe('Cloud Experiments server plugin', () => {
|
|||
});
|
||||
ldClientMock.waitForInitialization.mockResolvedValue(ldClientMock);
|
||||
plugin = new CloudExperimentsPlugin(initializerContext);
|
||||
plugin.setup(coreMock.createSetup(), {});
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
plugin.start(coreMock.createStart());
|
||||
});
|
||||
|
||||
|
|
|
@ -14,36 +14,33 @@ import type {
|
|||
} from '@kbn/core/server';
|
||||
import { get, has } from 'lodash';
|
||||
import LaunchDarkly, { type LDClient, type LDUser } from 'launchdarkly-node-server-sdk';
|
||||
import { createSHA256Hash } from '@kbn/crypto';
|
||||
import type { LogMeta } from '@kbn/logging';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import { registerUsageCollector } from './usage';
|
||||
import type { CloudExperimentsConfigType } from './config';
|
||||
import type {
|
||||
CloudExperimentsFeatureFlagNames,
|
||||
CloudExperimentsMetric,
|
||||
CloudExperimentsPluginSetup,
|
||||
CloudExperimentsPluginStart,
|
||||
} from '../common';
|
||||
import { FEATURE_FLAG_NAMES, METRIC_NAMES } from '../common/constants';
|
||||
|
||||
interface CloudExperimentsPluginSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export class CloudExperimentsPlugin
|
||||
implements
|
||||
Plugin<
|
||||
CloudExperimentsPluginSetup,
|
||||
CloudExperimentsPluginStart,
|
||||
CloudExperimentsPluginSetupDeps
|
||||
>
|
||||
implements Plugin<void, CloudExperimentsPluginStart, CloudExperimentsPluginSetupDeps>
|
||||
{
|
||||
private readonly logger: Logger;
|
||||
private readonly launchDarklyClient?: LDClient;
|
||||
private readonly flagOverrides?: Record<string, unknown>;
|
||||
private launchDarklyUser: LDUser | undefined;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
const config = initializerContext.config.get<CloudExperimentsConfigType>();
|
||||
if (config.flag_overrides) {
|
||||
|
@ -73,10 +70,7 @@ export class CloudExperimentsPlugin
|
|||
}
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
deps: CloudExperimentsPluginSetupDeps
|
||||
): CloudExperimentsPluginSetup {
|
||||
public setup(core: CoreSetup, deps: CloudExperimentsPluginSetupDeps) {
|
||||
if (deps.usageCollection) {
|
||||
registerUsageCollector(deps.usageCollection, () => ({
|
||||
launchDarklyClient: this.launchDarklyClient,
|
||||
|
@ -84,12 +78,17 @@ export class CloudExperimentsPlugin
|
|||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
identifyUser: (userId, userMetadata) => {
|
||||
this.launchDarklyUser = { key: userId, custom: userMetadata };
|
||||
this.launchDarklyClient?.identify(this.launchDarklyUser!);
|
||||
},
|
||||
};
|
||||
if (deps.cloud.isCloudEnabled && deps.cloud.cloudId) {
|
||||
this.launchDarklyUser = {
|
||||
// We use the Cloud ID as the userId in the Cloud Experiments
|
||||
key: createSHA256Hash(deps.cloud.cloudId),
|
||||
custom: {
|
||||
// This list of deployment metadata will likely grow in future versions
|
||||
kibanaVersion: this.initializerContext.env.packageInfo.version,
|
||||
},
|
||||
};
|
||||
this.launchDarklyClient?.identify(this.launchDarklyUser);
|
||||
}
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
|
|
|
@ -16,5 +16,6 @@
|
|||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../../cloud/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
|
|
7
x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json
Executable file
7
x-pack/plugins/cloud_integrations/cloud_full_story/.i18nrc.json
Executable file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"prefix": "cloudFullStory",
|
||||
"paths": {
|
||||
"cloudFullStory": "."
|
||||
},
|
||||
"translations": ["translations/ja-JP.json"]
|
||||
}
|
3
x-pack/plugins/cloud_integrations/cloud_full_story/README.md
Executable file
3
x-pack/plugins/cloud_integrations/cloud_full_story/README.md
Executable file
|
@ -0,0 +1,3 @@
|
|||
# Cloud FullStory
|
||||
|
||||
Integrates with FullStory in order to provide better product analytics, so we can understand how our users make use of Kibana. This plugin should only run on Elastic Cloud.
|
|
@ -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_full_story'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_full_story',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/cloud_integrations/cloud_full_story/{common,public,server}/**/*.{ts,tsx}',
|
||||
],
|
||||
};
|
15
x-pack/plugins/cloud_integrations/cloud_full_story/kibana.json
Executable file
15
x-pack/plugins/cloud_integrations/cloud_full_story/kibana.json
Executable file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "cloudFullStory",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"owner": {
|
||||
"name": "Kibana Core",
|
||||
"githubTeam": "kibana-core"
|
||||
},
|
||||
"description": "When Kibana runs on Elastic Cloud, this plugin registers FullStory as a shipper for telemetry.",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"configPath": ["xpack", "cloud_integrations", "full_story"],
|
||||
"requiredPlugins": ["cloud"],
|
||||
"optionalPlugins": ["security"]
|
||||
}
|
13
x-pack/plugins/cloud_integrations/cloud_full_story/public/index.ts
Executable file
13
x-pack/plugins/cloud_integrations/cloud_full_story/public/index.ts
Executable file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { CloudFullStoryPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudFullStoryPlugin(initializerContext);
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { CloudFullStoryConfigType } from '../server/config';
|
||||
import { CloudFullStoryPlugin } from './plugin';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
|
||||
describe('Cloud Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
describe('setupFullStory', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupPlugin = async ({
|
||||
config = {},
|
||||
isCloudEnabled = true,
|
||||
}: {
|
||||
config?: Partial<CloudFullStoryConfigType>;
|
||||
isCloudEnabled?: boolean;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext(config);
|
||||
|
||||
const plugin = new CloudFullStoryPlugin(initContext);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
||||
const cloud = { ...cloudMock.createSetup(), isCloudEnabled };
|
||||
|
||||
plugin.setup(coreSetup, { cloud });
|
||||
|
||||
// Wait for FullStory dynamic import to resolve
|
||||
await new Promise((r) => setImmediate(r));
|
||||
|
||||
return { initContext, plugin, coreSetup };
|
||||
};
|
||||
|
||||
test('register the shipper FullStory with correct args when enabled and org_id are set', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { org_id: 'foo' },
|
||||
});
|
||||
|
||||
expect(coreSetup.analytics.registerShipper).toHaveBeenCalled();
|
||||
expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
|
||||
fullStoryOrgId: 'foo',
|
||||
scriptUrl: '/internal/cloud/100/fullstory.js',
|
||||
namespace: 'FSKibana',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call initializeFullStory when isCloudEnabled=false', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { org_id: 'foo' },
|
||||
isCloudEnabled: false,
|
||||
});
|
||||
expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call initializeFullStory when org_id is undefined', async () => {
|
||||
const { coreSetup } = await setupPlugin({ config: {} });
|
||||
expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
75
x-pack/plugins/cloud_integrations/cloud_full_story/public/plugin.ts
Executable file
75
x-pack/plugins/cloud_integrations/cloud_full_story/public/plugin.ts
Executable file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 {
|
||||
AnalyticsServiceSetup,
|
||||
IBasePath,
|
||||
PluginInitializerContext,
|
||||
CoreSetup,
|
||||
Plugin,
|
||||
} from '@kbn/core/public';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
|
||||
interface SetupFullStoryDeps {
|
||||
analytics: AnalyticsServiceSetup;
|
||||
basePath: IBasePath;
|
||||
}
|
||||
|
||||
interface CloudFullStoryConfig {
|
||||
org_id?: string;
|
||||
eventTypesAllowlist: string[];
|
||||
}
|
||||
|
||||
interface CloudFullStorySetupDeps {
|
||||
cloud: CloudSetup;
|
||||
}
|
||||
|
||||
export class CloudFullStoryPlugin implements Plugin {
|
||||
private readonly config: CloudFullStoryConfig;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<CloudFullStoryConfig>();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, { cloud }: CloudFullStorySetupDeps) {
|
||||
if (cloud.isCloudEnabled) {
|
||||
this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error setting up FullStory: ${e.toString()}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
|
||||
/**
|
||||
* If the right config is provided, register the FullStory shipper to the analytics client.
|
||||
* @param analytics Core's Analytics service's setup contract.
|
||||
* @param basePath Core's http.basePath helper.
|
||||
* @private
|
||||
*/
|
||||
private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) {
|
||||
const { org_id: fullStoryOrgId, eventTypesAllowlist } = this.config;
|
||||
if (!fullStoryOrgId) {
|
||||
return; // do not load any FullStory code in the browser if not enabled
|
||||
}
|
||||
|
||||
// Keep this import async so that we do not load any FullStory code into the browser when it is disabled.
|
||||
const { FullStoryShipper } = await import('@kbn/analytics-shippers-fullstory');
|
||||
analytics.registerShipper(FullStoryShipper, {
|
||||
eventTypesAllowlist,
|
||||
fullStoryOrgId,
|
||||
// Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN.
|
||||
scriptUrl: basePath.prepend(
|
||||
`/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js`
|
||||
),
|
||||
namespace: 'FSKibana',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -10,28 +10,22 @@ import { config } from './config';
|
|||
describe('xpack.cloud config', () => {
|
||||
describe('full_story', () => {
|
||||
it('allows org_id when enabled: false', () => {
|
||||
expect(() =>
|
||||
config.schema.validate({ full_story: { enabled: false, org_id: 'asdf' } })
|
||||
).not.toThrow();
|
||||
expect(() => config.schema.validate({ enabled: false, org_id: 'asdf' })).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects undefined or empty org_id when enabled: true', () => {
|
||||
expect(() =>
|
||||
config.schema.validate({ full_story: { enabled: true } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[full_story.org_id]: expected value of type [string] but got [undefined]"`
|
||||
expect(() => config.schema.validate({ enabled: true })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[org_id]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
expect(() =>
|
||||
config.schema.validate({ full_story: { enabled: true, org_id: '' } })
|
||||
config.schema.validate({ enabled: true, org_id: '' })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[full_story.org_id]: value has length [0] but it must have a minimum length of [1]."`
|
||||
`"[org_id]: value has length [0] but it must have a minimum length of [1]."`
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts org_id when enabled: true', () => {
|
||||
expect(() =>
|
||||
config.schema.validate({ full_story: { enabled: true, org_id: 'asdf' } })
|
||||
).not.toThrow();
|
||||
expect(() => config.schema.validate({ enabled: true, org_id: 'asdf' })).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
import { get, has } from 'lodash';
|
||||
|
||||
const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
org_id: schema.conditional(
|
||||
schema.siblingRef('enabled'),
|
||||
true,
|
||||
schema.string({ minLength: 1 }),
|
||||
schema.maybe(schema.string())
|
||||
),
|
||||
eventTypesAllowlist: schema.arrayOf(schema.string(), {
|
||||
defaultValue: ['Loaded Kibana'],
|
||||
}),
|
||||
});
|
||||
|
||||
export type CloudFullStoryConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<CloudFullStoryConfigType> = {
|
||||
exposeToBrowser: {
|
||||
org_id: true,
|
||||
eventTypesAllowlist: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
deprecations: () => [
|
||||
// Silently move the chat configuration from `xpack.cloud` to `xpack.cloud_integrations.full_story`.
|
||||
// No need to emit a deprecation log because it's an internal restructure
|
||||
(cfg) => {
|
||||
return {
|
||||
set: [
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.full_story.enabled',
|
||||
toKey: 'xpack.cloud_integrations.full_story.enabled',
|
||||
}),
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.full_story.org_id',
|
||||
toKey: 'xpack.cloud_integrations.full_story.org_id',
|
||||
}),
|
||||
...copyIfExists({
|
||||
cfg,
|
||||
fromKey: 'xpack.cloud.full_story.eventTypesAllowlist',
|
||||
toKey: 'xpack.cloud_integrations.full_story.eventTypesAllowlist',
|
||||
}),
|
||||
],
|
||||
unset: [
|
||||
{ path: 'xpack.cloud.full_story.enabled' },
|
||||
{ path: 'xpack.cloud.full_story.org_id' },
|
||||
{ path: 'xpack.cloud.full_story.eventTypesAllowlist' },
|
||||
],
|
||||
};
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the `set` action only if the key exists in the `fromKey` value.
|
||||
* This is to avoid overwriting actual values with undefined.
|
||||
* @param cfg The config object
|
||||
* @param fromKey The key to copy from.
|
||||
* @param toKey The key where the value should be copied to.
|
||||
*/
|
||||
function copyIfExists({
|
||||
cfg,
|
||||
fromKey,
|
||||
toKey,
|
||||
}: {
|
||||
cfg: Readonly<{ [p: string]: unknown }>;
|
||||
fromKey: string;
|
||||
toKey: string;
|
||||
}) {
|
||||
return has(cfg, fromKey) ? [{ path: toKey, value: get(cfg, fromKey) }] : [];
|
||||
}
|
15
x-pack/plugins/cloud_integrations/cloud_full_story/server/index.ts
Executable file
15
x-pack/plugins/cloud_integrations/cloud_full_story/server/index.ts
Executable 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 { PluginInitializerContext } from '@kbn/core/server';
|
||||
import { CloudFullStoryPlugin } from './plugin';
|
||||
|
||||
export { config } from './config';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudFullStoryPlugin(initializerContext);
|
||||
}
|
|
@ -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 const registerFullStoryRouteMock = jest.fn();
|
||||
|
||||
jest.doMock('./routes', () => ({
|
||||
registerFullStoryRoute: registerFullStoryRouteMock,
|
||||
}));
|
|
@ -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 { coreMock } from '@kbn/core/server/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/server/mocks';
|
||||
import { registerFullStoryRouteMock } from './plugin.test.mock';
|
||||
import { CloudFullStoryPlugin } from './plugin';
|
||||
|
||||
describe('Cloud FullStory plugin', () => {
|
||||
let plugin: CloudFullStoryPlugin;
|
||||
beforeEach(() => {
|
||||
registerFullStoryRouteMock.mockReset();
|
||||
plugin = new CloudFullStoryPlugin(coreMock.createPluginInitializerContext());
|
||||
});
|
||||
|
||||
test('registers route when cloud is enabled', () => {
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
|
||||
});
|
||||
expect(registerFullStoryRouteMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('does not register the route when cloud is disabled', () => {
|
||||
plugin.setup(coreMock.createSetup(), {
|
||||
cloud: { ...cloudMock.createSetup(), isCloudEnabled: false },
|
||||
});
|
||||
expect(registerFullStoryRouteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
32
x-pack/plugins/cloud_integrations/cloud_full_story/server/plugin.ts
Executable file
32
x-pack/plugins/cloud_integrations/cloud_full_story/server/plugin.ts
Executable file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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, CoreSetup, Plugin } from '@kbn/core/server';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
|
||||
import { registerFullStoryRoute } from './routes';
|
||||
|
||||
interface CloudFullStorySetupDeps {
|
||||
cloud: CloudSetup;
|
||||
}
|
||||
|
||||
export class CloudFullStoryPlugin implements Plugin {
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup, { cloud }: CloudFullStorySetupDeps) {
|
||||
if (cloud.isCloudEnabled) {
|
||||
registerFullStoryRoute({
|
||||
httpResources: core.http.resources,
|
||||
packageInfo: this.initializerContext.env.packageInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -40,7 +40,7 @@ export const renderFullStoryLibraryFactory = (dist = true) =>
|
|||
}
|
||||
);
|
||||
|
||||
export const registerFullstoryRoute = ({
|
||||
export const registerFullStoryRoute = ({
|
||||
httpResources,
|
||||
packageInfo,
|
||||
}: {
|
|
@ -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 { registerFullStoryRoute } from './fullstory';
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
},
|
||||
"include": [
|
||||
".storybook/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../cloud/tsconfig.json" },
|
||||
]
|
||||
}
|
3
x-pack/plugins/cloud_integrations/cloud_links/README.md
Executable file
3
x-pack/plugins/cloud_integrations/cloud_links/README.md
Executable file
|
@ -0,0 +1,3 @@
|
|||
# Cloud Links
|
||||
|
||||
Adds all the links to the Elastic Cloud console.
|
18
x-pack/plugins/cloud_integrations/cloud_links/jest.config.js
Normal file
18
x-pack/plugins/cloud_integrations/cloud_links/jest.config.js
Normal 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_links'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_links',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/cloud_integrations/cloud_links/{common,public,server}/**/*.{ts,tsx}',
|
||||
],
|
||||
};
|
14
x-pack/plugins/cloud_integrations/cloud_links/kibana.json
Executable file
14
x-pack/plugins/cloud_integrations/cloud_links/kibana.json
Executable file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "cloudLinks",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"owner": {
|
||||
"name": "Kibana Core",
|
||||
"githubTeam": "@kibana-core"
|
||||
},
|
||||
"description": "Adds the links to the Elastic Cloud console",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": [],
|
||||
"optionalPlugins": ["cloud", "security"]
|
||||
}
|
12
x-pack/plugins/cloud_integrations/cloud_links/public/index.ts
Executable file
12
x-pack/plugins/cloud_integrations/cloud_links/public/index.ts
Executable file
|
@ -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.
|
||||
*/
|
||||
|
||||
import { CloudLinksPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new CloudLinksPlugin();
|
||||
}
|
|
@ -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 { maybeAddCloudLinks } from './maybe_add_cloud_links';
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
|
||||
import { maybeAddCloudLinks } from './maybe_add_cloud_links';
|
||||
|
||||
describe('maybeAddCloudLinks', () => {
|
||||
it('should skip if cloud is disabled', async () => {
|
||||
const security = securityMock.createStart();
|
||||
maybeAddCloudLinks({
|
||||
security,
|
||||
chrome: coreMock.createStart().chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: false },
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
expect(security.authc.getCurrentUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('when cloud enabled and the user is an Elastic Cloud user, it sets the links', async () => {
|
||||
const security = securityMock.createStart();
|
||||
security.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({ elastic_cloud_user: true })
|
||||
);
|
||||
const chrome = coreMock.createStart().chrome;
|
||||
maybeAddCloudLinks({
|
||||
security,
|
||||
chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
expect(security.authc.getCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"euiIconType": "logoCloud",
|
||||
"href": "deployment-url",
|
||||
"title": "Manage this deployment",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(security.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(security.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "profile-url",
|
||||
"iconType": "user",
|
||||
"label": "Edit profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "organization-url",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('when cloud enabled and it fails to fetch the user, it sets the links', async () => {
|
||||
const security = securityMock.createStart();
|
||||
security.authc.getCurrentUser.mockRejectedValue(new Error('Something went terribly wrong'));
|
||||
const chrome = coreMock.createStart().chrome;
|
||||
maybeAddCloudLinks({
|
||||
security,
|
||||
chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
expect(security.authc.getCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"euiIconType": "logoCloud",
|
||||
"href": "deployment-url",
|
||||
"title": "Manage this deployment",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(security.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(security.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "profile-url",
|
||||
"iconType": "user",
|
||||
"label": "Edit profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "organization-url",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('when cloud enabled and the user is NOT an Elastic Cloud user, it does not set the links', async () => {
|
||||
const security = securityMock.createStart();
|
||||
security.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({ elastic_cloud_user: false })
|
||||
);
|
||||
const chrome = coreMock.createStart().chrome;
|
||||
maybeAddCloudLinks({
|
||||
security,
|
||||
chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
expect(security.authc.getCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setCustomNavLink).not.toHaveBeenCalled();
|
||||
expect(security.navControlService.addUserMenuLinks).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { catchError, defer, filter, map, of } from 'rxjs';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { ChromeStart } from '@kbn/core/public';
|
||||
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
|
||||
import { createUserMenuLinks } from './user_menu_links';
|
||||
|
||||
export interface MaybeAddCloudLinksDeps {
|
||||
security: SecurityPluginStart;
|
||||
chrome: ChromeStart;
|
||||
cloud: CloudStart;
|
||||
}
|
||||
|
||||
export function maybeAddCloudLinks({ security, chrome, cloud }: MaybeAddCloudLinksDeps): void {
|
||||
if (cloud.isCloudEnabled) {
|
||||
defer(() => security.authc.getCurrentUser())
|
||||
.pipe(
|
||||
// Check if user is a cloud user.
|
||||
map((user) => user.elastic_cloud_user),
|
||||
// If user is not defined due to an unexpected error, then fail *open*.
|
||||
catchError(() => of(true)),
|
||||
filter((isElasticCloudUser) => isElasticCloudUser === true),
|
||||
map(() => {
|
||||
if (cloud.deploymentUrl) {
|
||||
chrome.setCustomNavLink({
|
||||
title: i18n.translate('xpack.cloudLinks.deploymentLinkLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
}),
|
||||
euiIconType: 'logoCloud',
|
||||
href: cloud.deploymentUrl,
|
||||
});
|
||||
}
|
||||
const userMenuLinks = createUserMenuLinks(cloud);
|
||||
security.navControlService.addUserMenuLinks(userMenuLinks);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
}
|
|
@ -6,33 +6,32 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UserMenuLink } from '@kbn/security-plugin/public';
|
||||
import { CloudConfigType } from '.';
|
||||
import { getFullCloudUrl } from './utils';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { UserMenuLink } from '@kbn/security-plugin/public';
|
||||
|
||||
export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => {
|
||||
const { profile_url: profileUrl, organization_url: organizationUrl, base_url: baseUrl } = config;
|
||||
export const createUserMenuLinks = (cloud: CloudStart): UserMenuLink[] => {
|
||||
const { profileUrl, organizationUrl } = cloud;
|
||||
const userMenuLinks = [] as UserMenuLink[];
|
||||
|
||||
if (baseUrl && profileUrl) {
|
||||
if (profileUrl) {
|
||||
userMenuLinks.push({
|
||||
label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', {
|
||||
label: i18n.translate('xpack.cloudLinks.userMenuLinks.profileLinkText', {
|
||||
defaultMessage: 'Edit profile',
|
||||
}),
|
||||
iconType: 'user',
|
||||
href: getFullCloudUrl(baseUrl, profileUrl),
|
||||
href: profileUrl,
|
||||
order: 100,
|
||||
setAsProfile: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (baseUrl && organizationUrl) {
|
||||
if (organizationUrl) {
|
||||
userMenuLinks.push({
|
||||
label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', {
|
||||
label: i18n.translate('xpack.cloudLinks.userMenuLinks.accountLinkText', {
|
||||
defaultMessage: 'Account & Billing',
|
||||
}),
|
||||
iconType: 'gear',
|
||||
href: getFullCloudUrl(baseUrl, organizationUrl),
|
||||
href: organizationUrl,
|
||||
order: 200,
|
||||
});
|
||||
}
|
|
@ -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 const maybeAddCloudLinksMock = jest.fn();
|
||||
|
||||
jest.doMock('./maybe_add_cloud_links', () => ({
|
||||
maybeAddCloudLinks: maybeAddCloudLinksMock,
|
||||
}));
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { maybeAddCloudLinksMock } from './plugin.test.mocks';
|
||||
import { CloudLinksPlugin } from './plugin';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
|
||||
describe('Cloud Links Plugin - public', () => {
|
||||
let plugin: CloudLinksPlugin;
|
||||
|
||||
beforeEach(() => {
|
||||
plugin = new CloudLinksPlugin();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
maybeAddCloudLinksMock.mockReset();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
beforeEach(() => {
|
||||
plugin.setup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
plugin.stop();
|
||||
});
|
||||
|
||||
test('calls maybeAddCloudLinks when cloud and security are enabled and it is an authenticated page', () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
|
||||
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
|
||||
const security = securityMock.createStart();
|
||||
plugin.start(coreStart, { cloud, security });
|
||||
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('does not call maybeAddCloudLinks when security is disabled', () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
|
||||
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
|
||||
plugin.start(coreStart, { cloud });
|
||||
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('does not call maybeAddCloudLinks when the page is anonymous', () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
|
||||
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
|
||||
const security = securityMock.createStart();
|
||||
plugin.start(coreStart, { cloud, security });
|
||||
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('does not call maybeAddCloudLinks when cloud is disabled', () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
|
||||
const security = securityMock.createStart();
|
||||
plugin.start(coreStart, { security });
|
||||
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('does not call maybeAddCloudLinks when isCloudEnabled is false', () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
|
||||
const cloud = { ...cloudMock.createStart(), isCloudEnabled: false };
|
||||
const security = securityMock.createStart();
|
||||
plugin.start(coreStart, { cloud, security });
|
||||
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
39
x-pack/plugins/cloud_integrations/cloud_links/public/plugin.ts
Executable file
39
x-pack/plugins/cloud_integrations/cloud_links/public/plugin.ts
Executable file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { CoreStart, Plugin } from '@kbn/core/public';
|
||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import { maybeAddCloudLinks } from './maybe_add_cloud_links';
|
||||
|
||||
interface CloudLinksDepsSetup {
|
||||
cloud?: CloudSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
interface CloudLinksDepsStart {
|
||||
cloud?: CloudStart;
|
||||
security?: SecurityPluginStart;
|
||||
}
|
||||
|
||||
export class CloudLinksPlugin
|
||||
implements Plugin<void, void, CloudLinksDepsSetup, CloudLinksDepsStart>
|
||||
{
|
||||
public setup() {}
|
||||
|
||||
public start(core: CoreStart, { cloud, security }: CloudLinksDepsStart) {
|
||||
if (
|
||||
cloud?.isCloudEnabled &&
|
||||
security &&
|
||||
!core.http.anonymousPaths.isAnonymous(window.location.pathname)
|
||||
) {
|
||||
maybeAddCloudLinks({ security, chrome: core.chrome, cloud });
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
21
x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json
Normal file
21
x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
},
|
||||
"include": [
|
||||
".storybook/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../cloud/tsconfig.json" },
|
||||
{ "path": "../../security/tsconfig.json" },
|
||||
]
|
||||
}
|
|
@ -31,7 +31,7 @@
|
|||
"fieldFormats",
|
||||
"uiActions",
|
||||
"lens",
|
||||
"cloud"
|
||||
"cloudChat"
|
||||
],
|
||||
"owner": {
|
||||
"name": "Machine Learning UI",
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
processResults,
|
||||
} from '../../../common/components/utils';
|
||||
|
||||
import { Chat } from '@kbn/cloud-plugin/public';
|
||||
import { Chat } from '@kbn/cloud-chat-plugin/public';
|
||||
|
||||
import { MODE } from './constants';
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/unified_search/tsconfig.json" },
|
||||
{ "path": "../cloud/tsconfig.json" },
|
||||
{ "path": "../cloud_integrations/cloud_chat/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/embeddable/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"requiredPlugins": ["features", "spaces", "security", "licensing", "data", "charts", "infra", "cloud", "esUiShared"],
|
||||
"configPath": ["enterpriseSearch"],
|
||||
"optionalPlugins": ["usageCollection", "home", "cloud", "customIntegrations"],
|
||||
"optionalPlugins": ["usageCollection", "home", "customIntegrations"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredBundles": ["kibanaReact"],
|
||||
"requiredBundles": ["kibanaReact", "cloudChat"],
|
||||
"owner": {
|
||||
"name": "Enterprise Search",
|
||||
"githubTeam": "enterprise-search-frontend"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
import { Capabilities } from '@kbn/core/public';
|
||||
|
||||
|
@ -18,6 +19,7 @@ export const mockKibanaValues = {
|
|||
config: { host: 'http://localhost:3002' },
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
cloud: {
|
||||
...cloudMock.createSetup(),
|
||||
isCloudEnabled: false,
|
||||
deployment_url: 'https://cloud.elastic.co/deployments/some-id',
|
||||
},
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { Chat } from '@kbn/cloud-plugin/public';
|
||||
import { Chat } from '@kbn/cloud-chat-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../cloud/tsconfig.json" },
|
||||
{ "path": "../cloud_integrations/cloud_chat/tsconfig.json" },
|
||||
{ "path": "../infra/tsconfig.json" },
|
||||
{ "path": "../features/tsconfig.json" },
|
||||
{ "path": "../licensing/tsconfig.json" },
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue