diff --git a/packages/core/forms/fixtures/mockData.ts b/packages/core/forms/fixtures/mockData.ts new file mode 100644 index 0000000000..b90b66a75e --- /dev/null +++ b/packages/core/forms/fixtures/mockData.ts @@ -0,0 +1,888 @@ +export const redisPartials = [ + { + 'created_at': 1739167439, + 'updated_at': 1739167439, + 'name': 'test-redis-ce-config', + 'type': 'redis-ce', + 'config': { + 'host': '127.0.0.1', + 'password': null, + 'ssl_verify': false, + 'database': 0, + 'server_name': 'test-ce-server-name', + 'username': null, + 'timeout': 2000, + 'ssl': false, + 'port': 6379, + }, + 'id': '028c9637-0408-4ab0-8d35-0b474de4bde6', + }, + { + 'created_at': 1739166982, + 'updated_at': 1739166982, + 'name': 'test-redis-ee', + 'type': 'redis-ee', + 'config': { + 'host': '127.0.0.1', + 'password': null, + 'ssl_verify': false, + 'username': 'test-username', + 'keepalive_pool_size': 256, + 'port': 6379, + 'connection_is_proxied': false, + 'cluster_nodes': null, + 'sentinel_role': null, + 'send_timeout': 2000, + 'sentinel_nodes': null, + 'database': 0, + 'ssl': false, + 'keepalive_backlog': 0, + 'server_name': null, + 'read_timeout': 2000, + 'cluster_max_redirections': 5, + 'sentinel_password': null, + 'sentinel_username': null, + 'connect_timeout': 2000, + 'sentinel_master': null, + }, + 'id': '3f2d22a2-fe4c-4211-b1db-2ad44b36eab7', + }, + { + 'created_at': 1739181896, + 'updated_at': 1739181896, + 'name': 'test-redis-ee-2', + 'type': 'redis-ee', + 'config': { + 'host': '127.0.0.1', + 'password': null, + 'ssl_verify': false, + 'username': null, + 'keepalive_pool_size': 256, + 'port': 6379, + 'connection_is_proxied': false, + 'cluster_nodes': null, + 'sentinel_role': null, + 'send_timeout': 2000, + 'sentinel_nodes': null, + 'database': 0, + 'ssl': false, + 'keepalive_backlog': 0, + 'server_name': 'test-redis-ee-server', + 'read_timeout': 2000, + 'cluster_max_redirections': 5, + 'sentinel_password': null, + 'sentinel_username': null, + 'connect_timeout': 2000, + 'sentinel_master': null, + }, + 'id': 'c41669c8-9ff6-4d8c-b667-494864a585ec', + }, +] + +export const redisCEConfig = { + 'created_at': 1739167439, + 'updated_at': 1739167439, + 'name': 'test-redis-ce-config', + 'type': 'redis-ce', + 'config': { + 'host': '127.0.0.1', + 'password': null, + 'ssl_verify': false, + 'database': 0, + 'server_name': 'test-ce-server-name', + 'username': null, + 'timeout': 2000, + 'ssl': false, + 'port': 6379, + }, + 'id': '028c9637-0408-4ab0-8d35-0b474de4bde6', +} + +export const redisEEConfig = { + 'created_at': 1739181896, + 'updated_at': 1739181896, + 'name': 'test-redis-ee', + 'type': 'redis-ee', + 'config': { + 'host': '127.0.0.1', + 'password': null, + 'ssl_verify': false, + 'username': null, + 'keepalive_pool_size': 256, + 'port': 6379, + 'connection_is_proxied': false, + 'cluster_nodes': null, + 'sentinel_role': null, + 'send_timeout': 2000, + 'sentinel_nodes': null, + 'database': 0, + 'ssl': false, + 'keepalive_backlog': 0, + 'server_name': 'test-redis-ee-server', + 'read_timeout': 2000, + 'cluster_max_redirections': 5, + 'sentinel_password': null, + 'sentinel_username': null, + 'connect_timeout': 2000, + 'sentinel_master': null, + }, + 'id': 'c41669c8-9ff6-4d8c-b667-494864a585ec', +} + +export const RLSchema = { + 'groups': [ + { + 'fields': [ + { + 'default': 'rate-limiting', + 'type': 'input', + 'inputType': 'hidden', + 'styleClasses': 'd-none hidden-field', + 'pinned': true, + 'id': 'name', + 'model': 'name', + 'label': '', + }, + { + 'type': 'switch', + 'model': 'enabled', + 'label': 'Enabled', + 'textOn': 'This plugin is Enabled', + 'textOff': 'This plugin is Disabled', + 'styleClasses': 'field-switch hide-label', + 'default': true, + 'pinned': true, + 'id': 'enabled', + }, + { + 'type': 'selectionGroup', + 'disabled': false, + 'inputType': 'hidden', + 'styleClasses': 'hide-label', + 'fields': [ + { + 'label': 'Global', + 'description': 'All services, routes, and consumers', + }, + { + 'label': 'Scoped', + 'description': 'Specific Gateway Services, Routes, Consumers, and/or Consumer Groups', + 'fields': [ + { + 'id': 'service-id', + 'model': 'service-id', + 'label': 'Gateway Service', + 'placeholder': 'Select a Gateway Service', + 'type': 'AutoSuggest', + 'entity': 'services', + 'inputValues': { + 'fields': [ + 'name', + 'id', + ], + }, + 'help': 'The Gateway Service to which this plugin configuration will apply', + 'disabled': false, + }, + { + 'id': 'route-id', + 'model': 'route-id', + 'label': 'Route', + 'placeholder': 'Select a Route', + 'type': 'AutoSuggest', + 'entity': 'routes', + 'inputValues': { + 'fields': [ + 'name', + 'id', + ], + 'primaryField': 'id', + }, + 'help': 'The Route that this Plugin configuration will target', + 'disabled': false, + }, + { + 'model': 'consumer-id', + 'label': 'Consumer', + 'placeholder': 'Select a Consumer', + 'type': 'AutoSuggest', + 'entity': 'consumers', + 'inputValues': { + 'fields': [ + 'username', + 'custom_id', + 'id', + ], + 'primaryField': 'username', + }, + 'help': 'The Consumer that this plugin configuration will target', + 'disabled': false, + }, + { + 'model': 'consumer_group-id', + 'label': 'Consumer Group', + 'placeholder': 'Select a Consumer Group', + 'type': 'AutoSuggest', + 'entity': 'consumer_groups', + 'entityDataKey': 'consumer_group', + 'inputValues': { + 'fields': [ + 'name', + 'id', + ], + 'primaryField': 'name', + }, + 'help': 'The Consumer Group that this plugin configuration will target', + 'disabled': false, + }, + ], + }, + ], + 'pinned': true, + 'id': 'selectionGroup', + 'model': 'selectionGroup', + 'label': '', + }, + ], + }, + { + 'fields': [ + { + 'id': 'protocols', + 'default': [ + 'grpc', + 'grpcs', + 'http', + 'https', + ], + 'help': 'A list of the request protocols that will trigger this plugin. The default value, as well as the possible values allowed on this field, may change depending on the plugin type.', + 'label': 'Protocols', + 'required': true, + 'styleClasses': 'plugin-protocols-select', + 'type': 'multiselect', + 'values': [ + { + 'label': 'grpc', + 'value': 'grpc', + }, + { + 'label': 'grpcs', + 'value': 'grpcs', + }, + { + 'label': 'http', + 'value': 'http', + }, + { + 'label': 'https', + 'value': 'https', + }, + ], + 'placeholder': 'Select valid protocols for the plugin. Default protocols: grpc, grpcs, http, https', + 'model': 'protocols', + }, + { + 'id': 'config-day', + 'model': 'config-day', + 'type': 'input', + 'label': 'Config.Day', + 'help': '

The number of HTTP requests that can be made per day.

\n', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-fault_tolerant', + 'model': 'config-fault_tolerant', + 'type': 'checkbox', + 'required': true, + 'label': 'Config.Fault Tolerant', + 'help': '

A boolean value that determines if the requests should be proxied even if Kong has troubles connecting a third-party data store. If true, requests will be proxied anyway, effectively disabling the rate-limiting function until the data store is working again. If false, then the clients will see 500 errors.

\n', + 'default': true, + 'valueType': 'boolean', + }, + { + 'id': 'config-hide_client_headers', + 'model': 'config-hide_client_headers', + 'type': 'checkbox', + 'required': true, + 'label': 'Config.Hide Client Headers', + 'help': '

Optionally hide informative response headers.

\n', + 'default': false, + 'valueType': 'boolean', + }, + { + 'id': 'config-hour', + 'model': 'config-hour', + 'type': 'input', + 'label': 'Config.Hour', + 'help': '

The number of HTTP requests that can be made per hour.

\n', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-minute', + 'model': 'config-minute', + 'type': 'input', + 'label': 'Config.Minute', + 'help': '

The number of HTTP requests that can be made per minute.

\n', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-month', + 'model': 'config-month', + 'type': 'input', + 'label': 'Config.Month', + 'help': '

The number of HTTP requests that can be made per month.

\n', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-second', + 'model': 'config-second', + 'type': 'input', + 'label': 'Config.Second', + 'help': '

The number of HTTP requests that can be made per second.

\n', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-sync_rate', + 'model': 'config-sync_rate', + 'type': 'input', + 'required': true, + 'label': 'Config.Sync Rate', + 'help': '

How often to sync counter data to the central data store. A value of -1 results in synchronous behavior.

\n', + 'selectOptions': { + 'hideNoneSelectedText': true, + }, + 'default': -1, + 'placeholder': 'Default: -1', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-year', + 'model': 'config-year', + 'type': 'input', + 'label': 'Config.Year', + 'help': '

The number of HTTP requests that can be made per year.

\n', + 'inputType': 'number', + 'valueType': 'number', + }, + ], + 'collapsible': { + 'title': 'Plugin Configuration', + 'description': 'Configuration parameters for this plugin. View advanced parameters for extended configuration.', + 'nestedCollapsible': { + 'fields': [ + { + 'id': '_redis', + 'fields': [ + { + 'id': 'config-redis-database', + 'model': 'config-redis-database', + 'type': 'input', + 'label': 'Config.Redis.Database', + 'help': '

Database to use for the Redis connection when using the redis strategy

\n', + 'default': 0, + 'placeholder': 'Default: 0', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-redis-host', + 'model': 'config-redis-host', + 'type': 'input', + 'label': 'Config.Redis.Host', + 'help': '

A string representing a host name, such as example.com.

\n', + 'inputType': 'text', + 'valueType': 'string', + }, + { + 'id': 'config-redis-password', + 'model': 'config-redis-password', + 'type': 'input', + 'referenceable': true, + 'label': 'Config.Redis.Password', + 'help': '

Password to use for Redis connections. If undefined, no AUTH commands are sent to Redis.

\n', + 'inputType': 'password', + 'valueType': 'string', + }, + { + 'id': 'config-redis-port', + 'model': 'config-redis-port', + 'type': 'input', + 'label': 'Config.Redis.Port', + 'help': '

An integer representing a port number between 0 and 65535, inclusive.

\n', + 'default': 6379, + 'placeholder': 'Default: 6379', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-redis-server_name', + 'model': 'config-redis-server_name', + 'type': 'input', + 'required': false, + 'label': 'Config.Redis.Server Name', + 'help': '

A string representing an SNI (server name indication) value for TLS.

\n', + 'inputType': 'text', + 'valueType': 'string', + }, + { + 'id': 'config-redis-ssl', + 'model': 'config-redis-ssl', + 'type': 'checkbox', + 'required': false, + 'label': 'Config.Redis.Ssl', + 'help': '

If set to true, uses SSL to connect to Redis.

\n', + 'default': false, + 'valueType': 'boolean', + }, + { + 'id': 'config-redis-ssl_verify', + 'model': 'config-redis-ssl_verify', + 'type': 'checkbox', + 'required': false, + 'label': 'Config.Redis.Ssl Verify', + 'help': '

If set to true, verifies the validity of the server SSL certificate. If setting this parameter, also configure lua_ssl_trusted_certificate in kong.conf to specify the CA (or server) certificate used by your Redis server. You may also need to configure lua_ssl_verify_depth accordingly.

\n', + 'default': false, + 'valueType': 'boolean', + }, + { + 'id': 'config-redis-timeout', + 'model': 'config-redis-timeout', + 'type': 'input', + 'label': 'Config.Redis.Timeout', + 'help': '

An integer representing a timeout in milliseconds. Must be between 0 and 2^31-2.

\n', + 'default': 2000, + 'placeholder': 'Default: 2000', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-redis-username', + 'model': 'config-redis-username', + 'type': 'input', + 'referenceable': true, + 'label': 'Config.Redis.Username', + 'help': '

Username to use for Redis connections. If undefined, ACL authentication won't be performed. This requires Redis v6.0.0+. To be compatible with Redis v5.x.y, you can set it to default.

\n', + 'inputType': 'text', + 'valueType': 'string', + }, + ], + 'model': '__redis_partial', + 'pluginType': 'bundled', + 'redisType': 'redis-ce', + 'order': -1, + }, + { + 'default': '', + 'type': 'input', + 'label': 'Instance Name', + 'inputType': 'text', + 'help': 'A custom name for this plugin instance to help identifying from the list view.', + 'id': 'instance_name', + 'model': 'instance_name', + }, + { + 'label': 'Tags', + 'name': 'tags', + 'type': 'input', + 'inputType': 'text', + 'valueType': 'array', + 'valueArrayType': 'string', + 'placeholder': 'Enter list of tags', + 'help': 'An optional set of strings for grouping and filtering, separated by commas.', + 'hint': 'e.g. tag1, tag2, tag3', + 'id': 'tags', + 'model': 'tags', + }, + { + 'id': 'config-error_code', + 'model': 'config-error_code', + 'type': 'input', + 'label': 'Config.Error Code', + 'help': '

Set a custom error code to return when the rate limit is exceeded.

\n', + 'default': 429, + 'placeholder': 'Default: 429', + 'inputType': 'number', + 'valueType': 'number', + }, + { + 'id': 'config-error_message', + 'model': 'config-error_message', + 'type': 'input', + 'label': 'Config.Error Message', + 'help': '

Set a custom error message to return when the rate limit is exceeded.

\n', + 'default': 'API rate limit exceeded', + 'placeholder': 'Default: API rate limit exceeded', + 'inputType': 'text', + 'valueType': 'string', + }, + { + 'id': 'config-header_name', + 'model': 'config-header_name', + 'type': 'input', + 'label': 'Config.Header Name', + 'help': '

A string representing an HTTP header name.

\n', + 'inputType': 'text', + 'valueType': 'string', + }, + { + 'id': 'config-limit_by', + 'model': 'config-limit_by', + 'type': 'select', + 'label': 'Config.Limit By', + 'help': '

The entity that is used when aggregating the limits.

\n', + 'values': [ + 'consumer', + 'credential', + 'ip', + 'service', + 'header', + 'path', + 'consumer-group', + ], + 'selectOptions': { + 'noneSelectedText': 'No selection...', + }, + 'default': 'consumer', + 'valueType': 'string', + }, + { + 'id': 'config-path', + 'model': 'config-path', + 'type': 'input', + 'label': 'Config.Path', + 'help': '

A string representing a URL path, such as /path/to/resource. Must start with a forward slash (/) and must not contain empty segments (i.e., two consecutive forward slashes).

\n', + 'inputType': 'text', + 'valueType': 'string', + }, + { + 'id': 'config-policy', + 'model': 'config-policy', + 'type': 'select', + 'label': 'Config.Policy', + 'help': '

The rate-limiting policies to use for retrieving and incrementing the limits.

\n', + 'values': [ + 'local', + 'cluster', + 'redis', + ], + 'selectOptions': { + 'noneSelectedText': 'No selection...', + }, + 'default': 'local', + 'valueType': 'string', + }, + ], + 'triggerLabel': { + 'expand': 'View Advanced Parameters', + 'collapse': 'Hide Advanced Parameters', + }, + }, + }, + 'slots': { + 'beforeContent': 'plugin-config-before-content', + 'emptyState': 'plugin-config-empty-state', + }, + }, + ], +} + +export const RLModel = { + 'name': 'rate-limiting', + 'enabled': true, + 'service-id': null, + 'route-id': null, + 'consumer-id': null, + 'consumer_group-id': null, + 'protocols': [ + 'grpc', + 'grpcs', + 'http', + 'https', + ], + 'instance_name': '', + 'tags': null, + 'config-day': null, + 'config-error_code': 429, + 'config-error_message': 'API rate limit exceeded', + 'config-fault_tolerant': true, + 'config-header_name': null, + 'config-hide_client_headers': false, + 'config-hour': null, + 'config-limit_by': 'consumer', + 'config-minute': null, + 'config-month': null, + 'config-path': null, + 'config-policy': 'local', + 'config-redis-database': 0, + 'config-redis-host': null, + 'config-redis-password': null, + 'config-redis-port': 6379, + 'config-redis-server_name': null, + 'config-redis-ssl': false, + 'config-redis-ssl_verify': false, + 'config-redis-timeout': 2000, + 'config-redis-username': null, + 'config-second': null, + 'config-sync_rate': -1, + 'config-year': null, +} + +export const customPluginSchema = { + 'groups': [ + { + 'fields': [ + { + 'default': 'custom-redis', + 'type': 'input', + 'inputType': 'hidden', + 'styleClasses': 'd-none hidden-field', + 'pinned': true, + 'id': 'name', + 'model': 'name', + 'label': '', + }, + { + 'type': 'switch', + 'model': 'enabled', + 'label': 'Enabled', + 'textOn': 'This plugin is Enabled', + 'textOff': 'This plugin is Disabled', + 'styleClasses': 'field-switch hide-label', + 'default': true, + 'pinned': true, + 'id': 'enabled', + }, + { + 'type': 'selectionGroup', + 'disabled': false, + 'inputType': 'hidden', + 'styleClasses': 'hide-label', + 'fields': [ + { + 'label': 'Global', + 'description': 'All services, routes, and consumers', + }, + { + 'label': 'Scoped', + 'description': 'Specific Gateway Services, Routes, Consumers, and/or Consumer Groups', + 'fields': [ + { + 'id': 'service-id', + 'model': 'service-id', + 'label': 'Gateway Service', + 'placeholder': 'Select a Gateway Service', + 'type': 'AutoSuggest', + 'entity': 'services', + 'inputValues': { + 'fields': [ + 'name', + 'id', + ], + }, + 'help': 'The Gateway Service to which this plugin configuration will apply', + 'disabled': false, + }, + { + 'id': 'route-id', + 'model': 'route-id', + 'label': 'Route', + 'placeholder': 'Select a Route', + 'type': 'AutoSuggest', + 'entity': 'routes', + 'inputValues': { + 'fields': [ + 'name', + 'id', + ], + 'primaryField': 'id', + }, + 'help': 'The Route that this Plugin configuration will target', + 'disabled': false, + }, + { + 'model': 'consumer-id', + 'label': 'Consumer', + 'placeholder': 'Select a Consumer', + 'type': 'AutoSuggest', + 'entity': 'consumers', + 'inputValues': { + 'fields': [ + 'username', + 'custom_id', + 'id', + ], + 'primaryField': 'username', + }, + 'help': 'The Consumer that this plugin configuration will target', + 'disabled': false, + }, + { + 'model': 'consumer_group-id', + 'label': 'Consumer Group', + 'placeholder': 'Select a Consumer Group', + 'type': 'AutoSuggest', + 'entity': 'consumer_groups', + 'entityDataKey': 'consumer_group', + 'inputValues': { + 'fields': [ + 'name', + 'id', + ], + 'primaryField': 'name', + }, + 'help': 'The Consumer Group that this plugin configuration will target', + 'disabled': false, + }, + ], + }, + ], + 'pinned': true, + 'id': 'selectionGroup', + 'model': 'selectionGroup', + 'label': '', + }, + ], + }, + { + 'fields': [ + { + 'id': 'protocols', + 'default': [ + 'grpc', + 'grpcs', + 'http', + 'https', + ], + 'help': 'A list of the request protocols that will trigger this plugin. The default value, as well as the possible values allowed on this field, may change depending on the plugin type.', + 'label': 'Protocols', + 'required': true, + 'styleClasses': 'plugin-protocols-select', + 'type': 'multiselect', + 'values': [ + { + 'label': 'grpc', + 'value': 'grpc', + }, + { + 'label': 'grpcs', + 'value': 'grpcs', + }, + { + 'label': 'http', + 'value': 'http', + }, + { + 'label': 'https', + 'value': 'https', + }, + ], + 'placeholder': 'Select valid protocols for the plugin. Default protocols: grpc, grpcs, http, https', + 'model': 'protocols', + }, + { + 'id': 'config-test_schema', + 'model': 'config-test_schema', + 'type': 'checkbox', + 'required': true, + 'label': 'Config.Test Schema', + 'default': true, + 'valueType': 'boolean', + }, + ], + 'collapsible': { + 'title': 'Plugin Configuration', + 'description': 'Configuration parameters for this plugin. View advanced parameters for extended configuration.', + 'nestedCollapsible': { + 'fields': [ + { + 'id': '_redis', + 'fields': [ + { + 'id': 'config-redis-host', + 'model': 'config-redis-host', + 'type': 'input', + 'label': 'Config.Redis.Host', + 'help': '

A string representing a host name, such as example.com.

\n', + 'default': '127.0.0.1', + 'placeholder': 'Default: 127.0.0.1', + 'inputType': 'text', + 'valueType': 'string', + }, + { + 'id': 'config-redis-port', + 'model': 'config-redis-port', + 'type': 'input', + 'label': 'Config.Redis.Port', + 'help': '

An integer representing a port number between 0 and 65535, inclusive.

\n', + 'default': 6379, + 'placeholder': 'Default: 6379', + 'inputType': 'number', + 'valueType': 'number', + }, + ], + 'model': '__redis_partial', + 'pluginType': 'custom', + 'order': -1, + }, + { + 'default': '', + 'type': 'input', + 'label': 'Instance Name', + 'inputType': 'text', + 'help': 'A custom name for this plugin instance to help identifying from the list view.', + 'id': 'instance_name', + 'model': 'instance_name', + }, + { + 'label': 'Tags', + 'name': 'tags', + 'type': 'input', + 'inputType': 'text', + 'valueType': 'array', + 'valueArrayType': 'string', + 'placeholder': 'Enter list of tags', + 'help': 'An optional set of strings for grouping and filtering, separated by commas.', + 'hint': 'e.g. tag1, tag2, tag3', + 'id': 'tags', + 'model': 'tags', + }, + ], + 'triggerLabel': { + 'expand': 'View Advanced Parameters', + 'collapse': 'Hide Advanced Parameters', + }, + }, + }, + 'slots': { + 'beforeContent': 'plugin-config-before-content', + 'emptyState': 'plugin-config-empty-state', + }, + }, + ], +} + +export const customPluginModel = { + 'name': 'custom-redis', + 'enabled': true, + 'service-id': null, + 'route-id': null, + 'consumer-id': null, + 'consumer_group-id': null, + 'protocols': [ + 'grpc', + 'grpcs', + 'http', + 'https', + ], + 'instance_name': '', + 'tags': null, + 'config-redis-host': '127.0.0.1', + 'config-redis-port': 6379, + 'config-test_schema': true, +} diff --git a/packages/core/forms/package.json b/packages/core/forms/package.json index 7e5741eb29..374b06622b 100644 --- a/packages/core/forms/package.json +++ b/packages/core/forms/package.json @@ -55,11 +55,15 @@ "lodash-es": "^4.17.21" }, "peerDependencies": { + "@kong-ui-public/entities-shared": "workspace:^", + "@kong-ui-public/entities-redis-configurations": "workspace:^", "@kong-ui-public/i18n": "workspace:^", "@kong/kongponents": "^9.14.16", "vue": "^3.5.12" }, "devDependencies": { + "@kong-ui-public/entities-shared": "workspace:^", + "@kong-ui-public/entities-redis-configurations": "workspace:^", "@kong-ui-public/i18n": "workspace:^", "@kong/design-tokens": "1.17.2", "@kong/kongponents": "9.17.2", diff --git a/packages/core/forms/src/components/FormGenerator.vue b/packages/core/forms/src/components/FormGenerator.vue index 644795941d..da7eb1b1db 100644 --- a/packages/core/forms/src/components/FormGenerator.vue +++ b/packages/core/forms/src/components/FormGenerator.vue @@ -11,8 +11,21 @@ v-for="field in fields" :key="field.model" > + + 0 }, }, + + enableRedisPartial: { + type: Boolean, + default: false, + }, }, - emits: ['validated', 'modelUpdated', 'refreshModel'], + emits: ['validated', 'modelUpdated', 'refreshModel', 'partialToggled', 'showNewPartialModal'], data() { return { @@ -339,6 +371,10 @@ export default { this.$emit('modelUpdated', newVal, schema) }, + onPartialToggled(field, model) { + this.$emit('partialToggled', field, model) + }, + // Validating the model properties validate(isAsync = null) { if (isAsync === null) { diff --git a/packages/core/forms/src/components/FormRedis.vue b/packages/core/forms/src/components/FormRedis.vue new file mode 100644 index 0000000000..9cbc933544 --- /dev/null +++ b/packages/core/forms/src/components/FormRedis.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/packages/core/forms/src/components/RedisConfigCard.vue b/packages/core/forms/src/components/RedisConfigCard.vue new file mode 100644 index 0000000000..003f080125 --- /dev/null +++ b/packages/core/forms/src/components/RedisConfigCard.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/packages/core/forms/src/components/RedisConfigSelect.vue b/packages/core/forms/src/components/RedisConfigSelect.vue new file mode 100644 index 0000000000..12d9682bd1 --- /dev/null +++ b/packages/core/forms/src/components/RedisConfigSelect.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/packages/core/forms/src/components/__tests__/FormGenerator.cy.ts b/packages/core/forms/src/components/__tests__/FormGenerator.cy.ts index 82ec050017..cffce4e587 100644 --- a/packages/core/forms/src/components/__tests__/FormGenerator.cy.ts +++ b/packages/core/forms/src/components/__tests__/FormGenerator.cy.ts @@ -4,16 +4,132 @@ // Cypress component test spec file import VueFormGenerator from '../FormGenerator.vue' -import { mount } from 'cypress/vue' +import { RLSchema, RLModel, customPluginSchema, customPluginModel, redisPartials, redisCEConfig } from '../../../fixtures/mockData' +import { createI18n } from '@kong-ui-public/i18n' +import english from '../../locales/en.json' + +const baseConfigKM = { + app: 'kongManager', + workspace: 'default', + apiBaseUrl: '/kong-manager', +} describe('', () => { - it('TODO: This is an example test', () => { - mount(VueFormGenerator, { + // it('should render redis fields as common fields when enableRedisPartial is not passed', () => { + // cy.mount(VueFormGenerator, { + // props: { + // schema: RLSchema, + // model: RLModel, + // enableRedisPartial: true, + // }, + // }) + // cy.get('.vue-form-generator').should('exist') + + // cy.getTestId('redis-config-card').should('not.exist') + + // }) + + it('should show shared config/grouped redis fields when toggling shared/dedicated redis configuration', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/partials?size=1000`, + }, + { + statusCode: 200, + body: { + data: redisPartials, + next: null, + }, + }, + ).as('getPartials') + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/partials/${redisPartials[0].id}`, + }, + { + statusCode: 200, + body: redisCEConfig, + }, + ).as('getRedisCEPartial') + cy.mount(VueFormGenerator, { props: { - schema: {}, + schema: RLSchema, + model: RLModel, + enableRedisPartial: true, + }, + global: { + provide: { + 'kong-ui-forms-config': baseConfigKM, + }, }, }) + cy.get('.vue-form-generator').should('exist') + cy.getTestId('collapse-trigger-label').click() + cy.getTestId('shared-redis-config-radio').click() + const [redisCEConfigDetail, redisEEConfigDetail] = redisPartials + cy.getTestId('redis-config-select').click() + + // should filter out EE plugins for CE plugin + cy.getTestId(`redis-configuration-dropdown-item-${redisCEConfigDetail.name}`).should('exist') + cy.getTestId(`redis-configuration-dropdown-item-${redisEEConfigDetail.name}`).should('not.exist') + cy.getTestId(`redis-configuration-dropdown-item-${redisCEConfigDetail.name}`).click() + cy.get('.partial-config-card').getTestId('name-property-value').should('contain.text', redisCEConfigDetail.name) + + }) + + it('should show redis configuration selector in advanced fields in custom plugin form', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/partials?size=1000`, + }, + { + statusCode: 200, + body: { + data: redisPartials, + next: null, + }, + }, + ).as('getPartials') + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/partials/${redisPartials[0].id}`, + }, + { + statusCode: 200, + body: redisCEConfig, + }, + ).as('getRedisCEPartial') + const { t } = createI18n('en-us', english) + cy.mount(VueFormGenerator, { + props: { + schema: customPluginSchema, + model: customPluginModel, + enableRedisPartial: true, + }, + global: { + provide: { + 'kong-ui-forms-config': baseConfigKM, + }, + }, + }) cy.get('.vue-form-generator').should('exist') + cy.getTestId('collapse-trigger-label').click() + cy.getTestId('custom-plugin-redis-config-note').should('contain.text', t('redis.custom_plugin.alert')) + cy.getTestId('redis-config-select').click() + + // all redis partials(CE/EE) are present in the redis configuration selector in a custom plugin form + for (const partial of redisPartials) { + cy.getTestId(`redis-configuration-dropdown-item-${partial.name}`).should('exist') + } + + const [redisCEConfigDetail] = redisPartials + cy.getTestId(`redis-configuration-dropdown-item-${redisCEConfigDetail.name}`).click() + cy.get('.partial-config-card').getTestId('name-property-value').should('contain.text', redisCEConfigDetail.name) + }) }) diff --git a/packages/core/forms/src/components/forms/OIDCForm.vue b/packages/core/forms/src/components/forms/OIDCForm.vue index 3b1ef91c86..6d04e22fbc 100644 --- a/packages/core/forms/src/components/forms/OIDCForm.vue +++ b/packages/core/forms/src/components/forms/OIDCForm.vue @@ -91,10 +91,13 @@ @@ -104,6 +107,7 @@ + + + diff --git a/packages/entities/entities-redis-configurations/sandbox/index.ts b/packages/entities/entities-redis-configurations/sandbox/index.ts new file mode 100644 index 0000000000..6d843a19ac --- /dev/null +++ b/packages/entities/entities-redis-configurations/sandbox/index.ts @@ -0,0 +1,46 @@ +import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' +import Kongponents from '@kong/kongponents' +import '@kong/kongponents/dist/style.css' +import App from './App.vue' + +const app = createApp(App) + +const init = async () => { + const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'home', + component: () => import('./pages/HomePage.vue'), + }, + { + path: '/redis-configuration', + name: 'redis-configuration-list', + component: () => import('./pages/RedisConfigurationListPage.vue'), + }, + { + path: '/redis-configuration/create', + name: 'create-redis-configuration', + component: () => import('./pages/RedisConfigurationFormPage.vue'), + }, + { + path: '/redis-configuration/edit/:id', + name: 'edit-redis-configuration', + component: () => import('./pages/RedisConfigurationFormPage.vue'), + }, + { + path: '/redis-configuration/:id', + name: 'view-redis-configuration', + component: () => import('./pages/RedisConfigurationListPage.vue'), + }, + ], + }) + + app.use(Kongponents) + app.use(router) + app.mount('#app') +} + +init() diff --git a/packages/entities/entities-redis-configurations/sandbox/pages/HomePage.vue b/packages/entities/entities-redis-configurations/sandbox/pages/HomePage.vue new file mode 100644 index 0000000000..7b8bccecd5 --- /dev/null +++ b/packages/entities/entities-redis-configurations/sandbox/pages/HomePage.vue @@ -0,0 +1,9 @@ + diff --git a/packages/entities/entities-redis-configurations/sandbox/pages/RedisConfigurationFormPage.vue b/packages/entities/entities-redis-configurations/sandbox/pages/RedisConfigurationFormPage.vue new file mode 100644 index 0000000000..d13cd990b7 --- /dev/null +++ b/packages/entities/entities-redis-configurations/sandbox/pages/RedisConfigurationFormPage.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/entities/entities-redis-configurations/sandbox/pages/RedisConfigurationListPage.vue b/packages/entities/entities-redis-configurations/sandbox/pages/RedisConfigurationListPage.vue new file mode 100644 index 0000000000..7700bbf364 --- /dev/null +++ b/packages/entities/entities-redis-configurations/sandbox/pages/RedisConfigurationListPage.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/entities/entities-redis-configurations/sandbox/tsconfig.json b/packages/entities/entities-redis-configurations/sandbox/tsconfig.json new file mode 100644 index 0000000000..6b0bff7930 --- /dev/null +++ b/packages/entities/entities-redis-configurations/sandbox/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@entities-shared-sandbox/*": [ + "../../entities-shared/sandbox/shared/*" + ] + } + }, + "include": [ + "**/*.ts", + "**/*.vue", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/entities/entities-redis-configurations/src/components/ClusterNodes.vue b/packages/entities/entities-redis-configurations/src/components/ClusterNodes.vue new file mode 100644 index 0000000000..7cf44c7c22 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/components/ClusterNodes.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/entities/entities-redis-configurations/src/components/FieldArrayCardContainer.vue b/packages/entities/entities-redis-configurations/src/components/FieldArrayCardContainer.vue new file mode 100644 index 0000000000..5c8104e12a --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/components/FieldArrayCardContainer.vue @@ -0,0 +1,65 @@ + + + + + + + diff --git a/packages/entities/entities-redis-configurations/src/components/RedisConfigurationForm.vue b/packages/entities/entities-redis-configurations/src/components/RedisConfigurationForm.vue new file mode 100644 index 0000000000..fb3763be55 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/components/RedisConfigurationForm.vue @@ -0,0 +1,474 @@ + + + + + diff --git a/packages/entities/entities-redis-configurations/src/components/RedisConfigurationList.vue b/packages/entities/entities-redis-configurations/src/components/RedisConfigurationList.vue new file mode 100644 index 0000000000..148fa55cf2 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/components/RedisConfigurationList.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/packages/entities/entities-redis-configurations/src/components/SentinelNodes.vue b/packages/entities/entities-redis-configurations/src/components/SentinelNodes.vue new file mode 100644 index 0000000000..621e799cb2 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/components/SentinelNodes.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/entities/entities-redis-configurations/src/composables/index.ts b/packages/entities/entities-redis-configurations/src/composables/index.ts new file mode 100644 index 0000000000..3893dfc828 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/composables/index.ts @@ -0,0 +1,6 @@ +import useI18n from './useI18n' + +// All composables must be exported as part of the default object for Cypress test stubs +export default { + useI18n, +} diff --git a/packages/entities/entities-redis-configurations/src/composables/useI18n.ts b/packages/entities/entities-redis-configurations/src/composables/useI18n.ts new file mode 100644 index 0000000000..950be42ab7 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/composables/useI18n.ts @@ -0,0 +1,16 @@ +import { createI18n, i18nTComponent } from '@kong-ui-public/i18n' +import english from '../locales/en.json' + +interface UseI18nReturn { + i18n: ReturnType> + i18nT: ReturnType> +} + +export default function useI18n(): UseI18nReturn { + const i18n = createI18n('en-us', english) + + return { + i18n, + i18nT: i18nTComponent(i18n), // Translation component + } +} diff --git a/packages/entities/entities-redis-configurations/src/composables/useRedisConfigurationForm.ts b/packages/entities/entities-redis-configurations/src/composables/useRedisConfigurationForm.ts new file mode 100644 index 0000000000..16adc1eeb7 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/composables/useRedisConfigurationForm.ts @@ -0,0 +1,227 @@ +import { computed, reactive, ref, watch } from 'vue' +import { EntityBaseFormType, useAxios, useErrors } from '@kong-ui-public/entities-shared' + +import { getRedisType, mapRedisTypeToPartialType, standardize as s } from '../helpers' +import { RedisType } from '../types' +import { DEFAULT_REDIS_TYPE } from '../constants' +import endpoints from '../partials-endpoints' + +import type { KongManagerRedisConfigurationFormConfig, KonnectRedisConfigurationFormConfig, RedisConfigurationFormState } from '../types' + +export type Options = { + partialId?: string + config: KonnectRedisConfigurationFormConfig | KongManagerRedisConfigurationFormConfig +} + +export const useRedisConfigurationForm = (options: Options) => { + const { partialId, config } = options + const isEdit = !!partialId + const { axiosInstance } = useAxios(config.axiosRequestConfig) + const { getMessageFromError } = useErrors() + const formType = computed((): EntityBaseFormType => partialId + ? EntityBaseFormType.Edit + : EntityBaseFormType.Create) + + + const form = reactive({ + fields: { + name: '', + type: mapRedisTypeToPartialType(DEFAULT_REDIS_TYPE), + config: { + port: 6379, + host: '127.0.0.1', + database: 0, + username: '', + password: '', + ssl: false, + ssl_verify: false, + server_name: '', + connect_timeout: 2000, + send_timeout: 2000, + read_timeout: 2000, + sentinel_username: '', + sentinel_password: '', + keepalive_pool_size: 256, + keepalive_backlog: 0, + sentinel_master: '', + sentinel_nodes: [], + cluster_nodes: [], + cluster_max_redirections: 0, + connection_is_proxied: false, + timeout: 2000, + }, + }, + readonly: false, + errorMessage: '', + }) + const userSelectedRedisType = ref() + const redisType = computed(() => { + if (isEdit) { + return getRedisType(form.fields) + } + if (userSelectedRedisType.value) { + return userSelectedRedisType.value + } + return DEFAULT_REDIS_TYPE + }) + + watch(redisType, (newValue) => { + form.fields.type = mapRedisTypeToPartialType(newValue) + }) + + const canSubmit = computed(() => { + if (!form.fields.name.length) { + return false + } + + const { config } = form.fields + + switch (redisType.value) { + case RedisType.HOST_PORT_CE: + return config.host.length > 0 && config.port > 0 + case RedisType.HOST_PORT_EE: + return config.host.length > 0 && config.port > 0 + case RedisType.CLUSTER: + return !!config.cluster_nodes.length && config.cluster_nodes.every((node) => node.ip.length > 0 && node.port > 0) + case RedisType.SENTINEL: + return !!config.sentinel_nodes.length + && config.sentinel_nodes.every((node) => node.host.length > 0 && node.port > 0) + && config.sentinel_master.length > 0 + && config.sentinel_role + && config.sentinel_role.length > 0 + default: + throw new Error('Invalid redis type') + } + }) + + const payload = computed(() => { + switch (redisType.value) { + case RedisType.HOST_PORT_CE: + return { + name: form.fields.name, + type: form.fields.type, + config: { + host: form.fields.config.host, + port: form.fields.config.port, + timeout: s.int(form.fields.config.timeout), + username: s.str(form.fields.config.username, null), + database: s.int(form.fields.config.database), + password: s.str(form.fields.config.password, null), + ssl: form.fields.config.ssl, + ssl_verify: form.fields.config.ssl_verify, + server_name: s.str(form.fields.config.server_name, null), + }, + } + case RedisType.HOST_PORT_EE: + return { + name: form.fields.name, + type: form.fields.type, + config: { + connect_timeout: s.int(form.fields.config.connect_timeout), + connection_is_proxied: form.fields.config.connection_is_proxied, + database: s.int(form.fields.config.database), + host: form.fields.config.host, + keepalive_backlog: s.int(form.fields.config.keepalive_backlog), + keepalive_pool_size: s.int(form.fields.config.keepalive_pool_size), + password: s.str(form.fields.config.password, null), + port: s.int(form.fields.config.port), + read_timeout: s.int(form.fields.config.read_timeout), + send_timeout: s.int(form.fields.config.send_timeout), + server_name: s.str(form.fields.config.server_name, null), + ssl_verify: form.fields.config.ssl_verify, + ssl: form.fields.config.ssl, + username: s.str(form.fields.config.username, null), + }, + } + case RedisType.CLUSTER: + return { + name: form.fields.name, + type: form.fields.type, + config: { + cluster_nodes: s.clusterNodes(form.fields.config.cluster_nodes), + cluster_max_redirections: s.int(form.fields.config.cluster_max_redirections), + username: s.str(form.fields.config.username, null), + password: s.str(form.fields.config.password, null), + ssl: form.fields.config.ssl, + ssl_verify: form.fields.config.ssl_verify, + server_name: s.str(form.fields.config.server_name, null), + connect_timeout: s.int(form.fields.config.connect_timeout), + send_timeout: s.int(form.fields.config.send_timeout), + read_timeout: s.int(form.fields.config.read_timeout), + keepalive_pool_size: s.int(form.fields.config.keepalive_pool_size), + keepalive_backlog: s.int(form.fields.config.keepalive_backlog), + connection_is_proxied: form.fields.config.connection_is_proxied, + }, + } + case RedisType.SENTINEL: + return { + name: form.fields.name, + type: form.fields.type, + config: { + sentinel_master: s.str(form.fields.config.sentinel_master, null), + sentinel_nodes: s.sentinelNodes(form.fields.config.sentinel_nodes), + sentinel_role: s.str(form.fields.config.sentinel_role, null), + username: s.str(form.fields.config.username, null), + password: s.str(form.fields.config.password, null), + ssl: form.fields.config.ssl, + ssl_verify: form.fields.config.ssl_verify, + server_name: s.str(form.fields.config.server_name, null), + connect_timeout: s.int(form.fields.config.connect_timeout), + send_timeout: s.int(form.fields.config.send_timeout), + read_timeout: s.int(form.fields.config.read_timeout), + keepalive_pool_size: s.int(form.fields.config.keepalive_pool_size), + keepalive_backlog: s.int(form.fields.config.keepalive_backlog), + connection_is_proxied: form.fields.config.connection_is_proxied, + }, + } + default: + throw new Error('Invalid redis type') + } + }) + + const submitUrl = computed(() => { + let url = `${config.apiBaseUrl}${endpoints.form[config.app][formType.value]}` + + if (config.app === 'konnect') { + url = url.replace(/{controlPlaneId}/gi, config?.controlPlaneId || '') + } else if (config.app === 'kongManager') { + url = url.replace(/\/{workspace}/gi, config?.workspace ? `/${config.workspace}` : '') + } + + // Always replace the id when editing + url = url.replace(/{id}/gi, partialId || '') + + return url + }) + + const fetchUrl = computed(() => endpoints.form[config?.app]?.edit) + + const submit = async () => { + try { + form.readonly = true + form.errorMessage = '' + + if (formType.value === EntityBaseFormType.Create) { + await axiosInstance.post(submitUrl.value, payload.value) + } else { + await axiosInstance.patch(submitUrl.value, payload.value) + } + } catch (e: unknown) { + form.errorMessage = getMessageFromError(e) + form.readonly = false + throw e + } + } + + return { + form, + canSubmit, + payload, + isEdit, + redisType, + userSelectedRedisType, + formType, + fetchUrl, + submit, + } +} diff --git a/packages/entities/entities-redis-configurations/src/constants.ts b/packages/entities/entities-redis-configurations/src/constants.ts new file mode 100644 index 0000000000..216f56baf6 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/constants.ts @@ -0,0 +1,14 @@ +import { RedisType } from './types' +import type { ClusterNode, SentinelNode } from './types' + +export const DEFAULT_CLUSTER_NODE: ClusterNode = { + ip: '127.0.0.1', + port: 6379, +} + +export const DEFAULT_SENTINEL_NODE: SentinelNode = { + host: '127.0.0.1', + port: 6379, +} + +export const DEFAULT_REDIS_TYPE = RedisType.HOST_PORT_CE diff --git a/packages/entities/entities-redis-configurations/src/global-components.d.ts b/packages/entities/entities-redis-configurations/src/global-components.d.ts new file mode 100644 index 0000000000..2f0048d672 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/global-components.d.ts @@ -0,0 +1,2 @@ +// Import globally available components +import '@kong/kongponents/dist/types/global-components' diff --git a/packages/entities/entities-redis-configurations/src/helpers.ts b/packages/entities/entities-redis-configurations/src/helpers.ts new file mode 100644 index 0000000000..9b40fdb135 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/helpers.ts @@ -0,0 +1,68 @@ +import { v4 as uuidv4 } from 'uuid' + +import { DEFAULT_CLUSTER_NODE, DEFAULT_SENTINEL_NODE } from './constants' +import { PartialType, type ClusterNode, type Identifiable, type RedisConfigurationFields, type SentinelNode } from './types' +import { RedisType } from './types' + +export const shallowCopyWithId = >(node: T): Identifiable => { + return { ...node, id: uuidv4() } +} + +export const shallowCopyWithoutId = (node: T): Omit => { + const { id, ...rest } = node + return rest +} + +export const genDefaultSentinelNode = () => shallowCopyWithId(DEFAULT_SENTINEL_NODE) + +export const genDefaultClusterNode = () => shallowCopyWithId(DEFAULT_CLUSTER_NODE) + +export const getRedisType = (fields: RedisConfigurationFields): RedisType => { + if (fields.type === PartialType.REDIS_CE) { + return RedisType.HOST_PORT_CE + } + + if (fields.config.sentinel_nodes?.length) { + return RedisType.SENTINEL + } + + if (fields.config.cluster_nodes?.length) { + return RedisType.CLUSTER + } + + return RedisType.HOST_PORT_EE +} + +export const mapRedisTypeToPartialType = (type: RedisType): PartialType => { + return type === RedisType.HOST_PORT_CE ? PartialType.REDIS_CE : PartialType.REDIS_EE +} + +export const standardize = { + int(value: string | number | undefined | null, defaultValue?: T): number | T { + if (value === undefined || value === null) { + return defaultValue as T + } + return parseInt(value.toString(), 10) + }, + + str(value: string | number | undefined | null, defaultValue?: T): string | T { + if (value === undefined || value === null || value === '') { + return defaultValue as T + } + return value.toString() + }, + + clusterNodes(nodes: Identifiable[]): ClusterNode[] { + return nodes.map(node => ({ + ...shallowCopyWithoutId(node), + port: standardize.int(node.port)!, + })) + }, + + sentinelNodes(nodes: Identifiable[]): SentinelNode[] { + return nodes.map(node => ({ + ...shallowCopyWithoutId(node), + port: standardize.int(node.port)!, + })) + }, +} diff --git a/packages/entities/entities-redis-configurations/src/index.ts b/packages/entities/entities-redis-configurations/src/index.ts new file mode 100644 index 0000000000..03b7d0acae --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/index.ts @@ -0,0 +1,9 @@ +import RedisConfigurationForm from './components/RedisConfigurationForm.vue' +import RedisConfigurationList from './components/RedisConfigurationList.vue' + +export { + RedisConfigurationForm, + RedisConfigurationList, +} + +export * from './types' diff --git a/packages/entities/entities-redis-configurations/src/locales/en.json b/packages/entities/entities-redis-configurations/src/locales/en.json new file mode 100644 index 0000000000..5237c37592 --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/locales/en.json @@ -0,0 +1,175 @@ +{ + "actions": { + "create": "New Redis Configuration", + "copy_id": "Copy ID", + "copy_json": "Copy JSON", + "edit": "Edit", + "delete": "Delete", + "view": "View Details", + "loading": "Loading..." + }, + "form": { + "sections": { + "type": { + "title": "Redis type", + "description": "Both Enterprise and Open Source plugins support Redis. Enterprise plugins can connect to a standalone Redis instance (host/port), Cluster, or Sentinel, while Open Source plugins support only a simplified host/port configuration." + }, + "general": { + "title": "General information", + "description": "Name your Redis configuration." + }, + "connection": { + "title": "Connection settings", + "description": "Define the Redis server’s host, port, authentication, and timeout options for establishing a connection." + }, + "cluster": { + "title": "Cluster configuration", + "description": "Enables data sharding and distribution across multiple Redis nodes, allowing for scalability and load balancing." + }, + "tls": { + "title": "TLS settings", + "description": "Configure secure connections to Redis, including SSL and verification options." + }, + "keepalive": { + "title": "Keepalive configuration", + "description": "Keepalive reuses active connections to Redis, improving performance and efficiency." + }, + "read_write_configuration": { + "title": "Read/Write configuration", + "description": "Set timeouts for reading from and writing to the Redis server to control data transmission reliability." + }, + "sentinel_configuration": { + "title": "Sentinel configuration", + "description": "Manages Redis failover and high availability by connecting to Sentinel nodes that monitor and switch masters." + } + }, + "fields": { + "type": { + "label": "Redis type" + }, + "name": { + "label": "Name", + "placeholder": "Enter unique name" + }, + "host": { + "label": "Host", + "tooltip": "A string representing a host name, such as example.com." + }, + "port": { + "label": "Port", + "tooltip": "An integer representing a port number between 0 and 65535, inclusive." + }, + "database": { + "label": "Database", + "tooltip": "Database to use for the Redis connection when using the `redis` strategy" + }, + "password": { + "label": "Password", + "tooltip": "Password to use for Redis connections. If undefined, no AUTH commands are sent to Redis." + }, + "username": { + "label": "Username", + "tooltip": "Username to use for Redis connections. If undefined, ACL authentication won't be performed. This requires Redis v6.0.0+. To be compatible with Redis v5.x.y, you can set it to `default`." + }, + "ssl": { + "label": "SSL", + "description": "If set to true, uses SSL to connect to Redis." + }, + "ssl_verify": { + "label": "SSL Verify", + "description": "If set to true, verifies the validity of the server SSL certificate. If setting this parameter, also configure lua_ssl_trusted_certificate in kong. conf to specify the CA (or server) certificate used by your Redis server. You may also need to configure lua_ss1_verify_depth accordingly." + }, + "server_name": { + "label": "Server name", + "tooltip": "A string representing an SNI (server name indication) value for TLS." + }, + "keepalive_backlog": { + "label": "Keepalive backlog", + "tooltip": "Limits the total number of opened connections for a pool. If the connection pool is full, connection queues above the limit go into the backlog queue. If the backlog queue is full, subsequent connect operations fail and return `nil`. Queued operations (subject to set timeouts) resume once the number of connections in the pool is less than `keepalive_pool_size`. If latency is high or throughput is low, try increasing this value. Empirically, this value is larger than `keepalive_pool_size`." + }, + "keepalive_pool_size": { + "label": "Keepalive pool size", + "tooltip": "The size limit for every cosocket connection pool associated with every remote server, per worker process. If neither `keepalive_pool_size` nor `keepalive_backlog` is specified, no pool is created. If `keepalive_pool_size` isn't specified but `keepalive_backlog` is specified, then the pool uses the default value. Try to increase (e.g. 512) this value if latency is high or throughput is low." + }, + "read_timeout": { + "label": "Read timeout" + }, + "send_timeout": { + "label": "Send timeout" + }, + "connect_timeout": { + "label": "Connect timeout" + }, + "sentinel_master": { + "label": "Sentinel master", + "tooltip": "Sentinel master to use for Redis connections. Defining this value implies using Redis Sentinel." + }, + "sentinel_role": { + "label": "Sentinel role", + "tooltip": "Sentinel role to use for Redis connections when the `redis` strategy is defined. Defining this value implies using Redis Sentinel." + }, + "sentinel_username": { + "label": "Sentinel username", + "tooltip": "Sentinel username to authenticate with a Redis Sentinel instance. If undefined, ACL authentication won't be performed. This requires Redis v6.2.0+." + }, + "sentinel_password": { + "label": "Sentinel password", + "tooltip": "Sentinel password to authenticate with a Redis Sentinel instance. If undefined, no AUTH commands are sent to Redis Sentinels." + }, + "cluster_node_ip": { + "label": "IP", + "tooltip": "A string representing a host name, such as example.com." + }, + "cluster_node_port": { + "label": "Port", + "tooltip": "An integer representing a port number between 0 and 65535, inclusive." + }, + "cluster_max_redirections": { + "label": "Cluster max redirections", + "tooltip": "Maximum retry attempts for redirection." + }, + "timeout": { + "label": "Timeout", + "tooltip": "redis schema field `timeout` is deprecated, use `connect_timeout`, `send_timeout` and `read_timeout`" + }, + "connection_is_proxied": { + "label": "Connection is proxied", + "tooltip": "If the connection to Redis is proxied (e.g. Envoy), set it `true`. Set the `host` and `port` to point to the proxy address." + }, + "cluster_nodes": { + "title": "Cluster nodes", + "tooltip": "This is a really long tooltip. Hopefully we won't have anything this long but we might. I wonder how it handles long inputs", + "add_button": "New item" + }, + "sentinel_nodes": { + "title": "Sentinel nodes", + "tooltip": "Sentinel node addresses to use for Redis connections when the `redis` strategy is defined. Defining this field implies using a Redis Sentinel. The minimum length of the array is 1 element.", + "add_button": "New item" + }, + "sentinel_node_host": { + "label": "Host", + "tooltip": "A string representing a host name, such as example.com." + }, + "sentinel_node_port": { + "label": "Port", + "tooltip": "An integer representing a port number between 0 and 65535, inclusive." + } + }, + "options": { + "type": { + "host_port": "Host/Port", + "cluster": "Cluster", + "sentinel": "Sentinel", + "open_source": "Open Source", + "enterprise": "Enterprise", + "suffix_open_source": " (Open Source)", + "suffix_enterprise": " (Enterprise)" + }, + "sentinel_role": { + "master": "master", + "slave": "slave", + "any": "any" + } + } + } +} diff --git a/packages/entities/entities-redis-configurations/src/partials-endpoints.ts b/packages/entities/entities-redis-configurations/src/partials-endpoints.ts new file mode 100644 index 0000000000..e07470a47d --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/partials-endpoints.ts @@ -0,0 +1,23 @@ +const konnectBaseApiUrl = '/v2/control-planes/{controlPlaneId}/core-entities' +const KMBaseApiUrl = '/{workspace}' + +export default { + list: { + konnect: { + all: `${konnectBaseApiUrl}/partials`, + }, + kongManager: { + all: `${KMBaseApiUrl}/partials`, + }, + }, + form: { + konnect: { + create: `${konnectBaseApiUrl}/partials`, + edit: `${konnectBaseApiUrl}/partials/{id}`, + }, + kongManager: { + create: `${KMBaseApiUrl}/partials`, + edit: `${KMBaseApiUrl}/partials/{id}`, + }, + }, +} diff --git a/packages/entities/entities-redis-configurations/src/types/index.ts b/packages/entities/entities-redis-configurations/src/types/index.ts new file mode 100644 index 0000000000..29d46c642c --- /dev/null +++ b/packages/entities/entities-redis-configurations/src/types/index.ts @@ -0,0 +1,85 @@ +import type { KonnectBaseFormConfig, KongManagerBaseFormConfig, KonnectBaseTableConfig, KongManagerBaseTableConfig } from '@kong-ui-public/entities-shared' +import type { RouteLocationRaw } from 'vue-router' + +export interface KonnectRedisConfigurationFormConfig extends KonnectBaseFormConfig { } +export interface KongManagerRedisConfigurationFormConfig extends KongManagerBaseFormConfig { } + +export interface BaseRedisConfigurationListConfig { + /** Route for creating a redis configuration */ + createRoute: RouteLocationRaw + /** A function that returns the route for viewing a redis configuration */ + getViewRoute: (id: string) => RouteLocationRaw + /** A function that returns the route for editing a redis configuration */ + getEditRoute: (id: string) => RouteLocationRaw +} + +/** Konnect redis configuration list config */ +export interface KonnectRedisConfigurationListConfig extends KonnectBaseTableConfig, BaseRedisConfigurationListConfig { } + +/** Kong Manager redis configuration list config */ +export interface KongManagerRedisConfigurationListConfig extends KongManagerBaseTableConfig, BaseRedisConfigurationListConfig { } + +export enum RedisType { + HOST_PORT_CE, + HOST_PORT_EE, + SENTINEL, + CLUSTER, +} + +export enum PartialType { + REDIS_CE = 'redis-ce', + REDIS_EE = 'redis-ee', +} + +export interface SentinelNode { + host: string + port: number +} + +export type Identifiable = T & { id: string } + +export interface ClusterNode { + ip: string + port: number +} + +export interface RedisConfigurationFields { + name: string + type: PartialType + config: { + port: number + host: string + database: number + username: string + password: string + ssl: boolean + ssl_verify: boolean + server_name?: string + timeout: number // todo + + connect_timeout: number + send_timeout: number + read_timeout: number + sentinel_username: string + sentinel_password: string + keepalive_pool_size: number + keepalive_backlog: number + sentinel_master: string + sentinel_role?: 'master' | 'slave' | 'any' + sentinel_nodes: Identifiable[] + cluster_nodes: Identifiable[] + cluster_max_redirections: number + connection_is_proxied: boolean + } +} + +export interface RedisConfigurationFormState { + fields: RedisConfigurationFields + readonly: boolean + errorMessage: string +} + +export interface EntityRow extends Record { + id: string + name: string +} diff --git a/packages/entities/entities-redis-configurations/tsconfig.build.json b/packages/entities/entities-redis-configurations/tsconfig.build.json new file mode 100644 index 0000000000..577de9d6ae --- /dev/null +++ b/packages/entities/entities-redis-configurations/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "exclude": [ + "src/**/*.cy.ts", + "src/**/*.spec.ts", + "sandbox", + "dist" + ] +} diff --git a/packages/entities/entities-redis-configurations/tsconfig.json b/packages/entities/entities-redis-configurations/tsconfig.json new file mode 100644 index 0000000000..e34e90e4e4 --- /dev/null +++ b/packages/entities/entities-redis-configurations/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "declarationDir": "dist/types", + "types": [ + "node", + "vite/client", + "cypress", + "cypress/vue", + "../../../cypress/support" + ] + }, + "include": [ + "src/**/*", + "sandbox/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/entities/entities-redis-configurations/vite.config.ts b/packages/entities/entities-redis-configurations/vite.config.ts new file mode 100644 index 0000000000..a6bf7b460f --- /dev/null +++ b/packages/entities/entities-redis-configurations/vite.config.ts @@ -0,0 +1,42 @@ +import sharedViteConfig, { getApiProxies, sanitizePackageName } from '../../../vite.config.shared' +import { resolve } from 'path' +import { defineConfig, mergeConfig } from 'vite' + +// Package name MUST always match the kebab-case package name inside the component's package.json file and the name of your `/packages/{package-name}` directory +const packageName = 'entities-redis-configurations' +const sanitizedPackageName = sanitizePackageName(packageName) + +// Merge the shared Vite config with the local one defined below +const config = mergeConfig(sharedViteConfig, defineConfig({ + build: { + lib: { + // The kebab-case name of the exposed global variable. MUST be in the format `kong-ui-public-{package-name}` + // Example: name: 'kong-ui-public-demo-component' + name: `kong-ui-public-${sanitizedPackageName}`, + entry: resolve(__dirname, './src/index.ts'), + fileName: (format) => `${sanitizedPackageName}.${format}.js`, + }, + rollupOptions: { + external: [ + // '@kong-ui-public/entities-vaults/dist/style.css', + // '@kong-ui-public/entities-vaults', + ], + }, + }, + server: { + proxy: { + // Add the API proxies to inject the Authorization header + ...getApiProxies(), + }, + }, +})) + +// If we are trying to preview a build of the local `package/entities-redis-configurations/sandbox` directory, +// unset the lib, rollupOptions.external and rollupOptions.output.globals properties +if (process.env.USE_SANDBOX) { + config.build.lib = undefined + config.build.rollupOptions.external = undefined + config.build.rollupOptions.output.global = undefined +} + +export default config diff --git a/packages/entities/entities-shared/src/components/entity-base-config-card/EntityBaseConfigCard.vue b/packages/entities/entities-shared/src/components/entity-base-config-card/EntityBaseConfigCard.vue index 235bde2bea..15d0b4e345 100644 --- a/packages/entities/entities-shared/src/components/entity-base-config-card/EntityBaseConfigCard.vue +++ b/packages/entities/entities-shared/src/components/entity-base-config-card/EntityBaseConfigCard.vue @@ -292,6 +292,12 @@ const DEFAULT_BASIC_FIELDS_CONFIGURATION: DefaultCommonFieldsConfigurationSchema order: -1, // the last property displayed section: ConfigurationSchemaSection.Basic, }, + partials: { + type: ConfigurationSchemaType.LinkInternal, + label: t('baseConfigCard.commonFields.partial_label'), + order: -1, // the last property displayed + section: ConfigurationSchemaSection.Basic, + }, } const isLoading = ref(false) diff --git a/packages/entities/entities-shared/src/locales/en.json b/packages/entities/entities-shared/src/locales/en.json index d89ae7205d..f20dc64a01 100644 --- a/packages/entities/entities-shared/src/locales/en.json +++ b/packages/entities/entities-shared/src/locales/en.json @@ -53,7 +53,8 @@ "updated_at_label": "Last Updated", "created_at_label": "Created", "tags_label": "Tags", - "link": "Link" + "link": "Link", + "partial_label": "Redis Configuration" }, "statusBadge": { "enabledLabel": "Enabled", diff --git a/packages/entities/entities-shared/src/types/entity-base-config-card.ts b/packages/entities/entities-shared/src/types/entity-base-config-card.ts index da7fec3bf4..f1d473d8af 100644 --- a/packages/entities/entities-shared/src/types/entity-base-config-card.ts +++ b/packages/entities/entities-shared/src/types/entity-base-config-card.ts @@ -19,6 +19,7 @@ export enum SupportedEntityType { Upstream = 'upstream', Target = 'target', Vault = 'vault', + RedisConfiguration = 'redis_configuration', // todo(zehao): not sure // Use this for any entity type that is not supported by terraform // If entityType is 'other' terraform scripts will not be available // Note: This is currently only supported by EntityBaseForm not EntityBaseConfigCard!! @@ -99,6 +100,7 @@ export interface DefaultCommonFieldsConfigurationSchema { updated_at: ConfigurationSchemaItem created_at: ConfigurationSchemaItem tags: ConfigurationSchemaItem + partials: ConfigurationSchemaItem } export interface ComponentAttrsData { diff --git a/packages/entities/entities-vaults/src/components/VaultSecretPickerProvider.vue b/packages/entities/entities-vaults/src/components/VaultSecretPickerProvider.vue index c2faa00bd5..42dddcf5ec 100644 --- a/packages/entities/entities-vaults/src/components/VaultSecretPickerProvider.vue +++ b/packages/entities/entities-vaults/src/components/VaultSecretPickerProvider.vue @@ -6,8 +6,8 @@ >