mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
feat(core): add prefill prop to input to allow nullable value (#11819)
Added a new 'prefill' property for all inputs to specify an optional UI hint for pre-filling the input,while allowing the input to be nullable. Fixes: #11819
This commit is contained in:
committed by
Florian Hussonnois
parent
163e1e2c8b
commit
6361a02deb
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.kestra.core.models.flows.input.*;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.validations.InputValidation;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
@@ -44,6 +45,7 @@ import lombok.experimental.SuperBuilder;
|
||||
@JsonSubTypes.Type(value = YamlInput.class, name = "YAML"),
|
||||
@JsonSubTypes.Type(value = EmailInput.class, name = "EMAIL"),
|
||||
})
|
||||
@InputValidation
|
||||
public abstract class Input<T> implements Data {
|
||||
@Schema(
|
||||
title = "The ID of the input."
|
||||
@@ -80,7 +82,13 @@ public abstract class Input<T> implements Data {
|
||||
title = "The default value to use if no value is specified."
|
||||
)
|
||||
Property<T> defaults;
|
||||
|
||||
|
||||
@Schema(
|
||||
title = "The suggested value for the input.",
|
||||
description = "Optional UI hint for pre-filling the input. Cannot be used together with a default value."
|
||||
)
|
||||
Property<T> prefill;
|
||||
|
||||
@Schema(
|
||||
title = "The display name of the input."
|
||||
)
|
||||
|
||||
@@ -130,7 +130,7 @@ public class FlowInputOutput {
|
||||
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
|
||||
return Flux.from(data)
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.<AbstractMap.SimpleEntry<String, String>>handle((input, sink) -> {
|
||||
.<Map.Entry<String, String>>handle((input, sink) -> {
|
||||
if (input instanceof CompletedFileUpload fileUpload) {
|
||||
boolean oldStyleInput = false;
|
||||
if ("files".equals(fileUpload.getName())) {
|
||||
@@ -150,7 +150,7 @@ public class FlowInputOutput {
|
||||
.getContextStorageURI()
|
||||
);
|
||||
fileUpload.discard();
|
||||
sink.next(new AbstractMap.SimpleEntry<>(inputId, from.toString()));
|
||||
sink.next(Map.entry(inputId, from.toString()));
|
||||
} else {
|
||||
try {
|
||||
final String fileExtension = FileInput.findFileInputExtension(inputs, fileName);
|
||||
@@ -165,7 +165,7 @@ public class FlowInputOutput {
|
||||
return;
|
||||
}
|
||||
URI from = storageInterface.from(execution, inputId, fileName, tempFile);
|
||||
sink.next(new AbstractMap.SimpleEntry<>(inputId, from.toString()));
|
||||
sink.next(Map.entry(inputId, from.toString()));
|
||||
} finally {
|
||||
if (!tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
@@ -178,13 +178,13 @@ public class FlowInputOutput {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
sink.next(new AbstractMap.SimpleEntry<>(input.getName(), new String(input.getBytes())));
|
||||
sink.next(Map.entry(input.getName(), new String(input.getBytes())));
|
||||
} catch (IOException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.collectMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue);
|
||||
.collectMap(Map.Entry::getKey, Map.Entry::getValue);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import io.kestra.core.validations.validator.InputValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = InputValidator.class)
|
||||
public @interface InputValidation {
|
||||
String message() default "invalid input";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.kestra.core.validations.validator;
|
||||
|
||||
import io.kestra.core.models.flows.Input;
|
||||
import io.kestra.core.runners.VariableRenderer;
|
||||
import io.kestra.core.validations.InputValidation;
|
||||
import io.micronaut.core.annotation.AnnotationValue;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import io.micronaut.core.annotation.NonNull;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidator;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
@Introspected
|
||||
public class InputValidator implements ConstraintValidator<InputValidation, Input<?>> {
|
||||
|
||||
@Inject
|
||||
VariableRenderer variableRenderer;
|
||||
|
||||
@Override
|
||||
public boolean isValid(@Nullable Input<?> value, @NonNull AnnotationValue<InputValidation> annotationMetadata, @NonNull ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
return true; // nulls are allowed according to spec
|
||||
}
|
||||
|
||||
if (value.getDefaults() != null && Boolean.FALSE.equals(value.getRequired())) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("Inputs with a default value must be required, since the default is always applied.")
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.getDefaults() != null && value.getPrefill() != null) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("Inputs with a default value cannot also have a prefill.")
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest
|
||||
class FlowInputOutputTest {
|
||||
@@ -335,8 +336,7 @@ class FlowInputOutputTest {
|
||||
// Then
|
||||
Assertions.assertEquals(TEST_SECRET_VALUE, ((MultiselectInput)results.getFirst().input()).getValues().getFirst());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void shouldNotObfuscateSecretsWhenReadingInputs() {
|
||||
// Given
|
||||
@@ -354,6 +354,22 @@ class FlowInputOutputTest {
|
||||
Assertions.assertEquals(TEST_SECRET_VALUE, results.get("input"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetDefaultWhenPassingNoDataForRequiredInput() {
|
||||
// Given
|
||||
StringInput input = StringInput.builder()
|
||||
.id("input")
|
||||
.type(Type.STRING)
|
||||
.defaults(Property.ofValue("default"))
|
||||
.build();
|
||||
|
||||
// When
|
||||
Map<String, Object> results = flowInputOutput.readExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, Mono.empty()).block();
|
||||
|
||||
// Then
|
||||
assertThat(results.get("input")).isEqualTo("default");
|
||||
}
|
||||
|
||||
private static class MemoryCompletedPart implements CompletedPart {
|
||||
|
||||
protected final String name;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.tenant.TenantService;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -66,6 +68,50 @@ class FlowValidationTest {
|
||||
|
||||
assertThat(validate.isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetConstraintErrorGivenInputWithBothDefaultsAndSuggestion() {
|
||||
// Given
|
||||
GenericFlow flow = GenericFlow.fromYaml(TenantService.MAIN_TENANT, """
|
||||
id: test
|
||||
namespace: unittest
|
||||
inputs:
|
||||
- id: input
|
||||
type: STRING
|
||||
prefill: "suggestion"
|
||||
defaults: "defaults"
|
||||
tasks: []
|
||||
""");
|
||||
|
||||
// When
|
||||
Optional<ConstraintViolationException> validate = modelValidator.isValid(flow);
|
||||
|
||||
// Then
|
||||
assertThat(validate.isPresent()).isEqualTo(true);
|
||||
assertThat(validate.get().getMessage()).contains("Inputs with a default value cannot also have a suggestion.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetConstraintErrorGivenOptionalInputWithDefault() {
|
||||
// Given
|
||||
GenericFlow flow = GenericFlow.fromYaml(TenantService.MAIN_TENANT, """
|
||||
id: test
|
||||
namespace: unittest
|
||||
inputs:
|
||||
- id: input
|
||||
type: STRING
|
||||
defaults: "defaults"
|
||||
required: false
|
||||
tasks: []
|
||||
""");
|
||||
|
||||
// When
|
||||
Optional<ConstraintViolationException> validate = modelValidator.isValid(flow);
|
||||
|
||||
// Then
|
||||
assertThat(validate.isPresent()).isEqualTo(true);
|
||||
assertThat(validate.get().getMessage()).contains("Inputs with a default value must be required, since the default is always applied.");
|
||||
}
|
||||
|
||||
private Flow parse(String path) {
|
||||
URL resource = TestsUtils.class.getClassLoader().getResource(path);
|
||||
|
||||
@@ -3,11 +3,9 @@ namespace: io.kestra.tests
|
||||
inputs:
|
||||
- name: namespace
|
||||
type: STRING
|
||||
required: false
|
||||
defaults: "io.kestra.tests"
|
||||
- name: errorOnMissing
|
||||
type: BOOL
|
||||
required: false
|
||||
defaults: false
|
||||
tasks:
|
||||
- id: get
|
||||
|
||||
@@ -539,7 +539,7 @@
|
||||
const metadataCallback = (response) => {
|
||||
this.inputsMetaData = response.inputs.reduce((acc,it) => {
|
||||
if(it.enabled){
|
||||
acc.push({...it.input, errors: it.errors, value: it.value, isDefault: it.isDefault});
|
||||
acc.push({...it.input, errors: it.errors, value: it.value || it.input.prefill, isDefault: it.isDefault});
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
|
||||
@@ -211,7 +211,7 @@ class PluginControllerTest {
|
||||
|
||||
assertThat(doc.getSchema().getProperties().size()).isEqualTo(3);
|
||||
Map<String, Object> properties = (Map<String, Object>) doc.getSchema().getProperties().get("properties");
|
||||
assertThat(properties.size()).isEqualTo(8);
|
||||
assertThat(properties.size()).isEqualTo(9);
|
||||
assertThat(((Map<String, Object>) properties.get("name")).get("$deprecated")).isEqualTo(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user