Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c255570
Add package for creating integration
Kerry350 Aug 16, 2023
c229a91
Update layout
Kerry350 Sep 6, 2023
83ad641
[CI] Auto-commit changed files from 'node scripts/generate codeowners'
kibanamachine Sep 6, 2023
f0c45f3
Add version to Fleet APIs and undo deepmerge on initial state
Kerry350 Sep 6, 2023
8e34d99
Merge branch '163788-extract-custom-integration-creation-to-package' …
Kerry350 Sep 6, 2023
0805303
Merge remote-tracking branch 'upstream/main' into 163788-extract-cust…
Kerry350 Sep 6, 2023
056b306
Tidy up events best as possible, fix delete previous
Kerry350 Sep 6, 2023
8f1b258
Shift responsibility of replacing special characters to the machine a…
Kerry350 Sep 7, 2023
8fb83d9
Fix falsey check and add client character limit validation
Kerry350 Sep 7, 2023
9d05451
Merge remote-tracking branch 'upstream/main' into 163788-extract-cust…
Kerry350 Sep 8, 2023
c247146
Make errorOnFailedCleanup an option and add additional notification e…
Kerry350 Sep 8, 2023
2f319f6
Remove memo whilst we have the one mode
Kerry350 Sep 8, 2023
2e0910a
Ensure previously created integration is cleared on successful cleanu…
Kerry350 Sep 8, 2023
d926310
Small amends
Kerry350 Sep 11, 2023
44b28de
Merge branch 'main' into 163788-extract-custom-integration-creation-t…
yngrdyn Sep 12, 2023
bcf2f1b
Merge remote-tracking branch 'upstream/main' into 163788-extract-cust…
Kerry350 Sep 12, 2023
f7a2da8
Fix e2e tests
Kerry350 Sep 12, 2023
6c10502
Merge branch '163788-extract-custom-integration-creation-to-package' …
Kerry350 Sep 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ x-pack/plugins/cross_cluster_replication @elastic/platform-deployment-management
packages/kbn-crypto @elastic/kibana-security
packages/kbn-crypto-browser @elastic/kibana-core
x-pack/plugins/custom_branding @elastic/appex-sharedux
packages/kbn-custom-integrations @elastic/infra-monitoring-ui
src/plugins/custom_integrations @elastic/fleet
packages/kbn-cypress-config @elastic/kibana-operations
x-pack/plugins/dashboard_enhanced @elastic/kibana-presentation
Expand Down Expand Up @@ -806,6 +807,7 @@ src/plugins/visualizations @elastic/kibana-visualizations
x-pack/plugins/watcher @elastic/platform-deployment-management
packages/kbn-web-worker-stub @elastic/kibana-operations
packages/kbn-whereis-pkg-cli @elastic/kibana-operations
packages/kbn-xstate-utils @elastic/infra-monitoring-ui
packages/kbn-yarn-lock-validator @elastic/kibana-operations
####
## Everything below this line overrides the default assignments for each package.
Expand Down
1 change: 1 addition & 0 deletions .i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"packages/core"
],
"customIntegrations": "src/plugins/custom_integrations",
"customIntegrationsPackage": "packages/kbn-custom-integrations",
"dashboard": "src/plugins/dashboard",
"domDragDrop": "packages/kbn-dom-drag-drop",
"controls": "src/plugins/controls",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@
"@kbn/crypto": "link:packages/kbn-crypto",
"@kbn/crypto-browser": "link:packages/kbn-crypto-browser",
"@kbn/custom-branding-plugin": "link:x-pack/plugins/custom_branding",
"@kbn/custom-integrations": "link:packages/kbn-custom-integrations",
"@kbn/custom-integrations-plugin": "link:src/plugins/custom_integrations",
"@kbn/dashboard-enhanced-plugin": "link:x-pack/plugins/dashboard_enhanced",
"@kbn/dashboard-plugin": "link:src/plugins/dashboard",
Expand Down Expand Up @@ -795,6 +796,7 @@
"@kbn/visualization-ui-components": "link:packages/kbn-visualization-ui-components",
"@kbn/visualizations-plugin": "link:src/plugins/visualizations",
"@kbn/watcher-plugin": "link:x-pack/plugins/watcher",
"@kbn/xstate-utils": "link:packages/kbn-xstate-utils",
"@loaders.gl/core": "^3.4.7",
"@loaders.gl/json": "^3.4.7",
"@loaders.gl/shapefile": "^3.4.7",
Expand Down
90 changes: 90 additions & 0 deletions packages/kbn-custom-integrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Custom integrations package

This package provides UI components and state machines to assist with the creation (and in the future other operations) of custom integrations. For consumers the process *should* be as simple as dropping in the provider and connected components.

## Basic / quickstart usage

1. Add provider

```ts
<CustomIntegrationsProvider
services={{ http }}
onIntegrationCreation={onIntegrationCreation}
initialState={{
mode: 'create',
fields: {
integrationName,
datasets: [{ name: datasetName, type: 'logs' as const }],
},
previouslyCreatedIntegration: lastCreatedIntegrationOptions,
}}
>
<ConfigureLogsContent />
</CustomIntegrationsProvider>
```

2. Include Connected form and button components

```ts
<ConnectedCustomIntegrationsForm />
```

The form will internally interact with the backing state machines.

```ts
<ConnectedCustomIntegrationsButton
isDisabled={logFilePathNotConfigured || !namespace}
onClick={onContinue}
/>
```

Most props are optional, here for example you may conditionally add an extra set of `isDisabled` conditions. They will be applied on top of the internal state machine conditions that ensure the button is disabled when necessary. TypeScript types can be checked for available options.

## Initial state

Initial state is just that, initial state, and isn't "reactive".

## Provider callbacks

The provider accepts some callbacks, for example `onIntegrationCreation`. Changes to these references are tracked internally, so feel free to have a callback handler that changes it's identity if needed.

An example handler:

```ts
const onIntegrationCreation: OnIntegrationCreationCallback = (
integrationOptions
) => {
const {
integrationName: createdIntegrationName,
datasets: createdDatasets,
} = integrationOptions;

setState((state) => ({
...state,
integrationName: createdIntegrationName,
datasetName: createdDatasets[0].name,
lastCreatedIntegrationOptions: integrationOptions,
}));
goToStep('installElasticAgent');
};
```

## Manual dispatching of events

Sometimes you may have a flow where it is necessary to manually update the internal state machines and bypass the connected components. This is discouraged, but it is possible for some operations. These events are exposed as `DispatchableEvents`, and these are exposed by the `useConsumerCustomIntegrations()` hook.

For example `updateCreateFields` will update the fields of the creation form in the same manner as the UI components would.

These functions will either exist, or be `undefined`, the presence of these functions means that the corresponding state checks against the machine have already passed. For instance, `saveCreateFields()` will only exist (and not be `undefined`) when the creation form is valid. These functions therefore also fulfill the role of condition checking if needed.

Example usage:

```ts
const {
dispatchableEvents: { updateCreateFields },
} = useConsumerCustomIntegrations();
```

## Cleanup

- For the create flow the machine will try to cleanup a previously created integration if needed (if `options.deletePrevious` is `true`). For example, imagine a wizard flow where someone has navigated forward, then navigates back, makes a change, and saves again, the machine will attempt to delete the previously created integration so that lots of rogue custom integrations aren't left behind. The provider accepts an optional `previouslyCreatedIntegration` prop that can serve as initial state.
19 changes: 19 additions & 0 deletions packages/kbn-custom-integrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export {
ConnectedCustomIntegrationsForm,
ConnectedCustomIntegrationsButton,
} from './src/components';
export { useConsumerCustomIntegrations, useCustomIntegrations } from './src/hooks';
export { CustomIntegrationsProvider } from './src/state_machines';

// Types
export type { DispatchableEvents } from './src/hooks';
export type { Callbacks, InitialState } from './src/state_machines';
export type { CustomIntegrationOptions } from './src/types';
13 changes: 13 additions & 0 deletions packages/kbn-custom-integrations/jest.config.js
Original file line number Diff line number Diff line change
@@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-custom-integrations'],
};
5 changes: 5 additions & 0 deletions packages/kbn-custom-integrations/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/custom-integrations",
"owner": "@elastic/infra-monitoring-ui"
}
6 changes: 6 additions & 0 deletions packages/kbn-custom-integrations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@kbn/custom-integrations",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}
91 changes: 91 additions & 0 deletions packages/kbn-custom-integrations/src/components/create/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useActor, useSelector } from '@xstate/react';
import React, { useCallback } from 'react';
import { isSubmittingSelector, isValidSelector } from '../../state_machines/create/selectors';
import { CreateCustomIntegrationActorRef } from '../../state_machines/create/state_machine';

const SUBMITTING_TEXT = i18n.translate('customIntegrationsPackage.create.button.submitting', {
defaultMessage: 'Creating integration...',
});

const CONTINUE_TEXT = i18n.translate('customIntegrationsPackage.create.button.continue', {
defaultMessage: 'Continue',
});

interface ConnectedCreateCustomIntegrationButtonProps {
machine: CreateCustomIntegrationActorRef;
isDisabled?: boolean;
onClick?: () => void;
submittingText?: string;
continueText?: string;
testSubj: string;
}
export const ConnectedCreateCustomIntegrationButton = ({
machine,
isDisabled = false,
onClick: consumerOnClick,
submittingText = SUBMITTING_TEXT,
continueText = CONTINUE_TEXT,
testSubj,
}: ConnectedCreateCustomIntegrationButtonProps) => {
const [, send] = useActor(machine);

const onClick = useCallback(() => {
if (consumerOnClick) {
consumerOnClick();
}
send({ type: 'SAVE' });
}, [consumerOnClick, send]);

const isValid = useSelector(machine, isValidSelector);
const isSubmitting = useSelector(machine, isSubmittingSelector);

return (
<CreateCustomIntegrationButton
onClick={onClick}
isValid={isValid}
isSubmitting={isSubmitting}
isDisabled={isDisabled}
submittingText={submittingText}
continueText={continueText}
testSubj={testSubj}
/>
);
};

type CreateCustomIntegrationButtonProps = {
isValid: boolean;
isSubmitting: boolean;
} & Omit<ConnectedCreateCustomIntegrationButtonProps, 'machine'>;

const CreateCustomIntegrationButton = ({
onClick,
isValid,
isSubmitting,
isDisabled,
submittingText,
continueText,
testSubj,
}: CreateCustomIntegrationButtonProps) => {
return (
<EuiButton
data-test-subj={testSubj}
color="primary"
fill
onClick={onClick}
isLoading={isSubmitting}
isDisabled={isDisabled || !isValid}
>
{isSubmitting ? submittingText : continueText}
</EuiButton>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiCallOut } from '@elastic/eui';
import {
AuthorizationError,
IntegrationError,
IntegrationNotInstalledError,
UnknownError,
} from '../../types';
import { CreateTestSubjects } from './form';

const TITLE = i18n.translate('customIntegrationsPackage.create.errorCallout.title', {
defaultMessage: 'Sorry, there was an error',
});

const RETRY_TEXT = i18n.translate('customIntegrationsPackage.create.errorCallout.retryText', {
defaultMessage: 'Retry',
});

export const ErrorCallout = ({
error,
onRetry,
testSubjects,
}: {
error: IntegrationError;
onRetry?: () => void;
testSubjects?: CreateTestSubjects['errorCallout'];
}) => {
if (error instanceof AuthorizationError) {
const authorizationDescription = i18n.translate(
'customIntegrationsPackage.create.errorCallout.authorization.description',
{
defaultMessage: 'This user does not have permissions to create an integration.',
}
);
return (
<BaseErrorCallout
message={authorizationDescription}
onRetry={onRetry}
testSubjects={testSubjects}
/>
);
} else if (error instanceof UnknownError || error instanceof IntegrationNotInstalledError) {
return (
<BaseErrorCallout message={error.message} onRetry={onRetry} testSubjects={testSubjects} />
);
} else {
return null;
}
};

const BaseErrorCallout = ({
message,
onRetry,
testSubjects,
}: {
message: string;
onRetry?: () => void;
testSubjects?: CreateTestSubjects['errorCallout'];
}) => {
return (
<EuiCallOut
title={TITLE}
color="danger"
iconType="error"
data-test-subj={testSubjects?.callout ?? 'customIntegrationsPackageCreateFormErrorCallout'}
>
<>
<p>{message}</p>
{onRetry ? (
<EuiButton
data-test-subj={
testSubjects?.retryButton ??
'customIntegrationsPackageCreateFormErrorCalloutRetryButton'
}
color="danger"
size="s"
onClick={onRetry}
>
{RETRY_TEXT}
</EuiButton>
) : null}
</>
</EuiCallOut>
);
};
Loading