Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
08c2d9e
first commit
jcger Nov 10, 2025
219f507
Changes from node scripts/capture_oas_snapshot --include-path /api/…
kibanamachine Nov 10, 2025
ea7f019
add default from meta and better submit handling
jcger Nov 10, 2025
8fc160c
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 10, 2025
671277f
remove not used spec
jcger Nov 11, 2025
61ac131
add bearer token + improve union layout
jcger Nov 11, 2025
353ac04
add schema related comment for clarification
jcger Nov 11, 2025
362d918
add key-pair widget
jcger Nov 11, 2025
64fa931
fix error handling in nested fields + chore
jcger Nov 12, 2025
35dbfda
centralized state management
jcger Nov 12, 2025
408eb05
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 12, 2025
16c21a7
infer types
jcger Nov 12, 2025
d58af27
Changes from node scripts/generate codeowners
kibanamachine Nov 12, 2025
ccaa4c9
fix nested validation
jcger Nov 12, 2025
3b6cb69
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 12, 2025
b23e09d
fix lint
jcger Nov 12, 2025
f3ec12e
add basic fields tests
jcger Nov 12, 2025
bdebfb1
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 12, 2025
87f28f6
add discriminated union field test
jcger Nov 12, 2025
d540d4d
add single option discr. union + refactor
jcger Nov 12, 2025
136f48c
form and form state tests + remove discr. transform for single option
jcger Nov 13, 2025
9165398
Merge branch 'main' into issue-ro-479-connector-form-generator
jcger Nov 13, 2025
54a72a2
migrate uimeta to meta
jcger Nov 13, 2025
cb1be2c
tmp metadata + label under widget options
jcger Nov 13, 2025
963b6a4
add other connector demos
jcger Nov 13, 2025
f8e7a99
rename nested to options, simplify validation, add basic helptext sup…
jcger Nov 13, 2025
764990f
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 13, 2025
92c55e6
state management + error validation refactor
jcger Nov 13, 2025
509027a
Merge branch 'main' of https://github.com/jcger/kibana into issue-ro-…
jcger Nov 13, 2025
e7f792e
add test + bugfix
jcger Nov 13, 2025
607d993
type refactor
jcger Nov 14, 2025
90ed94a
remove duplication + cleaner root level error code
jcger Nov 14, 2025
82f7350
simplify default value + nested no more than 2 levels"
jcger Nov 14, 2025
572f057
discriminated union works with the defined discriminator
jcger Nov 14, 2025
e4e7b6b
key-value tests
jcger Nov 17, 2025
e0fbe5d
cleaning
jcger Nov 17, 2025
31a181a
translations
jcger Nov 17, 2025
42104f2
adds default widgets for schemas
jcger Nov 18, 2025
5590d14
improve getting component from schema
jcger Nov 18, 2025
cfe0abb
create options from initial field generation + remove not needed option
jcger Nov 18, 2025
581776c
add aria-label
jcger Nov 18, 2025
1cf5495
fix tests
jcger Nov 18, 2025
a92b729
Merge branch 'main' into issue-ro-479-connector-form-generator
jcger Nov 19, 2025
3b45167
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 19, 2025
6ba587e
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 19, 2025
d4dd39a
use connector_spec
jcger Nov 19, 2025
245a294
remove not used variable
jcger Nov 19, 2025
6353363
simplify state type and error handling
jcger Nov 19, 2025
84b0ff4
cleanup
jcger Nov 19, 2025
e8bf67b
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 19, 2025
9fa2e48
Merge branch 'main' of https://github.com/jcger/kibana into issue-ro-…
jcger Nov 19, 2025
44cbc61
use shared form
jcger Nov 20, 2025
c72cd7c
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 20, 2025
766379f
Merge branch 'main' of https://github.com/jcger/kibana into issue-ro-…
jcger Nov 21, 2025
3809531
remove response-ops dependencies
jcger Nov 21, 2025
739685a
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 21, 2025
bff0fad
add global form config with readonly prop
jcger Nov 21, 2025
9f9c89c
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 21, 2025
469810c
make formConfig available in generator
jcger Nov 21, 2025
a55b30a
working UI
jcger Nov 21, 2025
fdafae9
better default handling + literal and url defaults
jcger Nov 21, 2025
6ab8f65
cleaner default approach
jcger Nov 21, 2025
d1bcf07
fix tests
jcger Nov 21, 2025
46071d1
docs + restructure
jcger Nov 21, 2025
8a5a25d
mend
jcger Nov 21, 2025
17f865e
add schema extract test
jcger Nov 24, 2025
99c8a85
add registry tests
jcger Nov 24, 2025
ba29e73
add field builder test
jcger Nov 24, 2025
5262382
readonly -> disabled + form test
jcger Nov 24, 2025
d5bd761
Merge branch 'main' of https://github.com/jcger/kibana into issue-ro-…
jcger Nov 24, 2025
0545387
fix disable and update generated auth schema
jcger Nov 24, 2025
1ba8e3f
Merge branch 'main' of https://github.com/jcger/kibana into issue-ro-…
jcger Nov 24, 2025
40b3234
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 24, 2025
3e3d8d7
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Nov 24, 2025
6a52ff7
review + missing tests
jcger Nov 24, 2025
b8dd200
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 24, 2025
cb13fc1
update limits
jcger Nov 24, 2025
f3aa5df
update test snapshots
jcger Nov 25, 2025
25cf7ef
add i18n to authSchema messages and labels
jcger Nov 25, 2025
dc357cf
Merge branch 'main' into issue-ro-479-connector-form-generator
jcger Nov 25, 2025
b381c75
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 25, 2025
9fa8a47
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Nov 25, 2025
c44f510
auth types i18n
jcger Nov 25, 2025
af83bf2
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 25, 2025
5368dce
add z.object widget
jcger Nov 25, 2025
9b88653
Merge branch 'main' of https://github.com/jcger/kibana into issue-ro-…
jcger Nov 26, 2025
026d1e8
fix tests
jcger Nov 26, 2025
3768333
fix test
jcger Nov 26, 2025
b0e6efc
lazy load zod and generator
jcger Nov 27, 2025
b643c36
use single webpackChunk name
jcger Nov 27, 2025
4982c19
fix core extraction
jcger Nov 27, 2025
900f13e
fix spacer
jcger Nov 27, 2025
ae19299
add optional label for optional fields
jcger Nov 27, 2025
5b59310
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 27, 2025
cab1fe1
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Nov 27, 2025
5ff71e0
Merge branch 'main' into issue-ro-479-connector-form-generator
jcger Nov 28, 2025
a14cc37
change file extension
jcger Nov 28, 2025
60b9b17
Merge branch 'main' of https://github.com/jcger/kibana into issue-ro-…
jcger Nov 28, 2025
8ad26af
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 28, 2025
da33ee5
first commit
jcger Nov 28, 2025
377d2a9
Merge branch 'issue-ro-479-connector-form-generator' of https://githu…
jcger Nov 28, 2025
31024f6
Merge branch 'main' of https://github.com/jcger/kibana into issue-244…
jcger Dec 1, 2025
427e9a6
add tests
jcger Dec 1, 2025
43e140c
fix tests
jcger Dec 1, 2025
ed26d81
add config.auth to server schema
jcger Dec 2, 2025
de0767c
fix test
jcger Dec 2, 2025
55bd26d
fix type
jcger Dec 2, 2025
0d8c54f
fix types
jcger Dec 2, 2025
452cfc8
fix tests
jcger Dec 3, 2025
2c0f9cb
Merge branch 'main' into issue-244390-discriminator-on-edit
jcger Dec 3, 2025
c2fa480
Merge branch 'main' into issue-244390-discriminator-on-edit
jcger Dec 3, 2025
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export const generateSecretsSchemaFromSpec = (authTypes: ConnectorSpec['authType
z
.discriminatedUnion('authType', [secretSchemas[0], ...secretSchemas.slice(1)])
.meta({ label: 'Authentication' })
: z.object({}).default({});
: z.object({});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { z } from '@kbn/zod/v4';
import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { addMeta } from '../../../schema_connector_metadata';
import { MultiOptionUnionWidget } from './multi_option_union_widget';

describe('MultiOptionUnionWidget - Serializer/Deserializer Integration', () => {
// Simulates a connector schema with secrets discriminated union
const createSchema = () => {
const noneOption = z.object({ authType: z.literal('none') });
addMeta(noneOption, { label: 'None' });

const basicOption = z.object({
authType: z.literal('basic'),
username: z.string(),
password: z.string(),
});
addMeta(basicOption, { label: 'Basic Auth' });

const bearerOption = z.object({
authType: z.literal('bearer'),
token: z.string(),
});
addMeta(bearerOption, { label: 'Bearer Token' });

return z.object({
config: z.object({
url: z.string(),
authType: z.string().optional(),
}),
secrets: z.discriminatedUnion('authType', [noneOption, basicOption, bearerOption]),
});
};

// Simulates the deserializer that reconstructs secrets from config
const deserializer = (apiData: any) => {
if (!apiData?.config?.authType || apiData.secrets?.authType) {
return apiData;
}

// Reconstruct secrets from config.authType
return {
...apiData,
secrets: { authType: apiData.config.authType },
};
};

// Simulates the serializer that copies authType to config
const serializer = (formData: any) => {
if (!formData?.secrets?.authType) {
return formData;
}

return {
...formData,
config: {
...formData.config,
authType: formData.secrets.authType,
},
};
};

const TestComponent = ({ initialData }: { initialData: any }) => {
const schema = createSchema();
const { form } = useForm({
defaultValue: initialData,
serializer,
deserializer,
});

return (
<Form form={form}>
<MultiOptionUnionWidget
path="secrets"
options={schema.shape.secrets.options}
discriminatorKey="authType"
schema={schema.shape.secrets}
fieldConfig={{
defaultValue: undefined,
validations: [{ validator: () => undefined }],
}}
fieldProps={{ label: 'Authentication', euiFieldProps: {} }}
formConfig={{}}
/>
<button
type="button"
onClick={async () => {
const { data } = await form.submit();
// Expose serialized data for testing
(window as any).serializedData = data;
}}
>
Submit
</button>
</Form>
);
};

beforeEach(() => {
(window as any).serializedData = undefined;
});

it('should initialize from async form data (deserializer)', async () => {
const apiData = {
config: { url: 'https://example.com', authType: 'basic' },
secrets: {}, // Secrets are stripped by API
};

render(<TestComponent initialData={apiData} />);

await waitFor(() => {
const basicCard = screen.getByLabelText('Basic Auth');
expect(basicCard).toBeChecked();
});
});

it('should not overwrite user selection when form re-renders (ref flag pattern)', async () => {
const apiData = {
config: { url: 'https://example.com', authType: 'basic' },
secrets: {},
};

const { rerender } = render(<TestComponent initialData={apiData} />);

await waitFor(() => {
expect(screen.getByLabelText('Basic Auth')).toBeChecked();
});

await userEvent.click(screen.getByLabelText('Bearer Token'));

await waitFor(() => {
expect(screen.getByLabelText('Bearer Token')).toBeChecked();
});

// simulating parent re-render
rerender(<TestComponent initialData={apiData} />);

await waitFor(() => {
expect(screen.getByLabelText('Bearer Token')).toBeChecked();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { EuiCheckableCard, EuiFormFieldset, EuiSpacer, EuiTitle } from '@elastic/eui';
import { useFormData, useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { addMeta, getMeta } from '../../../schema_connector_metadata';
import {
getDiscriminatorFieldValue,
Expand Down Expand Up @@ -81,10 +82,31 @@ export const MultiOptionUnionWidget: React.FC<DiscriminatedUnionWidgetProps> = (
fieldProps,
formConfig,
}) => {
const defaultOption = getDefaultOption(options, discriminatorKey, fieldConfig);
const [selectedOption, setSelectedOption] = useState(() =>
getDiscriminatorFieldValue(defaultOption, discriminatorKey)
);
const [selectedOption, setSelectedOption] = useState(() => {
const defaultOption = getDefaultOption(options, discriminatorKey, fieldConfig);
return getDiscriminatorFieldValue(defaultOption, discriminatorKey);
});

const [formData] = useFormData();
const { setFieldValue } = useFormContext();

const hasInitializedFromFormData = useRef(false);
const discriminatorValueFromForm = formData[rootPath]?.[discriminatorKey] as string | undefined;
const discriminatorFieldPath = `${rootPath}.${discriminatorKey}`;

useEffect(() => {
if (discriminatorValueFromForm && !hasInitializedFromFormData.current) {
setSelectedOption(discriminatorValueFromForm);
hasInitializedFromFormData.current = true;
return;
}

// After initialization: Sync selectedOption changes back to form data
// This happens when user clicks a different option
if (hasInitializedFromFormData.current && discriminatorValueFromForm !== selectedOption) {
setFieldValue(discriminatorFieldPath, selectedOption);
}
}, [discriminatorFieldPath, discriminatorValueFromForm, selectedOption, setFieldValue]);

const isFieldsetDisabled = formConfig.disabled || getMeta(schema).disabled;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
*/

import type { ConnectorSpec } from '@kbn/connector-specs';
import { z as z4 } from '@kbn/zod/v4';
import { z } from '@kbn/zod/v4';

import type { ActionTypeConfig, ValidatorType } from '../../types';

export const generateConfigSchema = (
schema: ConnectorSpec['schema']
): ValidatorType<ActionTypeConfig> => ({ schema: schema ?? z4.object({}) });
): ValidatorType<ActionTypeConfig> => {
const authType = z.string().optional();
const configSchema = schema ? schema.extend({ authType }) : z.object({ authType });
return { schema: configSchema };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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 { z } from '@kbn/zod/v4';
import {
createConnectorFormSerializer,
createConnectorFormDeserializer,
} from './connector_form_serializers';

describe('createConnectorFormSerializer', () => {
it('should copy authType from secrets to config', () => {
const serializer = createConnectorFormSerializer();
const formData = {
config: { url: 'https://example.com' },
secrets: { authType: 'basic', username: 'user', password: 'pass' },
};

const result = serializer(formData);

expect(result.config.authType).toBe('basic');
expect(result.secrets.authType).toBe('basic');
});

it('should return data unchanged when secrets.authType is missing', () => {
const serializer = createConnectorFormSerializer();
const formData = {
config: { url: 'https://example.com' },
secrets: {},
};

const result = serializer(formData);

expect(result).toEqual(formData);
expect(result.config.authType).toBeUndefined();
});

it('should overwrite existing config.authType if secrets.authType exists', () => {
const serializer = createConnectorFormSerializer();
const formData = {
config: { url: 'https://example.com', authType: 'old' },
secrets: { authType: 'bearer', token: 'xyz' },
};

const result = serializer(formData);

expect(result.config.authType).toBe('bearer');
});

it('should handle undefined formData', () => {
const serializer = createConnectorFormSerializer();

const result = serializer(undefined);

expect(result).toBeUndefined();
});
});

describe('createConnectorFormDeserializer', () => {
const commonApiData = {
actionTypeId: 'test-connector',
isDeprecated: false,
};

const createTestSchema = () => {
return z.object({
config: z.object({
url: z.string(),
authType: z.string().optional(),
}),
secrets: z.discriminatedUnion('authType', [
z.object({ authType: z.literal('none') }),
z.object({ authType: z.literal('basic'), username: z.string(), password: z.string() }),
z.object({ authType: z.literal('bearer'), token: z.string() }),
]),
});
};

it('should copy authType from config to secrets when editing', () => {
const schema = createTestSchema();
const deserializer = createConnectorFormDeserializer(schema);
const apiData = {
...commonApiData,
config: { url: 'https://example.com', authType: 'basic' },
secrets: {},
};

const result = deserializer(apiData);

expect(result.secrets.authType).toBe('basic');
expect(result.config.authType).toBe('basic');
});

it('should return data unchanged when config.authType is missing', () => {
const schema = createTestSchema();
const deserializer = createConnectorFormDeserializer(schema);
const apiData = {
...commonApiData,
config: { url: 'https://example.com' },
secrets: {},
};

const result = deserializer(apiData);

expect(result).toEqual(apiData);
expect(result.secrets.authType).toBeUndefined();
});

it('should return data unchanged when secrets.authType already exists', () => {
const schema = createTestSchema();
const deserializer = createConnectorFormDeserializer(schema);
const apiData = {
...commonApiData,
config: { url: 'https://example.com', authType: 'basic' },
secrets: { authType: 'bearer', token: 'xyz' },
};

const result = deserializer(apiData);

expect(result).toEqual(apiData);
expect(result.secrets.authType).toBe('bearer');
});

it('should handle schema without discriminated union gracefully', () => {
const schema = z.object({
config: z.object({ url: z.string() }),
secrets: z.object({ token: z.string() }),
});
const deserializer = createConnectorFormDeserializer(schema);
const apiData = {
...commonApiData,
config: { url: 'https://example.com', authType: 'basic' },
secrets: {},
};

const result = deserializer(apiData);

expect(result).toEqual(apiData);
});

it('should handle errors', () => {
const invalidSchema = {} as z.ZodObject<z.ZodRawShape>;
const deserializer = createConnectorFormDeserializer(invalidSchema);
const apiData = {
...commonApiData,
config: { url: 'https://example.com', authType: 'basic' },
secrets: {},
};

const result = deserializer(apiData);

expect(result).toEqual(apiData);
});
});
Loading