mirror of
https://github.com/FunnyWolf/agentic-soc-platform.git
synced 2026-08-22 13:12:56 +02:00
feat(custom): support typed variables
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+1
-1
Submodule asp-doc updated: c87ef70df2...528ff15c2e
@@ -0,0 +1,57 @@
|
||||
# Generated by Django 6.0.7 on 2026-08-03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_string_values(apps, schema_editor):
|
||||
CustomVariable = apps.get_model("settings", "CustomVariable")
|
||||
for variable in CustomVariable.objects.only("id", "value").iterator():
|
||||
variable.typed_value = variable.value
|
||||
variable.save(update_fields=["typed_value"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("settings", "0005_customvariable"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="customvariable",
|
||||
name="value_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("string", "String"),
|
||||
("integer", "Integer"),
|
||||
("float", "Float"),
|
||||
("boolean", "Boolean"),
|
||||
("list", "List"),
|
||||
("dictionary", "Dictionary"),
|
||||
],
|
||||
default="string",
|
||||
max_length=16,
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="customvariable",
|
||||
name="typed_value",
|
||||
field=models.JSONField(null=True),
|
||||
),
|
||||
migrations.RunPython(copy_string_values),
|
||||
migrations.RemoveField(
|
||||
model_name="customvariable",
|
||||
name="value",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="customvariable",
|
||||
old_name="typed_value",
|
||||
new_name="value",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="customvariable",
|
||||
name="value",
|
||||
field=models.JSONField(),
|
||||
),
|
||||
]
|
||||
@@ -160,6 +160,14 @@ class RuntimeConfig(models.Model):
|
||||
|
||||
|
||||
class CustomVariable(models.Model):
|
||||
class ValueType(models.TextChoices):
|
||||
STRING = "string", "String"
|
||||
INTEGER = "integer", "Integer"
|
||||
FLOAT = "float", "Float"
|
||||
BOOLEAN = "boolean", "Boolean"
|
||||
LIST = "list", "List"
|
||||
DICTIONARY = "dictionary", "Dictionary"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
key = models.CharField(
|
||||
max_length=128,
|
||||
@@ -171,7 +179,8 @@ class CustomVariable(models.Model):
|
||||
)
|
||||
],
|
||||
)
|
||||
value = models.TextField()
|
||||
value_type = models.CharField(max_length=16, choices=ValueType.choices)
|
||||
value = models.JSONField()
|
||||
is_secret = models.BooleanField(default=False)
|
||||
description = models.TextField(blank=True, default="")
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import json
|
||||
import math
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import (
|
||||
@@ -13,9 +16,94 @@ from .models import (
|
||||
|
||||
|
||||
MAX_CUSTOM_VARIABLE_VALUE_BYTES = 65_536
|
||||
MAX_CUSTOM_VARIABLE_DEPTH = 20
|
||||
MAX_SAFE_INTEGER = 9_007_199_254_740_991
|
||||
|
||||
|
||||
def _validate_structured_custom_variable(value, depth=0):
|
||||
if isinstance(value, list):
|
||||
if depth > MAX_CUSTOM_VARIABLE_DEPTH:
|
||||
raise serializers.ValidationError(
|
||||
f"Value cannot exceed {MAX_CUSTOM_VARIABLE_DEPTH} levels of nesting."
|
||||
)
|
||||
for item in value:
|
||||
_validate_structured_custom_variable(item, depth + 1)
|
||||
return
|
||||
if isinstance(value, dict):
|
||||
if depth > MAX_CUSTOM_VARIABLE_DEPTH:
|
||||
raise serializers.ValidationError(
|
||||
f"Value cannot exceed {MAX_CUSTOM_VARIABLE_DEPTH} levels of nesting."
|
||||
)
|
||||
if any(not isinstance(key, str) for key in value):
|
||||
raise serializers.ValidationError("Dictionary keys must be strings.")
|
||||
for item in value.values():
|
||||
_validate_structured_custom_variable(item, depth + 1)
|
||||
return
|
||||
if value is None or type(value) in {str, int, float, bool}:
|
||||
return
|
||||
raise serializers.ValidationError("Value must contain valid JSON values.")
|
||||
|
||||
|
||||
def _validate_custom_variable_value(value_type, value):
|
||||
if value_type == CustomVariable.ValueType.STRING:
|
||||
if not isinstance(value, str):
|
||||
raise serializers.ValidationError("Value must be a string.")
|
||||
if value == "":
|
||||
raise serializers.ValidationError("Value cannot be empty.")
|
||||
encoded_value = value.encode("utf-8")
|
||||
elif value_type == CustomVariable.ValueType.INTEGER:
|
||||
if type(value) is not int:
|
||||
raise serializers.ValidationError("Value must be an integer.")
|
||||
if not -MAX_SAFE_INTEGER <= value <= MAX_SAFE_INTEGER:
|
||||
raise serializers.ValidationError(
|
||||
f"Value must be between {-MAX_SAFE_INTEGER:,} and {MAX_SAFE_INTEGER:,}."
|
||||
)
|
||||
encoded_value = json.dumps(value).encode("utf-8")
|
||||
elif value_type == CustomVariable.ValueType.FLOAT:
|
||||
if type(value) not in {int, float}:
|
||||
raise serializers.ValidationError("Value must be a number.")
|
||||
value = float(value)
|
||||
if not math.isfinite(value):
|
||||
raise serializers.ValidationError("Value must be a finite number.")
|
||||
encoded_value = json.dumps(value).encode("utf-8")
|
||||
elif value_type == CustomVariable.ValueType.BOOLEAN:
|
||||
if type(value) is not bool:
|
||||
raise serializers.ValidationError("Value must be a boolean.")
|
||||
encoded_value = json.dumps(value).encode("utf-8")
|
||||
elif value_type == CustomVariable.ValueType.LIST:
|
||||
if not isinstance(value, list):
|
||||
raise serializers.ValidationError("Value must be a list.")
|
||||
encoded_value = _encode_structured_custom_variable(value)
|
||||
elif value_type == CustomVariable.ValueType.DICTIONARY:
|
||||
if not isinstance(value, dict):
|
||||
raise serializers.ValidationError("Value must be a dictionary.")
|
||||
encoded_value = _encode_structured_custom_variable(value)
|
||||
else:
|
||||
raise serializers.ValidationError("Unsupported value type.")
|
||||
|
||||
if len(encoded_value) > MAX_CUSTOM_VARIABLE_VALUE_BYTES:
|
||||
raise serializers.ValidationError(
|
||||
f"Value cannot exceed {MAX_CUSTOM_VARIABLE_VALUE_BYTES:,} UTF-8 bytes."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _encode_structured_custom_variable(value):
|
||||
_validate_structured_custom_variable(value, depth=1)
|
||||
try:
|
||||
serialized = json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise serializers.ValidationError("Value must contain valid JSON values.") from exc
|
||||
return serialized.encode("utf-8")
|
||||
|
||||
|
||||
class CustomVariableSerializer(serializers.ModelSerializer):
|
||||
value = serializers.JSONField(required=False)
|
||||
value_configured = serializers.SerializerMethodField()
|
||||
confirm_secret_exposure = serializers.BooleanField(write_only=True, required=False, default=False)
|
||||
|
||||
@@ -24,6 +112,7 @@ class CustomVariableSerializer(serializers.ModelSerializer):
|
||||
fields = (
|
||||
"id",
|
||||
"key",
|
||||
"value_type",
|
||||
"value",
|
||||
"value_configured",
|
||||
"is_secret",
|
||||
@@ -35,50 +124,65 @@ class CustomVariableSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
read_only_fields = ("id", "value_configured", "created_at", "updated_at")
|
||||
extra_kwargs = {
|
||||
"value": {"required": False, "trim_whitespace": False, "allow_blank": False},
|
||||
"description": {"required": False, "allow_blank": True},
|
||||
}
|
||||
|
||||
def get_value_configured(self, obj):
|
||||
return bool(obj.value)
|
||||
return obj.value is not None
|
||||
|
||||
def validate_key(self, value):
|
||||
if self.instance is not None and value != self.instance.key:
|
||||
raise serializers.ValidationError("Key cannot be changed.")
|
||||
return value
|
||||
|
||||
def validate_value(self, value):
|
||||
if value == "":
|
||||
raise serializers.ValidationError("Value cannot be empty.")
|
||||
if len(value.encode("utf-8")) > MAX_CUSTOM_VARIABLE_VALUE_BYTES:
|
||||
raise serializers.ValidationError(
|
||||
f"Value cannot exceed {MAX_CUSTOM_VARIABLE_VALUE_BYTES:,} UTF-8 bytes."
|
||||
)
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
confirmation = attrs.pop("confirm_secret_exposure", False)
|
||||
|
||||
if self.instance is None:
|
||||
if "value_type" not in attrs:
|
||||
raise serializers.ValidationError({"value_type": "Value type is required."})
|
||||
if "value" not in attrs:
|
||||
raise serializers.ValidationError({"value": "Value is required."})
|
||||
return attrs
|
||||
elif attrs.get("value_type", self.instance.value_type) != self.instance.value_type:
|
||||
if "value" not in attrs:
|
||||
raise serializers.ValidationError({
|
||||
"value": "Value is required when changing the value type."
|
||||
})
|
||||
|
||||
if not self.partial and "value" not in attrs and not self.instance.is_secret:
|
||||
raise serializers.ValidationError({"value": "Value is required."})
|
||||
|
||||
next_is_secret = attrs.get("is_secret", self.instance.is_secret)
|
||||
if self.instance.is_secret and not next_is_secret and not confirmation:
|
||||
value_type = attrs.get(
|
||||
"value_type",
|
||||
self.instance.value_type if self.instance else None,
|
||||
)
|
||||
next_is_secret = attrs.get(
|
||||
"is_secret",
|
||||
self.instance.is_secret if self.instance else False,
|
||||
)
|
||||
if next_is_secret and value_type != CustomVariable.ValueType.STRING:
|
||||
raise serializers.ValidationError({
|
||||
"is_secret": "Only String variables can be secret."
|
||||
})
|
||||
if (
|
||||
self.instance is not None
|
||||
and self.instance.is_secret
|
||||
and not next_is_secret
|
||||
and not confirmation
|
||||
):
|
||||
raise serializers.ValidationError({
|
||||
"confirm_secret_exposure": "Confirm that this secret value may be exposed."
|
||||
})
|
||||
|
||||
if "value" in attrs:
|
||||
try:
|
||||
attrs["value"] = _validate_custom_variable_value(value_type, attrs["value"])
|
||||
except serializers.ValidationError as exc:
|
||||
raise serializers.ValidationError({"value": exc.detail}) from exc
|
||||
return attrs
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
if instance.is_secret:
|
||||
data["value"] = ""
|
||||
data["value"] = None
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -66,7 +66,14 @@ RUNTIME_AUDIT_FIELDS = (
|
||||
"stream_maxlen",
|
||||
"dashboard_refresh_interval_seconds",
|
||||
)
|
||||
CUSTOM_VARIABLE_AUDIT_FIELDS = ("key", "value", "is_secret", "description", "enabled")
|
||||
CUSTOM_VARIABLE_AUDIT_FIELDS = (
|
||||
"key",
|
||||
"value_type",
|
||||
"value",
|
||||
"is_secret",
|
||||
"description",
|
||||
"enabled",
|
||||
)
|
||||
|
||||
|
||||
def _snapshot(instance, fields):
|
||||
@@ -211,11 +218,19 @@ class CustomVariableViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [permissions.IsAuthenticated, IsAdmin]
|
||||
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter, AdvancedFilterBackend)
|
||||
search_fields = ("key", "description")
|
||||
filterset_fields = ("is_secret", "enabled")
|
||||
ordering_fields = ("key", "is_secret", "enabled", "created_at", "updated_at")
|
||||
filterset_fields = ("value_type", "is_secret", "enabled")
|
||||
ordering_fields = (
|
||||
"key",
|
||||
"value_type",
|
||||
"is_secret",
|
||||
"enabled",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
advanced_filter_fields = {
|
||||
"key": "text",
|
||||
"description": "text",
|
||||
"value_type": "select",
|
||||
"is_secret": "select",
|
||||
"enabled": "select",
|
||||
"created_at": "date",
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^2.6.7",
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@uiw/react-codemirror": "^4.25.11",
|
||||
"@uiw/react-json-view": "2.0.0-alpha.43",
|
||||
"@uiw/react-md-editor": "^4.1.1",
|
||||
"antd": "^6.5.1",
|
||||
|
||||
Generated
+201
@@ -14,6 +14,9 @@ importers:
|
||||
'@ant-design/icons':
|
||||
specifier: ^6.3.2
|
||||
version: 6.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@codemirror/lang-json':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
@@ -23,6 +26,9 @@ importers:
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.2.7)
|
||||
'@uiw/react-codemirror':
|
||||
specifier: ^4.25.11
|
||||
version: 4.25.11(@babel/runtime@8.0.0)(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.7)(codemirror@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@uiw/react-json-view':
|
||||
specifier: 2.0.0-alpha.43
|
||||
version: 2.0.0-alpha.43(@babel/runtime@8.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
@@ -314,6 +320,33 @@ packages:
|
||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@codemirror/autocomplete@6.20.3':
|
||||
resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==}
|
||||
|
||||
'@codemirror/commands@6.10.4':
|
||||
resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==}
|
||||
|
||||
'@codemirror/lang-json@6.0.2':
|
||||
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
|
||||
|
||||
'@codemirror/language@6.12.4':
|
||||
resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==}
|
||||
|
||||
'@codemirror/lint@6.9.7':
|
||||
resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==}
|
||||
|
||||
'@codemirror/search@6.7.1':
|
||||
resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==}
|
||||
|
||||
'@codemirror/state@6.7.1':
|
||||
resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==}
|
||||
|
||||
'@codemirror/theme-one-dark@6.1.3':
|
||||
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
|
||||
|
||||
'@codemirror/view@6.43.7':
|
||||
resolution: {integrity: sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||
peerDependencies:
|
||||
@@ -432,6 +465,21 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@lezer/common@1.5.2':
|
||||
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
|
||||
|
||||
'@lezer/highlight@1.2.3':
|
||||
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
|
||||
|
||||
'@lezer/json@1.0.3':
|
||||
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
|
||||
|
||||
'@lezer/lr@1.4.10':
|
||||
resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.3':
|
||||
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6':
|
||||
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
|
||||
peerDependencies:
|
||||
@@ -982,9 +1030,31 @@ packages:
|
||||
resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@uiw/codemirror-extensions-basic-setup@4.25.11':
|
||||
resolution: {integrity: sha512-otyFa+n9IOYtEjaKOxPedHkj15fTPUF21wdR9pv0GpZPfuGl27cvmcv6+tognbRu9VvEcsHKE+ESoszeo3KfTw==}
|
||||
peerDependencies:
|
||||
'@codemirror/autocomplete': '>=6.0.0'
|
||||
'@codemirror/commands': '>=6.0.0'
|
||||
'@codemirror/language': '>=6.0.0'
|
||||
'@codemirror/lint': '>=6.0.0'
|
||||
'@codemirror/search': '>=6.0.0'
|
||||
'@codemirror/state': '>=6.0.0'
|
||||
'@codemirror/view': '>=6.0.0'
|
||||
|
||||
'@uiw/copy-to-clipboard@1.0.21':
|
||||
resolution: {integrity: sha512-apdzZJyJC/IEj21N22ry1H022pgpSA+FwNKxmvOGQ9rUMdyRbHf1nZq2UwqnfGOuTr7iOKLzNgWsCJtAwyAZpw==}
|
||||
|
||||
'@uiw/react-codemirror@4.25.11':
|
||||
resolution: {integrity: sha512-DYVFAKLX+F/4JS9N/7xexh+TICrlncwkX9HKKInrP1bwO0tSfc3k0GB6oawTYhelVKh20cX3TuRx+NJSkVXuMw==}
|
||||
peerDependencies:
|
||||
'@babel/runtime': '>=7.11.0'
|
||||
'@codemirror/state': '>=6.0.0'
|
||||
'@codemirror/theme-one-dark': '>=6.0.0'
|
||||
'@codemirror/view': '>=6.0.0'
|
||||
codemirror: '>=6.0.0'
|
||||
react: '>=17.0.0'
|
||||
react-dom: '>=17.0.0'
|
||||
|
||||
'@uiw/react-json-view@2.0.0-alpha.43':
|
||||
resolution: {integrity: sha512-PMj+6xRDCPbVGccfLUDhx7yGrmVBSeFx3py6bqDds4QIYScXo9GEgtTbWXn3gXpoehsmOu//6dveYTf872wu3Q==}
|
||||
peerDependencies:
|
||||
@@ -1116,6 +1186,9 @@ packages:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
codemirror@6.0.2:
|
||||
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
@@ -1146,6 +1219,9 @@ packages:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
crelt@1.0.7:
|
||||
resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2135,6 +2211,9 @@ packages:
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
style-mod@4.1.3:
|
||||
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
|
||||
|
||||
@@ -2299,6 +2378,9 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
@@ -2756,6 +2838,64 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@codemirror/autocomplete@6.20.3':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@codemirror/commands@6.10.4':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@codemirror/lang-json@6.0.2':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@lezer/json': 1.0.3
|
||||
|
||||
'@codemirror/language@6.12.4':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.10
|
||||
style-mod: 4.1.3
|
||||
|
||||
'@codemirror/lint@6.9.7':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
crelt: 1.0.7
|
||||
|
||||
'@codemirror/search@6.7.1':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
crelt: 1.0.7
|
||||
|
||||
'@codemirror/state@6.7.1':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.3
|
||||
|
||||
'@codemirror/theme-one-dark@6.1.3':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
'@lezer/highlight': 1.2.3
|
||||
|
||||
'@codemirror/view@6.43.7':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.1
|
||||
crelt: 1.0.7
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@19.2.7)':
|
||||
dependencies:
|
||||
react: 19.2.7
|
||||
@@ -2876,6 +3016,24 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@lezer/common@1.5.2': {}
|
||||
|
||||
'@lezer/highlight@1.2.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@lezer/json@1.0.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/lr@1.4.10':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@marijn/find-cluster-break@1.0.3': {}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.1
|
||||
@@ -3461,8 +3619,35 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.64.0
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
'@uiw/codemirror-extensions-basic-setup@4.25.11(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7)':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/commands': 6.10.4
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/lint': 6.9.7
|
||||
'@codemirror/search': 6.7.1
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
|
||||
'@uiw/copy-to-clipboard@1.0.21': {}
|
||||
|
||||
'@uiw/react-codemirror@4.25.11(@babel/runtime@8.0.0)(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.7)(codemirror@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||
dependencies:
|
||||
'@babel/runtime': 8.0.0
|
||||
'@codemirror/commands': 6.10.4
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/theme-one-dark': 6.1.3
|
||||
'@codemirror/view': 6.43.7
|
||||
'@uiw/codemirror-extensions-basic-setup': 4.25.11(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7)
|
||||
codemirror: 6.0.2
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
transitivePeerDependencies:
|
||||
- '@codemirror/autocomplete'
|
||||
- '@codemirror/language'
|
||||
- '@codemirror/lint'
|
||||
- '@codemirror/search'
|
||||
|
||||
'@uiw/react-json-view@2.0.0-alpha.43(@babel/runtime@8.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||
dependencies:
|
||||
'@babel/runtime': 8.0.0
|
||||
@@ -3649,6 +3834,16 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
codemirror@6.0.2:
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/commands': 6.10.4
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/lint': 6.9.7
|
||||
'@codemirror/search': 6.7.1
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.7
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
color-string@1.9.1:
|
||||
@@ -3672,6 +3867,8 @@ snapshots:
|
||||
|
||||
cookie@1.1.1: {}
|
||||
|
||||
crelt@1.0.7: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -4963,6 +5160,8 @@ snapshots:
|
||||
character-entities-html4: 2.1.0
|
||||
character-entities-legacy: 3.0.0
|
||||
|
||||
style-mod@4.1.3: {}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
dependencies:
|
||||
style-to-object: 1.0.14
|
||||
@@ -5111,6 +5310,8 @@ snapshots:
|
||||
'@types/node': 24.13.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
which@2.0.2:
|
||||
|
||||
@@ -106,6 +106,19 @@ const customVariableConfiguredTag = (v: unknown) => {
|
||||
const configured = v === true || v === 'true'
|
||||
return choiceTag(configured ? 'Configured' : 'Not configured', configured ? 'green' : 'red')
|
||||
}
|
||||
const customVariableTypeOptions = [
|
||||
{label: 'String', value: 'string'},
|
||||
{label: 'Integer', value: 'integer'},
|
||||
{label: 'Float', value: 'float'},
|
||||
{label: 'Boolean', value: 'boolean'},
|
||||
{label: 'List', value: 'list'},
|
||||
{label: 'Dictionary', value: 'dictionary'},
|
||||
]
|
||||
const customVariableTypeTag = (v: unknown) => {
|
||||
const type = String(v || '')
|
||||
const option = customVariableTypeOptions.find((item) => item.value === type)
|
||||
return choiceTag(option?.label || type, 'blue')
|
||||
}
|
||||
const llmTagColors: Record<string, string> = {
|
||||
fast: 'blue',
|
||||
powerful: 'purple',
|
||||
@@ -1027,12 +1040,14 @@ export const resourceConfigs: Record<string, ResourceConfig<RecordRow>> = {
|
||||
rowKey: 'id',
|
||||
searchPlaceholder: 'Key, Description',
|
||||
filters: [
|
||||
{key: 'value_type', label: 'Type', valueType: 'select', options: customVariableTypeOptions, width: L160},
|
||||
{key: 'is_secret', label: 'Secret', valueType: 'select', options: [{label: 'Secret', value: 'true'}, {label: 'Plain', value: 'false'}], width: L132},
|
||||
{key: 'enabled', label: 'Enabled', valueType: 'select', options: [{label: 'Enabled', value: 'true'}, {label: 'Disabled', value: 'false'}], width: L132},
|
||||
],
|
||||
advancedFilters: [
|
||||
field('key', 'Key', 'text'),
|
||||
field('description', 'Description', 'text'),
|
||||
{key: 'value_type', label: 'Type', valueType: 'select', options: customVariableTypeOptions},
|
||||
{key: 'is_secret', label: 'Secret', valueType: 'select', options: [{label: 'Secret', value: 'true'}, {label: 'Plain', value: 'false'}]},
|
||||
{key: 'enabled', label: 'Enabled', valueType: 'select', options: [{label: 'Enabled', value: 'true'}, {label: 'Disabled', value: 'false'}]},
|
||||
field('created_at', 'Created At', 'date'),
|
||||
@@ -1041,6 +1056,7 @@ export const resourceConfigs: Record<string, ResourceConfig<RecordRow>> = {
|
||||
columns: [
|
||||
column('key', 'Key', L240, {required: true, defaultVisible: true, openRecord: true, sorter: true}),
|
||||
column('description', 'Description', L360, {defaultVisible: true}),
|
||||
column('value_type', 'Type', L132, {defaultVisible: true, sorter: true, render: customVariableTypeTag}),
|
||||
column('is_secret', 'Secret', L96, {defaultVisible: true, sorter: true, render: customVariableSecretTag}),
|
||||
column('enabled', 'Enabled', L96, {defaultVisible: true, sorter: true, render: llmProviderStatusTag}),
|
||||
column('value_configured', 'Value', L132, {defaultVisible: true, render: customVariableConfiguredTag}),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {App as AntApp, Button, Form, Input, Modal, Popconfirm, Space, Switch, Typography} from 'antd'
|
||||
import {App as AntApp, Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Typography} from 'antd'
|
||||
import {DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined} from '@ant-design/icons'
|
||||
import CodeMirror from '@uiw/react-codemirror'
|
||||
import {json} from '@codemirror/lang-json'
|
||||
import client from '../api/client'
|
||||
import DataTable from '../components/DataTable'
|
||||
import {getResourceConfig} from '../config/resources'
|
||||
@@ -9,7 +11,8 @@ import {message} from '../utils/appMessage'
|
||||
type CustomVariable = Record<string, unknown> & {
|
||||
id: string
|
||||
key: string
|
||||
value: string
|
||||
value_type: CustomVariableValueType
|
||||
value: unknown
|
||||
value_configured: boolean
|
||||
is_secret: boolean
|
||||
description: string
|
||||
@@ -18,9 +21,12 @@ type CustomVariable = Record<string, unknown> & {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type CustomVariableValueType = 'string' | 'integer' | 'float' | 'boolean' | 'list' | 'dictionary'
|
||||
|
||||
interface CustomVariableFormValues {
|
||||
key: string
|
||||
value?: string
|
||||
value_type: CustomVariableValueType
|
||||
value?: unknown
|
||||
is_secret: boolean
|
||||
description?: string
|
||||
enabled: boolean
|
||||
@@ -32,6 +38,16 @@ interface RevealedValue {
|
||||
}
|
||||
|
||||
const MAX_VALUE_BYTES = 65_536
|
||||
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
||||
const JSON_EDITOR_EXTENSIONS = [json()]
|
||||
const VALUE_TYPE_OPTIONS = [
|
||||
{label: 'String', value: 'string'},
|
||||
{label: 'Integer', value: 'integer'},
|
||||
{label: 'Float', value: 'float'},
|
||||
{label: 'Boolean', value: 'boolean'},
|
||||
{label: 'List', value: 'list'},
|
||||
{label: 'Dictionary', value: 'dictionary'},
|
||||
]
|
||||
|
||||
function apiErrorMessage(error: unknown, fallback: string) {
|
||||
const data = (error as { response?: { data?: unknown } }).response?.data
|
||||
@@ -47,6 +63,7 @@ function apiErrorMessage(error: unknown, fallback: string) {
|
||||
function initialValues(): CustomVariableFormValues {
|
||||
return {
|
||||
key: '',
|
||||
value_type: 'string',
|
||||
value: '',
|
||||
is_secret: false,
|
||||
description: '',
|
||||
@@ -65,6 +82,7 @@ export default function CustomVariablesSettings() {
|
||||
const [revealed, setRevealed] = useState<RevealedValue | null>(null)
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
const isSecret = Form.useWatch('is_secret', form) ?? false
|
||||
const valueType = Form.useWatch('value_type', form) ?? 'string'
|
||||
|
||||
const refresh = () => setRefreshKey((value) => value + 1)
|
||||
|
||||
@@ -78,7 +96,12 @@ export default function CustomVariablesSettings() {
|
||||
try {
|
||||
const {data} = await client.get<CustomVariable>(`/custom/variables/${record.id}/`)
|
||||
setEditing(data)
|
||||
form.setFieldsValue({...initialValues(), ...data})
|
||||
const value = data.is_secret
|
||||
? ''
|
||||
: data.value_type === 'list' || data.value_type === 'dictionary'
|
||||
? JSON.stringify(data.value, null, 2)
|
||||
: data.value
|
||||
form.setFieldsValue({...initialValues(), ...data, value})
|
||||
setModalOpen(true)
|
||||
} catch (error: unknown) {
|
||||
message.error(apiErrorMessage(error, 'Failed to load custom variable'))
|
||||
@@ -93,12 +116,18 @@ export default function CustomVariablesSettings() {
|
||||
|
||||
const persist = async (values: CustomVariableFormValues, confirmSecretExposure: boolean) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
value_type: values.value_type,
|
||||
is_secret: values.is_secret,
|
||||
description: values.description || '',
|
||||
enabled: values.enabled,
|
||||
}
|
||||
if (!editing) payload.key = values.key
|
||||
if (values.value !== '' && values.value !== undefined) payload.value = values.value
|
||||
const omitUnchangedSecret = Boolean(editing?.is_secret && values.value_type === 'string' && values.value === '')
|
||||
if (!omitUnchangedSecret) {
|
||||
payload.value = values.value_type === 'list' || values.value_type === 'dictionary'
|
||||
? JSON.parse(String(values.value))
|
||||
: values.value
|
||||
}
|
||||
if (confirmSecretExposure) payload.confirm_secret_exposure = true
|
||||
|
||||
if (editing) {
|
||||
@@ -116,12 +145,18 @@ export default function CustomVariablesSettings() {
|
||||
setSaving(true)
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
if (editing?.is_secret && !values.is_secret) {
|
||||
const exposesSecret = Boolean(editing?.is_secret && !values.is_secret)
|
||||
const changesType = Boolean(editing && editing.value_type !== values.value_type)
|
||||
if (exposesSecret || changesType) {
|
||||
modal.confirm({
|
||||
title: `Expose ${editing.key} as a non-secret variable?`,
|
||||
content: 'This value will become visible in normal Admin API responses and UI.',
|
||||
okText: 'Expose value',
|
||||
okButtonProps: {danger: true},
|
||||
title: exposesSecret
|
||||
? `Expose ${editing?.key} as a non-secret variable?`
|
||||
: `Change ${editing?.key} from ${editing?.value_type} to ${values.value_type}?`,
|
||||
content: exposesSecret
|
||||
? 'This value will become visible in normal Admin API responses and UI.'
|
||||
: 'Playbooks and modules will receive a different Python value type.',
|
||||
okText: exposesSecret ? 'Expose value' : 'Change type',
|
||||
okButtonProps: {danger: exposesSecret},
|
||||
onOk: async () => {
|
||||
try {
|
||||
await persist(values, true)
|
||||
@@ -163,16 +198,53 @@ export default function CustomVariablesSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const validateValue = (_: unknown, value: string | undefined) => {
|
||||
if (!value && !editing?.is_secret) {
|
||||
const validateValue = (_: unknown, value: unknown) => {
|
||||
const unchangedSecret = editing?.is_secret && valueType === 'string' && value === ''
|
||||
if (unchangedSecret) return Promise.resolve()
|
||||
if (value === undefined || value === null || (valueType === 'string' && value === '')) {
|
||||
return Promise.reject(new Error('Value is required.'))
|
||||
}
|
||||
if (value !== undefined && new TextEncoder().encode(value).length > MAX_VALUE_BYTES) {
|
||||
if (valueType === 'integer' && (typeof value !== 'number' || !Number.isSafeInteger(value))) {
|
||||
return Promise.reject(new Error('Value must be a safe integer.'))
|
||||
}
|
||||
let serialized = String(value)
|
||||
if (valueType === 'list' || valueType === 'dictionary') {
|
||||
try {
|
||||
const parsed = JSON.parse(serialized)
|
||||
if (valueType === 'list' && !Array.isArray(parsed)) {
|
||||
return Promise.reject(new Error('Value must be a JSON list.'))
|
||||
}
|
||||
if (valueType === 'dictionary' && (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object')) {
|
||||
return Promise.reject(new Error('Value must be a JSON dictionary.'))
|
||||
}
|
||||
serialized = JSON.stringify(parsed)
|
||||
} catch {
|
||||
return Promise.reject(new Error('Value must be valid JSON.'))
|
||||
}
|
||||
}
|
||||
if (new TextEncoder().encode(serialized).length > MAX_VALUE_BYTES) {
|
||||
return Promise.reject(new Error(`Value cannot exceed ${MAX_VALUE_BYTES.toLocaleString()} UTF-8 bytes.`))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const changeValueType = (nextType: CustomVariableValueType) => {
|
||||
form.setFieldsValue({
|
||||
value_type: nextType,
|
||||
value: nextType === 'boolean' ? false : undefined,
|
||||
is_secret: nextType === 'string' ? form.getFieldValue('is_secret') : false,
|
||||
})
|
||||
}
|
||||
|
||||
const formatStructuredValue = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(String(form.getFieldValue('value') ?? ''))
|
||||
form.setFieldValue('value', JSON.stringify(parsed, null, 2))
|
||||
} catch {
|
||||
message.error('Value must be valid JSON before it can be formatted')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{height: 'calc(100vh - 188px)', minHeight: 360}}>
|
||||
<DataTable
|
||||
@@ -241,6 +313,7 @@ export default function CustomVariablesSettings() {
|
||||
<Modal
|
||||
title={editing ? `Custom Variable: ${editing.key}` : 'Add Custom Variable'}
|
||||
open={modalOpen}
|
||||
width={valueType === 'list' || valueType === 'dictionary' ? 760 : undefined}
|
||||
onCancel={closeEditor}
|
||||
destroyOnHidden
|
||||
footer={(
|
||||
@@ -261,23 +334,72 @@ export default function CustomVariablesSettings() {
|
||||
>
|
||||
<Input disabled={editing !== null} placeholder="EDR_API_TOKEN" maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="value_type"
|
||||
label="Type"
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<Select options={VALUE_TYPE_OPTIONS} onChange={changeValueType} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="value"
|
||||
label="Value"
|
||||
valuePropName={valueType === 'boolean' ? 'checked' : 'value'}
|
||||
label={(
|
||||
<Space>
|
||||
<span>Value</span>
|
||||
{(valueType === 'list' || valueType === 'dictionary')
|
||||
? <Button type="link" size="small" onClick={formatStructuredValue}>Format</Button>
|
||||
: null}
|
||||
</Space>
|
||||
)}
|
||||
rules={[{validator: validateValue}]}
|
||||
extra={editing?.is_secret
|
||||
? 'Leave blank to keep the current value. Maximum 65,536 UTF-8 bytes.'
|
||||
: 'Maximum 65,536 UTF-8 bytes.'}
|
||||
>
|
||||
{isSecret
|
||||
? <Input.Password autoComplete="new-password" />
|
||||
: <Input.TextArea autoSize={{minRows: 3, maxRows: 10}} />}
|
||||
{valueType === 'boolean' ? (
|
||||
<Switch />
|
||||
) : valueType === 'integer' ? (
|
||||
<InputNumber
|
||||
min={-MAX_SAFE_INTEGER}
|
||||
max={MAX_SAFE_INTEGER}
|
||||
precision={0}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
) : valueType === 'float' ? (
|
||||
<InputNumber style={{width: '100%'}} />
|
||||
) : isSecret ? (
|
||||
<Input.Password autoComplete="new-password" />
|
||||
) : valueType === 'list' || valueType === 'dictionary' ? (
|
||||
<CodeMirror
|
||||
height="320px"
|
||||
theme="dark"
|
||||
extensions={JSON_EDITOR_EXTENSIONS}
|
||||
placeholder={valueType === 'list' ? '[\n \n]' : '{\n \n}'}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightActiveLineGutter: true,
|
||||
}}
|
||||
style={{
|
||||
border: '1px solid rgba(253, 253, 253, 0.12)',
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input.TextArea autoSize={{minRows: 3, maxRows: 10}} />
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="Description">
|
||||
<Input.TextArea autoSize={{minRows: 2, maxRows: 5}} />
|
||||
</Form.Item>
|
||||
<Form.Item name="is_secret" label="Secret" valuePropName="checked">
|
||||
<Switch />
|
||||
<Switch disabled={valueType !== 'string'} />
|
||||
</Form.Item>
|
||||
<Form.Item name="enabled" label="Enabled" valuePropName="checked">
|
||||
<Switch />
|
||||
|
||||
Reference in New Issue
Block a user