mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
57 Commits
dependabot
...
v0.18.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0254f3997 | ||
|
|
788dc1376a | ||
|
|
1b0e3cbbd6 | ||
|
|
5bbc235677 | ||
|
|
e0f8fcbcde | ||
|
|
87a667dc27 | ||
|
|
81d54ef423 | ||
|
|
abe2086dbd | ||
|
|
7c0b0b14ec | ||
|
|
7061eef48c | ||
|
|
3c35c8a48f | ||
|
|
0d4e8f2c26 | ||
|
|
e1b7f1ce16 | ||
|
|
095e29ee18 | ||
|
|
4ede173331 | ||
|
|
442306f82e | ||
|
|
19fda0d1a4 | ||
|
|
549e3d7bec | ||
|
|
880bc67756 | ||
|
|
249bae8a01 | ||
|
|
089b9ee0ca | ||
|
|
615d42db62 | ||
|
|
a70ca97912 | ||
|
|
f2718797d0 | ||
|
|
04ce98e0ef | ||
|
|
d9e18d5e13 | ||
|
|
b9aafb37d4 | ||
|
|
6c9833936a | ||
|
|
d48f5407e8 | ||
|
|
6e3e4810f9 | ||
|
|
8afaec885c | ||
|
|
afc76509f2 | ||
|
|
63a8b69221 | ||
|
|
92f5b3face | ||
|
|
1a3343ff6c | ||
|
|
73a82d1cd5 | ||
|
|
f1ce76ff9f | ||
|
|
81817291ee | ||
|
|
9646a42ea7 | ||
|
|
1253d96c8a | ||
|
|
3e40a56c1c | ||
|
|
de2467012c | ||
|
|
a38bf61c3b | ||
|
|
92a323a36c | ||
|
|
d102033a2f | ||
|
|
654d62118c | ||
|
|
f9186b29b4 | ||
|
|
2ee9b9f069 | ||
|
|
d57d9dd3e8 | ||
|
|
ae15cef7b7 | ||
|
|
0e46055884 | ||
|
|
f91b070dca | ||
|
|
d964d49861 | ||
|
|
dd45545202 | ||
|
|
8d2589485f | ||
|
|
78069b45f8 | ||
|
|
7a13ed79d8 |
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -22,12 +22,20 @@ updates:
|
|||||||
- "dependency-upgrade"
|
- "dependency-upgrade"
|
||||||
open-pull-requests-limit: 50
|
open-pull-requests-limit: 50
|
||||||
|
|
||||||
# Maintain dependencies for Npm modules
|
# Maintain dependencies for NPM modules
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/ui"
|
directory: "/ui"
|
||||||
schedule:
|
schedule:
|
||||||
# Check for updates to Npm modules every week
|
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
day: "sunday"
|
||||||
|
time: "09:00"
|
||||||
|
open-pull-requests-limit: 50
|
||||||
labels:
|
labels:
|
||||||
- "dependency-upgrade"
|
- "dependency-upgrade"
|
||||||
open-pull-requests-limit: 50
|
ignore:
|
||||||
|
# Ignore updates of version 1.x, as we're using beta of 2.x
|
||||||
|
- dependency-name: "vue-virtual-scroller"
|
||||||
|
versions: ["1.x"]
|
||||||
|
# Ignore major versions greater than 8, as it's still known to be flaky
|
||||||
|
- dependency-name: "eslint"
|
||||||
|
versions: [">8"]
|
||||||
5
.github/workflows/docker.yml
vendored
5
.github/workflows/docker.yml
vendored
@@ -109,6 +109,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:latest{0}', matrix.image.name) }}
|
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:latest{0}', matrix.image.name) }}
|
||||||
|
|
||||||
|
- name: Retag latest to latest-full
|
||||||
|
if: ${{ github.event.inputs.retag-latest == 'true' && matrix.image.name == ''}}
|
||||||
|
run: |
|
||||||
|
regctl image copy kestra/kestra:latest kestra/kestra:latest-full
|
||||||
|
|
||||||
end:
|
end:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@@ -117,10 +117,12 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Docker Build
|
# Docker Build
|
||||||
- name: Build & Export Docker Image
|
- name: Build & Export Docker Image
|
||||||
|
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -275,7 +277,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Github Release
|
name: Github Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ check, check-e2e ]
|
needs: build-artifacts
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
# Download Exec
|
# Download Exec
|
||||||
@@ -367,7 +369,7 @@ jobs:
|
|||||||
maven:
|
maven:
|
||||||
name: Publish to Maven
|
name: Publish to Maven
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [check, check-e2e]
|
needs: [check]
|
||||||
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')
|
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.kestra.core.docs;
|
|||||||
|
|
||||||
import com.google.common.base.CaseFormat;
|
import com.google.common.base.CaseFormat;
|
||||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||||
|
import io.kestra.core.models.tasks.runners.TaskRunner;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -144,6 +145,10 @@ public abstract class AbstractClassDocumentation<T> {
|
|||||||
if (AbstractRetry.class.isAssignableFrom(Class.forName(key))) {
|
if (AbstractRetry.class.isAssignableFrom(Class.forName(key))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TaskRunner.class.isAssignableFrom(Class.forName(key))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} catch (ClassNotFoundException ignored) {
|
} catch (ClassNotFoundException ignored) {
|
||||||
log.debug(ignored.getMessage(), ignored);
|
log.debug(ignored.getMessage(), ignored);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,14 +61,13 @@ public class LogEntry implements DeletedInterface, TenantInterface {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
boolean deleted = false;
|
boolean deleted = false;
|
||||||
|
|
||||||
public static List<String> findLevelsByMin(Level minLevel) {
|
public static List<Level> findLevelsByMin(Level minLevel) {
|
||||||
if (minLevel == null) {
|
if (minLevel == null) {
|
||||||
return Arrays.stream(Level.values()).map(Enum::name).toList();
|
return Arrays.asList(Level.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Arrays.stream(Level.values())
|
return Arrays.stream(Level.values())
|
||||||
.filter(level -> level.toInt() >= minLevel.toInt())
|
.filter(level -> level.toInt() >= minLevel.toInt())
|
||||||
.map(Enum::name)
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ public class Flow extends AbstractFlow {
|
|||||||
public List<String> allTriggerIds() {
|
public List<String> allTriggerIds() {
|
||||||
return this.triggers != null ? this.triggers.stream()
|
return this.triggers != null ? this.triggers.stream()
|
||||||
.map(AbstractTrigger::getId)
|
.map(AbstractTrigger::getId)
|
||||||
|
.filter(id -> id != null) // this can happen when validation a flow under creation
|
||||||
.collect(Collectors.toList()) : Collections.emptyList();
|
.collect(Collectors.toList()) : Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import io.kestra.core.models.validations.ManualConstraintViolation;
|
|||||||
import io.kestra.core.validations.Regex;
|
import io.kestra.core.validations.Regex;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.ConstraintViolationException;
|
import jakarta.validation.ConstraintViolationException;
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@@ -18,11 +17,19 @@ import java.util.List;
|
|||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class MultiselectInput extends Input<List<String>> implements ItemTypeInterface {
|
public class MultiselectInput extends Input<List<String>> implements ItemTypeInterface {
|
||||||
|
@Schema(
|
||||||
|
title = "Deprecated, please use `values` instead."
|
||||||
|
)
|
||||||
|
// @NotNull
|
||||||
|
@Deprecated
|
||||||
|
List<@Regex String> options;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
title = "List of values available."
|
title = "List of values available."
|
||||||
)
|
)
|
||||||
@NotNull
|
// FIXME: REMOVE `options` in 0.20 and bring back the NotNull
|
||||||
List<@Regex String> options;
|
// @NotNull
|
||||||
|
List<@Regex String> values;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
title = "Type of the different values available.",
|
title = "Type of the different values available.",
|
||||||
@@ -33,10 +40,21 @@ public class MultiselectInput extends Input<List<String>> implements ItemTypeInt
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate(List<String> inputs) throws ConstraintViolationException {
|
public void validate(List<String> inputs) throws ConstraintViolationException {
|
||||||
|
if (values != null && options != null) {
|
||||||
|
throw ManualConstraintViolation.toConstraintViolationException(
|
||||||
|
"you can't define both `values` and `options`",
|
||||||
|
this,
|
||||||
|
MultiselectInput.class,
|
||||||
|
getId(),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for(String input : inputs){
|
for(String input : inputs){
|
||||||
if (!options.contains(input)) {
|
List<@Regex String> finalValues = this.values != null ? this.values : this.options;
|
||||||
|
if (!finalValues.contains(input)) {
|
||||||
throw ManualConstraintViolation.toConstraintViolationException(
|
throw ManualConstraintViolation.toConstraintViolationException(
|
||||||
"it must match the values `" + options + "`",
|
"it must match the values `" + finalValues + "`",
|
||||||
this,
|
this,
|
||||||
MultiselectInput.class,
|
MultiselectInput.class,
|
||||||
getId(),
|
getId(),
|
||||||
|
|||||||
@@ -40,13 +40,17 @@ public interface TaskCommands {
|
|||||||
TargetOS getTargetOS();
|
TargetOS getTargetOS();
|
||||||
|
|
||||||
default List<Path> relativeWorkingDirectoryFilesPaths() throws IOException {
|
default List<Path> relativeWorkingDirectoryFilesPaths() throws IOException {
|
||||||
|
return this.relativeWorkingDirectoryFilesPaths(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<Path> relativeWorkingDirectoryFilesPaths(boolean includeDirectories) throws IOException {
|
||||||
Path workingDirectory = this.getWorkingDirectory();
|
Path workingDirectory = this.getWorkingDirectory();
|
||||||
if (workingDirectory == null) {
|
if (workingDirectory == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Stream<Path> walk = Files.walk(workingDirectory)) {
|
try (Stream<Path> walk = Files.walk(workingDirectory)) {
|
||||||
Stream<Path> filtered = walk.filter(path -> !Files.isDirectory(path));
|
Stream<Path> filtered = includeDirectories ? walk : walk.filter(path -> !Files.isDirectory(path));
|
||||||
Path outputDirectory = this.getOutputDirectory();
|
Path outputDirectory = this.getOutputDirectory();
|
||||||
if (outputDirectory != null) {
|
if (outputDirectory != null) {
|
||||||
filtered = filtered.filter(Predicate.not(path -> path.startsWith(outputDirectory)));
|
filtered = filtered.filter(Predicate.not(path -> path.startsWith(outputDirectory)));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import java.util.function.Predicate;
|
|||||||
* @see io.kestra.core.plugins.serdes.PluginDeserializer
|
* @see io.kestra.core.plugins.serdes.PluginDeserializer
|
||||||
* @see PluginScanner
|
* @see PluginScanner
|
||||||
*/
|
*/
|
||||||
public final class DefaultPluginRegistry implements PluginRegistry {
|
public class DefaultPluginRegistry implements PluginRegistry {
|
||||||
|
|
||||||
private static class LazyHolder {
|
private static class LazyHolder {
|
||||||
static final DefaultPluginRegistry INSTANCE = new DefaultPluginRegistry();
|
static final DefaultPluginRegistry INSTANCE = new DefaultPluginRegistry();
|
||||||
@@ -43,7 +43,7 @@ public final class DefaultPluginRegistry implements PluginRegistry {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultPluginRegistry() {
|
protected DefaultPluginRegistry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isInitialized() {
|
private boolean isInitialized() {
|
||||||
@@ -53,7 +53,7 @@ public final class DefaultPluginRegistry implements PluginRegistry {
|
|||||||
/**
|
/**
|
||||||
* Initializes the registry by loading all core plugins.
|
* Initializes the registry by loading all core plugins.
|
||||||
*/
|
*/
|
||||||
private void init() {
|
protected void init() {
|
||||||
if (initialized.compareAndSet(false, true)) {
|
if (initialized.compareAndSet(false, true)) {
|
||||||
register(scanner.scan());
|
register(scanner.scan());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public interface FlowRepositoryInterface {
|
|||||||
execution.getTenantId(),
|
execution.getTenantId(),
|
||||||
execution.getNamespace(),
|
execution.getNamespace(),
|
||||||
execution.getFlowId(),
|
execution.getFlowId(),
|
||||||
Optional.of(execution.getFlowRevision())
|
Optional.ofNullable(execution.getFlowRevision())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (find.isEmpty()) {
|
if (find.isEmpty()) {
|
||||||
@@ -50,7 +50,7 @@ public interface FlowRepositoryInterface {
|
|||||||
execution.getTenantId(),
|
execution.getTenantId(),
|
||||||
execution.getNamespace(),
|
execution.getNamespace(),
|
||||||
execution.getFlowId(),
|
execution.getFlowId(),
|
||||||
Optional.of(execution.getFlowRevision())
|
Optional.ofNullable(execution.getFlowRevision())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (find.isEmpty()) {
|
if (find.isEmpty()) {
|
||||||
|
|||||||
@@ -144,18 +144,16 @@ public class DefaultRunContext extends RunContext {
|
|||||||
@Override
|
@Override
|
||||||
public DefaultRunContext clone() {
|
public DefaultRunContext clone() {
|
||||||
DefaultRunContext runContext = new DefaultRunContext();
|
DefaultRunContext runContext = new DefaultRunContext();
|
||||||
runContext.variableRenderer = this.variableRenderer;
|
|
||||||
runContext.applicationContext = this.applicationContext;
|
|
||||||
runContext.storageInterface = this.storageInterface;
|
|
||||||
runContext.storage = this.storage;
|
|
||||||
runContext.variables = new HashMap<>(this.variables);
|
runContext.variables = new HashMap<>(this.variables);
|
||||||
runContext.metrics = new ArrayList<>();
|
|
||||||
runContext.meterRegistry = this.meterRegistry;
|
|
||||||
runContext.workingDir = this.workingDir;
|
runContext.workingDir = this.workingDir;
|
||||||
runContext.logger = this.logger;
|
runContext.logger = this.logger;
|
||||||
|
runContext.metrics = new ArrayList<>();
|
||||||
|
runContext.storage = this.storage;
|
||||||
runContext.pluginConfiguration = this.pluginConfiguration;
|
runContext.pluginConfiguration = this.pluginConfiguration;
|
||||||
runContext.version = version;
|
if (this.isInitialized.get()) {
|
||||||
runContext.isInitialized.set(this.isInitialized.get());
|
//Inject all services
|
||||||
|
runContext.init(applicationContext);
|
||||||
|
}
|
||||||
return runContext;
|
return runContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,15 +58,19 @@ public abstract class FilesService {
|
|||||||
return inputFiles;
|
return inputFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, URI> outputFiles(RunContext runContext, List<String> outputs) throws Exception {
|
public static Map<String, URI> outputFiles(RunContext runContext, List<String> outputs) throws Exception {
|
||||||
List<Path> allFilesMatching = runContext.workingDir().findAllFilesMatching(outputs);
|
List<String> renderedOutputs = outputs != null ? runContext.render(outputs) : null;
|
||||||
var outputFiles = allFilesMatching.stream()
|
List<Path> allFilesMatching = runContext.workingDir().findAllFilesMatching(renderedOutputs);
|
||||||
.map(throwFunction(path -> new AbstractMap.SimpleEntry<>(
|
var outputFiles = allFilesMatching.stream()
|
||||||
runContext.workingDir().path().relativize(path).toString(),
|
.map(throwFunction(path -> new AbstractMap.SimpleEntry<>(
|
||||||
runContext.storage().putFile(path.toFile(), resolveUniqueNameForFile(path))
|
runContext.workingDir().path().relativize(path).toString(),
|
||||||
)))
|
runContext.storage().putFile(path.toFile(), resolveUniqueNameForFile(path))
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
)))
|
||||||
runContext.logger().info("Captured {} output(s).", allFilesMatching.size());
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
if (runContext.logger().isTraceEnabled()) {
|
||||||
|
runContext.logger().trace("Captured {} output(s).", allFilesMatching.size());
|
||||||
|
}
|
||||||
|
|
||||||
return outputFiles;
|
return outputFiles;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public class Extension extends AbstractExtension {
|
|||||||
return operators;
|
return operators;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Filter> getFilters() {
|
public Map<String, Filter> getFilters() {
|
||||||
Map<String, Filter> filters = new HashMap<>();
|
Map<String, Filter> filters = new HashMap<>();
|
||||||
@@ -104,6 +105,7 @@ public class Extension extends AbstractExtension {
|
|||||||
return tests;
|
return tests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Function> getFunctions() {
|
public Map<String, Function> getFunctions() {
|
||||||
Map<String, Function> functions = new HashMap<>();
|
Map<String, Function> functions = new HashMap<>();
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ import java.util.Map;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public class JsonFilter extends ToJsonFilter {
|
public class JsonFilter extends ToJsonFilter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object apply(Object input, Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException {
|
public Object apply(Object input, Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException {
|
||||||
log.warn("The 'json' filter is deprecated, please use 'toJson' instead");
|
|
||||||
|
|
||||||
return super.apply(input, args, self, context, lineNumber);
|
return super.apply(input, args, self, context, lineNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ import java.util.Map;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public class JsonFunction extends FromJsonFunction {
|
public class JsonFunction extends FromJsonFunction {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
||||||
log.warn("The 'json' function is deprecated, please use 'fromJson' instead");
|
|
||||||
|
|
||||||
return super.execute(args, self, context, lineNumber);
|
return super.execute(args, self, context, lineNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
|||||||
var trigger = f.getTriggerContext().toBuilder().nextExecutionDate(nextExecutionDate).build().checkBackfill();
|
var trigger = f.getTriggerContext().toBuilder().nextExecutionDate(nextExecutionDate).build().checkBackfill();
|
||||||
this.triggerState.save(trigger, scheduleContext);
|
this.triggerState.save(trigger, scheduleContext);
|
||||||
}
|
}
|
||||||
} catch (InternalException ie) {
|
} catch (Exception ie) {
|
||||||
// validate schedule condition can fail to render variables
|
// validate schedule condition can fail to render variables
|
||||||
// in this case, we send a failed execution so the trigger is not evaluated each second.
|
// in this case, we send a failed execution so the trigger is not evaluated each second.
|
||||||
logger.error("Unable to evaluate the trigger '{}'", f.getAbstractTrigger().getId(), ie);
|
logger.error("Unable to evaluate the trigger '{}'", f.getAbstractTrigger().getId(), ie);
|
||||||
|
|||||||
@@ -561,6 +561,11 @@ public class ForEachItem extends Task implements FlowableTask<VoidOutput>, Child
|
|||||||
// get the list of splits from the outputs of the split task
|
// get the list of splits from the outputs of the split task
|
||||||
String taskId = this.id.substring(0, this.id.lastIndexOf('_')) + ForEachItemExecutable.SUFFIX;
|
String taskId = this.id.substring(0, this.id.lastIndexOf('_')) + ForEachItemExecutable.SUFFIX;
|
||||||
var taskOutput = extractOutput(runContext, taskId);
|
var taskOutput = extractOutput(runContext, taskId);
|
||||||
|
if (taskOutput == null) {
|
||||||
|
// there were no subflow executions
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Integer iterations = (Integer) taskOutput.get(ExecutableUtils.TASK_VARIABLE_NUMBER_OF_BATCHES);
|
Integer iterations = (Integer) taskOutput.get(ExecutableUtils.TASK_VARIABLE_NUMBER_OF_BATCHES);
|
||||||
String subflowOutputsBaseUri = (String) taskOutput.get(ExecutableUtils.TASK_VARIABLE_SUBFLOW_OUTPUTS_BASE_URI);
|
String subflowOutputsBaseUri = (String) taskOutput.get(ExecutableUtils.TASK_VARIABLE_SUBFLOW_OUTPUTS_BASE_URI);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import io.kestra.core.models.tasks.VoidOutput;
|
|||||||
import io.kestra.core.runners.FlowableUtils;
|
import io.kestra.core.runners.FlowableUtils;
|
||||||
import io.kestra.core.runners.RunContext;
|
import io.kestra.core.runners.RunContext;
|
||||||
import io.kestra.core.utils.GraphUtils;
|
import io.kestra.core.utils.GraphUtils;
|
||||||
|
import io.kestra.core.utils.ListUtils;
|
||||||
import io.kestra.core.utils.TruthUtils;
|
import io.kestra.core.utils.TruthUtils;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@@ -158,7 +159,7 @@ public class If extends Task implements FlowableTask<VoidOutput> {
|
|||||||
@Override
|
@Override
|
||||||
public Optional<State.Type> resolveState(RunContext runContext, Execution execution, TaskRun parentTaskRun) throws IllegalVariableEvaluationException {
|
public Optional<State.Type> resolveState(RunContext runContext, Execution execution, TaskRun parentTaskRun) throws IllegalVariableEvaluationException {
|
||||||
List<ResolvedTask> childTask = this.childTasks(runContext, parentTaskRun);
|
List<ResolvedTask> childTask = this.childTasks(runContext, parentTaskRun);
|
||||||
if (childTask == null) {
|
if (ListUtils.isEmpty(childTask)) {
|
||||||
// no next task to run, we guess the state from the parent task
|
// no next task to run, we guess the state from the parent task
|
||||||
return Optional.of(execution.guessFinalState(null, parentTaskRun, this.isAllowFailure()));
|
return Optional.of(execution.guessFinalState(null, parentTaskRun, this.isAllowFailure()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,9 @@ public class WaitFor extends Task implements FlowableTask<WaitFor.Output> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer iterationCount = (Integer) parentTaskRun.getOutputs().get("iterationCount");
|
Integer iterationCount = Optional.ofNullable(parentTaskRun.getOutputs())
|
||||||
|
.map(outputs -> (Integer) outputs.get("iterationCount"))
|
||||||
|
.orElse(0);
|
||||||
if (this.checkFrequency.maxIterations != null && iterationCount != null && iterationCount > this.checkFrequency.maxIterations) {
|
if (this.checkFrequency.maxIterations != null && iterationCount != null && iterationCount > this.checkFrequency.maxIterations) {
|
||||||
if (printLog) {logger.warn("Max iterations reached");}
|
if (printLog) {logger.warn("Max iterations reached");}
|
||||||
return true;
|
return true;
|
||||||
@@ -225,7 +227,9 @@ public class WaitFor extends Task implements FlowableTask<WaitFor.Output> {
|
|||||||
|
|
||||||
public WaitFor.Output outputs(TaskRun parentTaskRun) throws IllegalVariableEvaluationException {
|
public WaitFor.Output outputs(TaskRun parentTaskRun) throws IllegalVariableEvaluationException {
|
||||||
String value = parentTaskRun != null ?
|
String value = parentTaskRun != null ?
|
||||||
parentTaskRun.getOutputs().get("iterationCount").toString() : "0";
|
String.valueOf(Optional.ofNullable(parentTaskRun.getOutputs())
|
||||||
|
.map(outputs -> outputs.get("iterationCount"))
|
||||||
|
.orElse("0")) : "0";
|
||||||
|
|
||||||
return Output.builder()
|
return Output.builder()
|
||||||
.iterationCount(Integer.parseInt(value) + 1)
|
.iterationCount(Integer.parseInt(value) + 1)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import java.util.List;
|
|||||||
"- TRACE",
|
"- TRACE",
|
||||||
"- DEBUG",
|
"- DEBUG",
|
||||||
"- INFO",
|
"- INFO",
|
||||||
"- WARNING",
|
"- WARN",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import io.kestra.core.Helpers;
|
import io.kestra.core.Helpers;
|
||||||
import io.kestra.core.events.CrudEvent;
|
import io.kestra.core.events.CrudEvent;
|
||||||
import io.kestra.core.events.CrudEventType;
|
import io.kestra.core.events.CrudEventType;
|
||||||
import io.kestra.core.models.flows.Flow;
|
import io.kestra.core.models.executions.Execution;
|
||||||
import io.kestra.core.models.flows.FlowWithException;
|
import io.kestra.core.models.flows.*;
|
||||||
import io.kestra.core.models.flows.FlowWithSource;
|
|
||||||
import io.kestra.core.models.flows.Type;
|
|
||||||
import io.kestra.core.models.flows.input.StringInput;
|
import io.kestra.core.models.flows.input.StringInput;
|
||||||
import io.kestra.core.models.triggers.Trigger;
|
import io.kestra.core.models.triggers.Trigger;
|
||||||
import io.kestra.core.queues.QueueFactoryInterface;
|
import io.kestra.core.queues.QueueFactoryInterface;
|
||||||
@@ -55,6 +53,9 @@ public abstract class AbstractFlowRepositoryTest {
|
|||||||
@Inject
|
@Inject
|
||||||
protected FlowRepositoryInterface flowRepository;
|
protected FlowRepositoryInterface flowRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected ExecutionRepositoryInterface executionRepository;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private LocalFlowRepositoryLoader repositoryLoader;
|
private LocalFlowRepositoryLoader repositoryLoader;
|
||||||
|
|
||||||
@@ -546,6 +547,67 @@ public abstract class AbstractFlowRepositoryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findByExecution() {
|
||||||
|
Flow flow = builder()
|
||||||
|
.revision(1)
|
||||||
|
.build();
|
||||||
|
flowRepository.create(flow, flow.generateSource(), pluginDefaultService.injectDefaults(flow));
|
||||||
|
Execution execution = Execution.builder()
|
||||||
|
.id(IdUtils.create())
|
||||||
|
.namespace(flow.getNamespace())
|
||||||
|
.flowId(flow.getId())
|
||||||
|
.flowRevision(flow.getRevision())
|
||||||
|
.state(new State())
|
||||||
|
.build();
|
||||||
|
execution = executionRepository.save(execution);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Flow full = flowRepository.findByExecution(execution);
|
||||||
|
assertThat(full, notNullValue());
|
||||||
|
assertThat(full.getNamespace(), is(flow.getNamespace()));
|
||||||
|
assertThat(full.getId(), is(flow.getId()));
|
||||||
|
|
||||||
|
full = flowRepository.findByExecutionWithoutAcl(execution);
|
||||||
|
assertThat(full, notNullValue());
|
||||||
|
assertThat(full.getNamespace(), is(flow.getNamespace()));
|
||||||
|
assertThat(full.getId(), is(flow.getId()));
|
||||||
|
} finally {
|
||||||
|
deleteFlow(flow);
|
||||||
|
executionRepository.delete(execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findByExecutionNoRevision() {
|
||||||
|
Flow flow = builder()
|
||||||
|
.revision(3)
|
||||||
|
.build();
|
||||||
|
flowRepository.create(flow, flow.generateSource(), pluginDefaultService.injectDefaults(flow));
|
||||||
|
Execution execution = Execution.builder()
|
||||||
|
.id(IdUtils.create())
|
||||||
|
.namespace(flow.getNamespace())
|
||||||
|
.flowId(flow.getId())
|
||||||
|
.state(new State())
|
||||||
|
.build();
|
||||||
|
executionRepository.save(execution);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Flow full = flowRepository.findByExecution(execution);
|
||||||
|
assertThat(full, notNullValue());
|
||||||
|
assertThat(full.getNamespace(), is(flow.getNamespace()));
|
||||||
|
assertThat(full.getId(), is(flow.getId()));
|
||||||
|
|
||||||
|
full = flowRepository.findByExecutionWithoutAcl(execution);
|
||||||
|
assertThat(full, notNullValue());
|
||||||
|
assertThat(full.getNamespace(), is(flow.getNamespace()));
|
||||||
|
assertThat(full.getId(), is(flow.getId()));
|
||||||
|
} finally {
|
||||||
|
deleteFlow(flow);
|
||||||
|
executionRepository.delete(execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void deleteFlow(Flow flow) {
|
private void deleteFlow(Flow flow) {
|
||||||
Integer revision = flowRepository.lastRevision(flow.getTenantId(), flow.getNamespace(), flow.getId());
|
Integer revision = flowRepository.lastRevision(flow.getTenantId(), flow.getNamespace(), flow.getId());
|
||||||
flowRepository.delete(flow.toBuilder().revision(revision).build());
|
flowRepository.delete(flow.toBuilder().revision(revision).build());
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public abstract class AbstractLogRepositoryTest {
|
|||||||
|
|
||||||
logRepository.save(log1);
|
logRepository.save(log1);
|
||||||
|
|
||||||
logRepository.deleteByQuery(null, "io.kestra.unittest", "flowId", null, null, ZonedDateTime.now().plusMinutes(1));
|
logRepository.deleteByQuery(null, "io.kestra.unittest", "flowId", List.of(Level.TRACE, Level.DEBUG, Level.INFO), null, ZonedDateTime.now().plusMinutes(1));
|
||||||
|
|
||||||
find = logRepository.findByExecutionId(null, log1.getExecutionId(), null, Pageable.from(1, 50));
|
find = logRepository.findByExecutionId(null, log1.getExecutionId(), null, Pageable.from(1, 50));
|
||||||
assertThat(find.size(), is(0));
|
assertThat(find.size(), is(0));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.apache.commons.io.FileUtils;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
@@ -49,4 +50,13 @@ class FilesServiceTest {
|
|||||||
Map<String, URI> outputs = FilesService.outputFiles(runContext, files.keySet().stream().toList());
|
Map<String, URI> outputs = FilesService.outputFiles(runContext, files.keySet().stream().toList());
|
||||||
assertThat(outputs.size(), is(1));
|
assertThat(outputs.size(), is(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void renderOutputFiles() throws Exception {
|
||||||
|
RunContext runContext = runContextFactory.of(Map.of("extension", "txt"));
|
||||||
|
Map<String, String> files = FilesService.inputFiles(runContext, Map.of("file.txt", "content"));
|
||||||
|
|
||||||
|
Map<String, URI> outputs = FilesService.outputFiles(runContext, List.of("*.{{extension}}"));
|
||||||
|
assertThat(outputs.size(), is(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,21 @@ public class ForEachItemCaseTest {
|
|||||||
assertThat(triggered.get().getTaskRunList(), hasSize(1));
|
assertThat(triggered.get().getTaskRunList(), hasSize(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void forEachItemEmptyItems() throws TimeoutException, URISyntaxException, IOException {
|
||||||
|
URI file = emptyItems();
|
||||||
|
Map<String, Object> inputs = Map.of("file", file.toString());
|
||||||
|
Execution execution = runnerUtils.runOne(null, TEST_NAMESPACE, "for-each-item", null,
|
||||||
|
(flow, execution1) -> flowIO.typedInputs(flow, execution1, inputs),
|
||||||
|
Duration.ofSeconds(30));
|
||||||
|
|
||||||
|
// assert on the main flow execution
|
||||||
|
assertThat(execution.getTaskRunList(), hasSize(4));
|
||||||
|
assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
|
||||||
|
Map<String, Object> outputs = execution.getTaskRunList().get(2).getOutputs();
|
||||||
|
assertThat(outputs, nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void forEachItemNoWait() throws TimeoutException, InterruptedException, URISyntaxException, IOException {
|
public void forEachItemNoWait() throws TimeoutException, InterruptedException, URISyntaxException, IOException {
|
||||||
CountDownLatch countDownLatch = new CountDownLatch(26);
|
CountDownLatch countDownLatch = new CountDownLatch(26);
|
||||||
@@ -261,6 +276,16 @@ public class ForEachItemCaseTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private URI emptyItems() throws URISyntaxException, IOException {
|
||||||
|
File tempFile = File.createTempFile("file", ".txt");
|
||||||
|
|
||||||
|
return storageInterface.put(
|
||||||
|
null,
|
||||||
|
new URI("/file/storage/file.txt"),
|
||||||
|
new FileInputStream(tempFile)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> content() {
|
private List<String> content() {
|
||||||
return IntStream
|
return IntStream
|
||||||
.range(0, 102)
|
.range(0, 102)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
version=0.18.0-SNAPSHOT
|
version=0.18.5
|
||||||
|
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.priority=low
|
org.gradle.priority=low
|
||||||
|
|||||||
@@ -7,9 +7,17 @@ import org.jooq.*;
|
|||||||
import org.jooq.Record;
|
import org.jooq.Record;
|
||||||
import org.jooq.impl.DSL;
|
import org.jooq.impl.DSL;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class MysqlQueue<T> extends JdbcQueue<T> {
|
public class MysqlQueue<T> extends JdbcQueue<T> {
|
||||||
|
|
||||||
|
// TODO - remove once 'queue' table is re-designed
|
||||||
|
private static final MysqlQueueConsumers QUEUE_CONSUMERS = new MysqlQueueConsumers();
|
||||||
|
|
||||||
public MysqlQueue(Class<T> cls, ApplicationContext applicationContext) {
|
public MysqlQueue(Class<T> cls, ApplicationContext applicationContext) {
|
||||||
super(cls, applicationContext);
|
super(cls, applicationContext);
|
||||||
}
|
}
|
||||||
@@ -59,7 +67,7 @@ public class MysqlQueue<T> extends JdbcQueue<T> {
|
|||||||
.where(AbstractJdbcRepository.field("type").eq(this.cls.getName()))
|
.where(AbstractJdbcRepository.field("type").eq(this.cls.getName()))
|
||||||
.and(DSL.or(List.of(
|
.and(DSL.or(List.of(
|
||||||
AbstractJdbcRepository.field("consumers").isNull(),
|
AbstractJdbcRepository.field("consumers").isNull(),
|
||||||
DSL.condition("NOT(FIND_IN_SET(?, consumers) > 0)", queueType)
|
AbstractJdbcRepository.field("consumers").in(QUEUE_CONSUMERS.allForConsumerNotIn(queueType))
|
||||||
)));
|
)));
|
||||||
|
|
||||||
if (consumerGroup != null) {
|
if (consumerGroup != null) {
|
||||||
@@ -101,4 +109,38 @@ public class MysqlQueue<T> extends JdbcQueue<T> {
|
|||||||
|
|
||||||
update.execute();
|
update.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class MysqlQueueConsumers {
|
||||||
|
|
||||||
|
private static final Set<String> CONSUMERS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
CONSUMERS = new HashSet<>();
|
||||||
|
String[] elements = {"indexer", "executor", "worker", "scheduler"};
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
// Generate all combinations and their permutations
|
||||||
|
generateCombinations(elements, new boolean[elements.length], new ArrayList<>(), results);
|
||||||
|
CONSUMERS.addAll(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> allForConsumerNotIn(String consumer) {
|
||||||
|
return CONSUMERS.stream().filter(s -> !s.contains(consumer)).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void generateCombinations(String[] elements, boolean[] used, List<String> current, List<String> results) {
|
||||||
|
if (!current.isEmpty()) {
|
||||||
|
results.add(String.join(",", current));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < elements.length; i++) {
|
||||||
|
if (!used[i]) {
|
||||||
|
used[i] = true;
|
||||||
|
current.add(elements[i]);
|
||||||
|
generateCombinations(elements, used, current, results);
|
||||||
|
current.removeLast();
|
||||||
|
used[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.jooq.impl.DSL;
|
|||||||
import org.slf4j.event.Level;
|
import org.slf4j.event.Level;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
@@ -27,10 +28,9 @@ public class PostgresLogRepository extends AbstractJdbcLogRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Condition minLevel(Level minLevel) {
|
protected Condition levelsCondition(List<Level> levels) {
|
||||||
return DSL.condition("level in (" +
|
return DSL.condition("level in (" +
|
||||||
LogEntry
|
levels
|
||||||
.findLevelsByMin(minLevel)
|
|
||||||
.stream()
|
.stream()
|
||||||
.map(s -> "'" + s + "'::log_level")
|
.map(s -> "'" + s + "'::log_level")
|
||||||
.collect(Collectors.joining(", ")) +
|
.collect(Collectors.joining(", ")) +
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.kestra.repository.postgres;
|
package io.kestra.repository.postgres;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
import io.kestra.core.queues.QueueService;
|
import io.kestra.core.queues.QueueService;
|
||||||
import io.kestra.core.repositories.ArrayListTotal;
|
import io.kestra.core.repositories.ArrayListTotal;
|
||||||
import io.kestra.jdbc.JdbcMapper;
|
import io.kestra.jdbc.JdbcMapper;
|
||||||
@@ -21,6 +22,7 @@ import org.jooq.Result;
|
|||||||
import org.jooq.SelectConditionStep;
|
import org.jooq.SelectConditionStep;
|
||||||
import org.jooq.impl.DSL;
|
import org.jooq.impl.DSL;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
@@ -52,12 +54,10 @@ public class PostgresRepository<T> extends io.kestra.jdbc.AbstractJdbcRepository
|
|||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@Override
|
@Override
|
||||||
public Map<Field<Object>, Object> persistFields(T entity) {
|
public Map<Field<Object>, Object> persistFields(T entity) {
|
||||||
Map<Field<Object>, Object> fields = super.persistFields(entity);
|
|
||||||
|
|
||||||
String json = JdbcMapper.of().writeValueAsString(entity);
|
String json = JdbcMapper.of().writeValueAsString(entity);
|
||||||
fields.replace(AbstractJdbcRepository.field("value"), DSL.val(JSONB.valueOf(json)));
|
return new HashMap<>(ImmutableMap
|
||||||
|
.of(io.kestra.jdbc.repository.AbstractJdbcRepository.field("value"), DSL.val(JSONB.valueOf(json)))
|
||||||
return fields;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ import java.util.function.Function;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcRepository implements ExecutionRepositoryInterface, JdbcIndexerInterface<Execution> {
|
public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcRepository implements ExecutionRepositoryInterface, JdbcIndexerInterface<Execution> {
|
||||||
|
private static final int FETCH_SIZE = 100;
|
||||||
|
|
||||||
protected final io.kestra.jdbc.AbstractJdbcRepository<Execution> jdbcRepository;
|
protected final io.kestra.jdbc.AbstractJdbcRepository<Execution> jdbcRepository;
|
||||||
private final ApplicationEventPublisher<CrudEvent<Execution>> eventPublisher;
|
private final ApplicationEventPublisher<CrudEvent<Execution>> eventPublisher;
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
@@ -108,10 +110,13 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
|||||||
.where(this.defaultFilter(tenantId))
|
.where(this.defaultFilter(tenantId))
|
||||||
.and(field("trigger_execution_id").eq(triggerExecutionId));
|
.and(field("trigger_execution_id").eq(triggerExecutionId));
|
||||||
|
|
||||||
select.fetch()
|
// fetchSize will fetch rows 100 by 100 even for databases where the driver loads all in memory
|
||||||
.map(this.jdbcRepository::map)
|
// using a stream will fetch lazily, otherwise all fetches would be done before starting emitting the items
|
||||||
.forEach(emitter::next);
|
try (var stream = select.fetchSize(FETCH_SIZE).stream()) {
|
||||||
emitter.complete();
|
stream.map(this.jdbcRepository::map).forEach(emitter::next);
|
||||||
|
} finally {
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
FluxSink.OverflowStrategy.BUFFER
|
FluxSink.OverflowStrategy.BUFFER
|
||||||
);
|
);
|
||||||
@@ -213,11 +218,13 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
|||||||
deleted
|
deleted
|
||||||
);
|
);
|
||||||
|
|
||||||
select.fetch()
|
// fetchSize will fetch rows 100 by 100 even for databases where the driver loads all in memory
|
||||||
.map(this.jdbcRepository::map)
|
// using a stream will fetch lazily, otherwise all fetches would be done before starting emitting the items
|
||||||
.forEach(emitter::next);
|
try (var stream = select.fetchSize(FETCH_SIZE).stream()) {
|
||||||
|
stream.map(this.jdbcRepository::map).forEach(emitter::next);
|
||||||
emitter.complete();
|
} finally {
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
FluxSink.OverflowStrategy.BUFFER
|
FluxSink.OverflowStrategy.BUFFER
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (logLevels != null) {
|
if (logLevels != null) {
|
||||||
delete = delete.and(field("level").in(logLevels));
|
delete = delete.and(levelsCondition(logLevels));
|
||||||
}
|
}
|
||||||
|
|
||||||
return delete.execute();
|
return delete.execute();
|
||||||
@@ -493,7 +493,11 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Condition minLevel(Level minLevel) {
|
private Condition minLevel(Level minLevel) {
|
||||||
return field("level").in(LogEntry.findLevelsByMin(minLevel));
|
return levelsCondition(LogEntry.findLevelsByMin(minLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Condition levelsCondition(List<Level> levels) {
|
||||||
|
return field("level").in(levels.stream().map(level -> level.name()).toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,6 +276,11 @@ public abstract class JdbcRunnerTest {
|
|||||||
forEachItemCaseTest.forEachItem();
|
forEachItemCaseTest.forEachItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forEachItemEmptyItems() throws URISyntaxException, IOException, TimeoutException {
|
||||||
|
forEachItemCaseTest.forEachItemEmptyItems();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void forEachItemNoWait() throws URISyntaxException, IOException, InterruptedException, TimeoutException {
|
void forEachItemNoWait() throws URISyntaxException, IOException, InterruptedException, TimeoutException {
|
||||||
forEachItemCaseTest.forEachItemNoWait();
|
forEachItemCaseTest.forEachItemNoWait();
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ public class Docker extends TaskRunner {
|
|||||||
CreateContainerResponse exec = container.exec();
|
CreateContainerResponse exec = container.exec();
|
||||||
logger.debug("Container created: {}", exec.getId());
|
logger.debug("Container created: {}", exec.getId());
|
||||||
|
|
||||||
List<Path> relativeWorkingDirectoryFilesPaths = taskCommands.relativeWorkingDirectoryFilesPaths();
|
List<Path> relativeWorkingDirectoryFilesPaths = taskCommands.relativeWorkingDirectoryFilesPaths(true);
|
||||||
boolean hasFilesToUpload = !ListUtils.isEmpty(relativeWorkingDirectoryFilesPaths);
|
boolean hasFilesToUpload = !ListUtils.isEmpty(relativeWorkingDirectoryFilesPaths);
|
||||||
boolean hasFilesToDownload = !ListUtils.isEmpty(filesToDownload);
|
boolean hasFilesToDownload = !ListUtils.isEmpty(filesToDownload);
|
||||||
boolean outputDirectoryEnabled = taskCommands.outputDirectoryEnabled();
|
boolean outputDirectoryEnabled = taskCommands.outputDirectoryEnabled();
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ public abstract class AbstractTaskRunnerTest {
|
|||||||
Mockito.when(commands.getEnableOutputDirectory()).thenReturn(true);
|
Mockito.when(commands.getEnableOutputDirectory()).thenReturn(true);
|
||||||
Mockito.when(commands.outputDirectoryEnabled()).thenReturn(true);
|
Mockito.when(commands.outputDirectoryEnabled()).thenReturn(true);
|
||||||
Mockito.when(commands.getTimeout()).thenReturn(null);
|
Mockito.when(commands.getTimeout()).thenReturn(null);
|
||||||
Mockito.when(commands.relativeWorkingDirectoryFilesPaths()).thenCallRealMethod();
|
Mockito.when(commands.relativeWorkingDirectoryFilesPaths(true)).thenCallRealMethod();
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
public/vscode/
|
|
||||||
public/vscode-web/
|
|
||||||
|
|
||||||
node/
|
node/
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
<strong>We're sorry but Kestra doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong>We're sorry but Kestra doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<div id="loader-wrapper">
|
<div id="loader-wrapper" data-test-id="loader-wrapper">
|
||||||
<div id="loader"></div>
|
<div id="loader" data-test-id="loader"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="app-container">
|
<div id="app-container">
|
||||||
|
|||||||
2280
ui/package-lock.json
generated
2280
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "kestra",
|
"name": "kestra",
|
||||||
"version": "0.1.0",
|
"version": "0.18.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "npm@9.8.1",
|
"type": "module",
|
||||||
|
"packageManager": "npm@9.9.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "vite build --emptyOutDir",
|
"build": "vite build --emptyOutDir",
|
||||||
@@ -12,17 +13,17 @@
|
|||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kestra-io/ui-libs": "^0.0.53",
|
"@kestra-io/ui-libs": "^0.0.57",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/background": "^1.3.0",
|
||||||
"@vue-flow/controls": "^1.1.2",
|
"@vue-flow/controls": "^1.1.2",
|
||||||
"@vue-flow/core": "^1.39.1",
|
"@vue-flow/core": "^1.39.3",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.3",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.3",
|
||||||
"chartjs-chart-treemap": "^2.3.1",
|
"chartjs-chart-treemap": "^2.3.1",
|
||||||
"core-js": "^3.37.1",
|
"core-js": "^3.38.0",
|
||||||
"cronstrue": "^2.50.0",
|
"cronstrue": "^2.50.0",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"element-plus": "^2.7.8",
|
"element-plus": "^2.7.8",
|
||||||
@@ -39,20 +40,18 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-range": "4.0.2",
|
"moment-range": "4.0.2",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.45",
|
||||||
"node-modules-polyfill": "^0.1.4",
|
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdfjs-dist": "^4.5.136",
|
"pdfjs-dist": "^4.5.136",
|
||||||
"posthog-js": "^1.150.1",
|
"posthog-js": "^1.154.5",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vue": "^3.4.36",
|
||||||
"vue": "^3.4.34",
|
|
||||||
"vue-axios": "3.5.2",
|
"vue-axios": "3.5.2",
|
||||||
"vue-chartjs": "^5.3.1",
|
"vue-chartjs": "^5.3.1",
|
||||||
"vue-gtag": "^2.0.1",
|
"vue-gtag": "^2.0.1",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^9.13.1",
|
||||||
"vue-material-design-icons": "^5.3.0",
|
"vue-material-design-icons": "^5.3.0",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.3",
|
||||||
"vue-sidebar-menu": "^5.4.0",
|
"vue-sidebar-menu": "^5.4.1",
|
||||||
"vue-virtual-scroller": "^2.0.0-beta.8",
|
"vue-virtual-scroller": "^2.0.0-beta.8",
|
||||||
"vue3-popper": "^1.5.0",
|
"vue3-popper": "^1.5.0",
|
||||||
"vue3-runtime-template": "^1.0.2",
|
"vue3-runtime-template": "^1.0.2",
|
||||||
@@ -62,10 +61,11 @@
|
|||||||
"yaml": "^2.5.0"
|
"yaml": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||||
"@rushstack/eslint-patch": "^1.10.4",
|
"@rushstack/eslint-patch": "^1.10.4",
|
||||||
"@shikijs/markdown-it": "^1.6.3",
|
"@shikijs/markdown-it": "^1.12.1",
|
||||||
"@typescript-eslint/parser": "^7.17.0",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-vue": "^5.1.1",
|
"@vitejs/plugin-vue": "^5.1.2",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
@@ -77,13 +77,14 @@
|
|||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "^1.77.4",
|
"sass": "^1.77.8",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.3.5",
|
"vite": "^5.3.5",
|
||||||
"vitest": "^2.0.4"
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vitest": "^2.0.5"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-x64-gnu": "^4.19.0"
|
"@rollup/rollup-linux-x64-gnu": "^4.20.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"bootstrap": {
|
"bootstrap": {
|
||||||
|
|||||||
30
ui/plugins/commit.ts
Normal file
30
ui/plugins/commit.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type {Plugin} from "vite";
|
||||||
|
import {execSync} from "child_process";
|
||||||
|
|
||||||
|
const getInfo = (formats: string[]): string[] => formats.map(format => execSync(`git log -1 --format=${format}`).toString().trim());
|
||||||
|
|
||||||
|
const comment = (message: string, author: string, date: string): string => `
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Last Commit:
|
||||||
|
|
||||||
|
${message}
|
||||||
|
----------
|
||||||
|
Author: ${author}
|
||||||
|
Date: ${date}
|
||||||
|
|
||||||
|
-->`;
|
||||||
|
|
||||||
|
export const commit = (): Plugin => {
|
||||||
|
const [message, author, date] = getInfo(["%s", "%an", "%cd"]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "commit",
|
||||||
|
transformIndexHtml: {
|
||||||
|
order: "pre",
|
||||||
|
handler(html: string): string {
|
||||||
|
return comment(message, author, date) + html;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type {Plugin} from "vite";
|
|
||||||
import {execSync} from "child_process";
|
|
||||||
|
|
||||||
const comment = (hash: string, date: string): string => {
|
|
||||||
return `
|
|
||||||
<!--
|
|
||||||
|
|
||||||
Last Commit:
|
|
||||||
|
|
||||||
URL: https://github.com/kestra-io/kestra/commit/${hash}
|
|
||||||
Date: ${date}
|
|
||||||
|
|
||||||
-->`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const details = (): Plugin => {
|
|
||||||
const hash: string = execSync("git rev-parse --short HEAD").toString().trim();
|
|
||||||
const date: string = execSync("git log -1 --format=%cd").toString().trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "details",
|
|
||||||
transformIndexHtml: {
|
|
||||||
order: "pre",
|
|
||||||
handler(html: string): string {
|
|
||||||
return comment(hash, date) + html;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -106,6 +106,13 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-switch
|
||||||
|
:model-value="showChart"
|
||||||
|
@update:model-value="onShowChartChange"
|
||||||
|
:active-text="$t('show chart')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<filters :storage-key="filterStorageKey" />
|
<filters :storage-key="filterStorageKey" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -118,7 +125,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #top v-if="isDisplayedTop">
|
<template #top v-if="showStatChart()">
|
||||||
<state-global-chart
|
<state-global-chart
|
||||||
v-if="daily"
|
v-if="daily"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
@@ -493,6 +500,7 @@
|
|||||||
dblClickRouteName: "executions/update",
|
dblClickRouteName: "executions/update",
|
||||||
flowTriggerDetails: undefined,
|
flowTriggerDetails: undefined,
|
||||||
recomputeInterval: false,
|
recomputeInterval: false,
|
||||||
|
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_CHART)),
|
||||||
optionalColumns: [
|
optionalColumns: [
|
||||||
{
|
{
|
||||||
label: "start date",
|
label: "start date",
|
||||||
@@ -568,7 +576,6 @@
|
|||||||
}
|
}
|
||||||
this.displayColumns = localStorage.getItem(this.storageKey)?.split(",")
|
this.displayColumns = localStorage.getItem(this.storageKey)?.split(",")
|
||||||
|| this.optionalColumns.filter(col => col.default).map(col => col.prop);
|
|| this.optionalColumns.filter(col => col.default).map(col => col.prop);
|
||||||
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState("execution", ["executions", "total"]),
|
...mapState("execution", ["executions", "total"]),
|
||||||
@@ -641,6 +648,17 @@
|
|||||||
displayColumn(column) {
|
displayColumn(column) {
|
||||||
return this.hidden ? !this.hidden.includes(column) : this.displayColumns.includes(column);
|
return this.hidden ? !this.hidden.includes(column) : this.displayColumns.includes(column);
|
||||||
},
|
},
|
||||||
|
onShowChartChange(value) {
|
||||||
|
this.showChart = value;
|
||||||
|
localStorage.setItem(storageKeys.SHOW_CHART, value);
|
||||||
|
|
||||||
|
if (this.showChart) {
|
||||||
|
this.loadStats();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showStatChart() {
|
||||||
|
return this.isDisplayedTop && this.showChart;
|
||||||
|
},
|
||||||
refresh() {
|
refresh() {
|
||||||
this.recomputeInterval = !this.recomputeInterval;
|
this.recomputeInterval = !this.recomputeInterval;
|
||||||
this.load();
|
this.load();
|
||||||
@@ -676,19 +694,22 @@
|
|||||||
|
|
||||||
return _merge(base, queryFilter)
|
return _merge(base, queryFilter)
|
||||||
},
|
},
|
||||||
|
loadStats() {
|
||||||
|
this.dailyReady = false;
|
||||||
|
|
||||||
|
this.$store
|
||||||
|
.dispatch("stat/daily", this.loadQuery({
|
||||||
|
startDate: this.$moment(this.startDate).toISOString(true),
|
||||||
|
endDate: this.$moment(this.endDate).toISOString(true)
|
||||||
|
}, true))
|
||||||
|
.then(() => {
|
||||||
|
this.dailyReady = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
loadData(callback) {
|
loadData(callback) {
|
||||||
this.refreshDates = !this.refreshDates;
|
this.refreshDates = !this.refreshDates;
|
||||||
if (this.isDisplayedTop) {
|
if (this.showStatChart()) {
|
||||||
this.dailyReady = false;
|
this.loadStats();
|
||||||
|
|
||||||
this.$store
|
|
||||||
.dispatch("stat/daily", this.loadQuery({
|
|
||||||
startDate: this.$moment(this.startDate).toISOString(true),
|
|
||||||
endDate: this.$moment(this.endDate).toISOString(true)
|
|
||||||
}, true))
|
|
||||||
.then(() => {
|
|
||||||
this.dailyReady = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.dispatch("execution/findExecutions", this.loadQuery({
|
this.$store.dispatch("execution/findExecutions", this.loadQuery({
|
||||||
@@ -765,7 +786,7 @@
|
|||||||
},
|
},
|
||||||
deleteExecutions() {
|
deleteExecutions() {
|
||||||
const includeNonTerminated = ref(false);
|
const includeNonTerminated = ref(false);
|
||||||
|
|
||||||
const deleteLogs = ref(true);
|
const deleteLogs = ref(true);
|
||||||
const deleteMetrics = ref(true);
|
const deleteMetrics = ref(true);
|
||||||
const deleteStorage = ref(true);
|
const deleteStorage = ref(true);
|
||||||
@@ -784,7 +805,7 @@
|
|||||||
"onUpdate:modelValue": (val) => {
|
"onUpdate:modelValue": (val) => {
|
||||||
includeNonTerminated.value = val
|
includeNonTerminated.value = val
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
h(ElAlert, {
|
h(ElAlert, {
|
||||||
title: this.$t("execution-warn-deleting-still-running"),
|
title: this.$t("execution-warn-deleting-still-running"),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button-group>
|
<el-button-group>
|
||||||
|
<restart :execution="execution" class="ms-0" @follow="forwardEvent('follow', $event)" />
|
||||||
<el-button @click="downloadContent()">
|
<el-button @click="downloadContent()">
|
||||||
<kicon :tooltip="$t('download logs')">
|
<kicon :tooltip="$t('download logs')">
|
||||||
<download />
|
<download />
|
||||||
@@ -79,6 +80,7 @@
|
|||||||
import State from "../../utils/state";
|
import State from "../../utils/state";
|
||||||
import Utils from "../../utils/utils";
|
import Utils from "../../utils/utils";
|
||||||
import LogLine from "../logs/LogLine.vue";
|
import LogLine from "../logs/LogLine.vue";
|
||||||
|
import Restart from "./Restart.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -88,7 +90,8 @@
|
|||||||
Kicon,
|
Kicon,
|
||||||
Download,
|
Download,
|
||||||
Magnify,
|
Magnify,
|
||||||
Collapse
|
Collapse,
|
||||||
|
Restart
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-button-group>
|
</el-button-group>
|
||||||
|
|
||||||
|
<el-button-group v-else-if="isURI(value)">
|
||||||
|
<a class="el-button el-button--small el-button--primary" :href="value" target="_blank">
|
||||||
|
<OpenInNew />
|
||||||
|
{{ $t('open') }}
|
||||||
|
</a>
|
||||||
|
</el-button-group>
|
||||||
|
|
||||||
<span v-else-if="value === null">
|
<span v-else-if="value === null">
|
||||||
<em>null</em>
|
<em>null</em>
|
||||||
</span>
|
</span>
|
||||||
@@ -20,6 +27,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Download from "vue-material-design-icons/Download.vue";
|
import Download from "vue-material-design-icons/Download.vue";
|
||||||
|
import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
|
||||||
import FilePreview from "./FilePreview.vue";
|
import FilePreview from "./FilePreview.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -37,6 +45,14 @@
|
|||||||
isFile(value) {
|
isFile(value) {
|
||||||
return typeof(value) === "string" && value.startsWith("kestra:///")
|
return typeof(value) === "string" && value.startsWith("kestra:///")
|
||||||
},
|
},
|
||||||
|
isURI(value) {
|
||||||
|
try {
|
||||||
|
new URL(value);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
itemUrl(value) {
|
itemUrl(value) {
|
||||||
return `${apiUrl(this.$store)}/executions/${this.execution.id}/file?path=${value}`;
|
return `${apiUrl(this.$store)}/executions/${this.execution.id}/file?path=${value}`;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
@expand-change="() => scrollRight()"
|
@expand-change="() => scrollRight()"
|
||||||
>
|
>
|
||||||
<template #default="{data}">
|
<template #default="{data}">
|
||||||
<div v-if="data.heading" class="pe-none d-flex fs-5">
|
<div v-if="data.heading" @click="expandedValue = data.path" class="pe-none d-flex fs-5">
|
||||||
<component :is="data.component" class="me-2" />
|
<component :is="data.component" class="me-2" />
|
||||||
<span>{{ data.label }}</span>
|
<span>{{ data.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="w-100 d-flex justify-content-between">
|
<div v-else @click="expandedValue = data.path" class="w-100 d-flex justify-content-between">
|
||||||
<div class="pe-5 d-flex task">
|
<div class="pe-5 d-flex task">
|
||||||
<TaskIcon v-if="data.icon" :icons="allIcons" :cls="icons[data.taskId]" only-icon />
|
<TaskIcon v-if="data.icon" :icons="allIcons" :cls="icons[data.taskId]" only-icon />
|
||||||
<span :class="{'ms-3': data.icon}">{{ data.label }}</span>
|
<span :class="{'ms-3': data.icon}">{{ data.label }}</span>
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
:input="true"
|
:input="true"
|
||||||
:navbar="false"
|
:navbar="false"
|
||||||
:model-value="computedDebugValue"
|
:model-value="computedDebugValue"
|
||||||
|
@confirm="onDebugExpression($event)"
|
||||||
class="w-100"
|
class="w-100"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -91,9 +92,13 @@
|
|||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
|
|
||||||
<el-alert v-if="debugError" type="error" :closable="false">
|
<el-alert v-if="debugError" type="error" :closable="false" class="overflow-auto">
|
||||||
<p><strong>{{ debugError }}</strong></p>
|
<p><strong>{{ debugError }}</strong></p>
|
||||||
<pre class="mb-0">{{ debugStackTrace }}</pre>
|
<div class="my-2">
|
||||||
|
<CopyToClipboard :text="debugError" label="Copy Error" class="d-inline-block me-2" />
|
||||||
|
<CopyToClipboard :text="debugStackTrace" label="Copy Stack Trace" class="d-inline-block" />
|
||||||
|
</div>
|
||||||
|
<pre class="mb-0" style="overflow: scroll;">{{ debugStackTrace }}</pre>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<VarValue :value="selectedValue" :execution="execution" />
|
<VarValue :value="selectedValue" :execution="execution" />
|
||||||
@@ -115,15 +120,25 @@
|
|||||||
|
|
||||||
import {apiUrl} from "override/utils/route";
|
import {apiUrl} from "override/utils/route";
|
||||||
|
|
||||||
|
import CopyToClipboard from "../../layout/CopyToClipboard.vue"
|
||||||
|
|
||||||
import Editor from "../../inputs/Editor.vue";
|
import Editor from "../../inputs/Editor.vue";
|
||||||
const debugEditor = ref(null);
|
const debugEditor = ref(null);
|
||||||
const debugExpression = ref("");
|
const debugExpression = ref("");
|
||||||
const computedDebugValue = computed(() => `{{ outputs${selectedTask()?.taskId ? `.${selectedTask().taskId}` : ""} }}`);
|
const computedDebugValue = computed(() => {
|
||||||
|
const task = selectedTask()?.taskId;
|
||||||
|
if(!task) return "";
|
||||||
|
|
||||||
|
const path = expandedValue.value;
|
||||||
|
if(!path) return `{{ outputs.${task} }}`
|
||||||
|
|
||||||
|
return `{{ outputs.${path} }}`
|
||||||
|
});
|
||||||
const debugError = ref("");
|
const debugError = ref("");
|
||||||
const debugStackTrace = ref("");
|
const debugStackTrace = ref("");
|
||||||
const isJSON = ref(false);
|
const isJSON = ref(false);
|
||||||
const selectedTask = () => {
|
const selectedTask = () => {
|
||||||
const filter = selected.value.length ? selected.value[0] : (cascader.value as any).menuList?.[0]?.panel?.expandingNode?.label;
|
const filter = selected.value?.length ? selected.value[0] : (cascader.value as any).menuList?.[0]?.panel?.expandingNode?.label;
|
||||||
const taskRunList = [...execution.value.taskRunList];
|
const taskRunList = [...execution.value.taskRunList];
|
||||||
return taskRunList.find(e => e.taskId === filter);
|
return taskRunList.find(e => e.taskId === filter);
|
||||||
};
|
};
|
||||||
@@ -162,22 +177,41 @@
|
|||||||
|
|
||||||
const execution = computed(() => store.state.execution.execution);
|
const execution = computed(() => store.state.execution.execution);
|
||||||
|
|
||||||
const processedValue = (data): { label: string, regular: boolean; } => {
|
function isValidURL(url) {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedValue = (data) => {
|
||||||
const regular = false;
|
const regular = false;
|
||||||
|
|
||||||
if (!data.value && !data.children?.length) return {label: data.value, regular};
|
if (!data.value && !data.children?.length) {
|
||||||
else if (data?.children?.length) {
|
return {label: data.value, regular};
|
||||||
|
} else if (data?.children?.length) {
|
||||||
const message = (length) => ({label: `${length} items`, regular});
|
const message = (length) => ({label: `${length} items`, regular});
|
||||||
const length = data.children.length;
|
const length = data.children.length;
|
||||||
|
|
||||||
return data.children[0].isFirstPass ? message(length - 1) : message(length);
|
return data.children[0].isFirstPass ? message(length - 1) : message(length);
|
||||||
}
|
}
|
||||||
return data.value.toString().startsWith("kestra:///") ? {label: "Internal link", regular} : {label: trim(data.value), regular: true};
|
|
||||||
|
// Check if the value is a valid URL and not an internal "kestra:///" link
|
||||||
|
if (isValidURL(data.value)) {
|
||||||
|
return data.value.startsWith("kestra:///")
|
||||||
|
? {label: "Internal link", regular}
|
||||||
|
: {label: "External link", regular};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {label: trim(data.value), regular: true};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expandedValue = ref([])
|
||||||
const selected = ref([]);
|
const selected = ref([]);
|
||||||
const selectedValue = computed(() => {
|
const selectedValue = computed(() => {
|
||||||
if (selected.value.length) return selected.value[selected.value.length - 1];
|
if (selected.value?.length) return selected.value[selected.value.length - 1];
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
const selectedNode = () => {
|
const selectedNode = () => {
|
||||||
@@ -190,21 +224,34 @@
|
|||||||
return {label, value};
|
return {label, value};
|
||||||
};
|
};
|
||||||
|
|
||||||
const transform = (o, isFirstPass = true) => {
|
const transform = (o, isFirstPass, path = "") => {
|
||||||
const result = Object.keys(o).map(key => {
|
const result = Object.keys(o).map(key => {
|
||||||
const value = o[key];
|
const value = o[key];
|
||||||
const isObject = typeof value === "object" && value !== null;
|
const isObject = typeof value === "object" && value !== null;
|
||||||
|
|
||||||
|
const currentPath = `${path}["${key}"]`;
|
||||||
|
|
||||||
// If the value is an array with exactly one element, use that element as the value
|
// If the value is an array with exactly one element, use that element as the value
|
||||||
if (Array.isArray(value) && value.length === 1) {
|
if (Array.isArray(value) && value.length === 1) {
|
||||||
return {label: key, value: value[0], children: []};
|
return {label: key, value: value[0], children: [], path: currentPath};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {label: key, value: isObject && !Array.isArray(value) ? null : value, children: isObject ? transform(value, false) : []};
|
return {
|
||||||
|
label: key,
|
||||||
|
value: isObject && !Array.isArray(value) ? key : value,
|
||||||
|
children: isObject ? transform(value, false, currentPath) : [],
|
||||||
|
path: currentPath
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFirstPass) {
|
if (isFirstPass) {
|
||||||
const OUTPUTS = {label: t("outputs"), heading: true, component: shallowRef(TextBoxSearchOutline), isFirstPass: true};
|
const OUTPUTS = {
|
||||||
|
label: t("outputs"),
|
||||||
|
heading: true,
|
||||||
|
component: shallowRef(TextBoxSearchOutline),
|
||||||
|
isFirstPass: true,
|
||||||
|
path: path
|
||||||
|
};
|
||||||
result.unshift(OUTPUTS);
|
result.unshift(OUTPUTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +259,7 @@
|
|||||||
};
|
};
|
||||||
const outputs = computed(() => {
|
const outputs = computed(() => {
|
||||||
const tasks = store.state.execution.execution.taskRunList.map((task) => {
|
const tasks = store.state.execution.execution.taskRunList.map((task) => {
|
||||||
return {label: task.taskId, value: task.taskId, ...task, icon: true, children: task?.outputs ? transform(task.outputs) : []};
|
return {label: task.taskId, value: task.taskId, ...task, icon: true, children: task?.outputs ? transform(task.outputs, true, task.taskId) : []};
|
||||||
});
|
});
|
||||||
|
|
||||||
const HEADING = {label: t("tasks"), heading: true, component: shallowRef(TimelineTextOutline)};
|
const HEADING = {label: t("tasks"), heading: true, component: shallowRef(TimelineTextOutline)};
|
||||||
|
|||||||
@@ -36,9 +36,23 @@
|
|||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
exampleFileName: "kestra.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
curlCommand() {
|
curlCommand() {
|
||||||
return this.generateCurlCommand();
|
const mainCommand = this.generateCurlCommand();
|
||||||
|
|
||||||
|
if (this.flow.inputs && this.flow.inputs.find((input) => input.type === "FILE")) {
|
||||||
|
return `${this.toShell(this.generatePrefix())} && \\\n${this.toShell(mainCommand)}`;
|
||||||
|
} else {
|
||||||
|
return `${this.toShell(mainCommand)}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exampleFileInputUrl() {
|
||||||
|
return `https://huggingface.co/datasets/kestra/datasets/resolve/main/json/${this.exampleFileName}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -55,16 +69,25 @@
|
|||||||
|
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case "FILE": {
|
case "FILE": {
|
||||||
const fileInput = this.inputs[input.id];
|
inputValue = this.exampleFileName;
|
||||||
if (fileInput) {
|
|
||||||
inputValue = fileInput.name;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "SECRET": {
|
case "SECRET": {
|
||||||
inputValue = this.inputs[input.id] ? "******" : undefined;
|
inputValue = this.inputs[input.id] ? "******" : undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "DURATION": {
|
||||||
|
inputValue = this.$moment.duration(this.$moment(this.inputs[input.id]).format("hh:mm:ss")).toJSON();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "DATE": {
|
||||||
|
inputValue = this.$moment(this.inputs[input.id]).format("YYYY-MM-DD");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "TIME": {
|
||||||
|
inputValue = this.$moment(this.inputs[input.id]).format("hh:mm:ss");
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
inputValue = this.inputs[input.id];
|
inputValue = this.inputs[input.id];
|
||||||
}
|
}
|
||||||
@@ -115,8 +138,22 @@
|
|||||||
|
|
||||||
command.push(`'${this.generateUrl()}'`);
|
command.push(`'${this.generateUrl()}'`);
|
||||||
|
|
||||||
|
return command
|
||||||
|
},
|
||||||
|
generatePrefix() {
|
||||||
|
return ["curl", "-O", `'${this.exampleFileInputUrl}'`];
|
||||||
|
},
|
||||||
|
toShell(command) {
|
||||||
return command.join(" ");
|
return command.join(" ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* Allow line-wraps */
|
||||||
|
code {
|
||||||
|
display: block;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
async setupFlow() {
|
async setupFlow() {
|
||||||
if (this.$route.query.copy && this.flow){
|
if (this.$route.query.copy && this.flow){
|
||||||
this.source = this.flow.source;
|
this.source = this.flow.source;
|
||||||
} else if (this.$route.query.blueprintId) {
|
} else if (this.$route.query.blueprintId && this.$route.query.blueprintSource) {
|
||||||
this.source = await this.queryBlueprint(this.$route.query.blueprintId)
|
this.source = await this.queryBlueprint(this.$route.query.blueprintId)
|
||||||
} else {
|
} else {
|
||||||
this.source = `id: myflow
|
this.source = `id: myflow
|
||||||
@@ -86,7 +86,7 @@ tasks:
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
blueprintUri() {
|
blueprintUri() {
|
||||||
return `${apiUrl(this.$store)}/blueprints/community`
|
return `${apiUrl(this.$store)}/blueprints/${this.$route.query.blueprintSource}`
|
||||||
},
|
},
|
||||||
flowParsed() {
|
flowParsed() {
|
||||||
return YamlUtils.parse(this.source);
|
return YamlUtils.parse(this.source);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
:icon="ContentSave"
|
:icon="ContentSave"
|
||||||
@click="saveTask"
|
@click="saveTask"
|
||||||
v-if="canSave && !readOnly"
|
v-if="canSave && !readOnly"
|
||||||
:disabled="errors"
|
:disabled="errors && !!errors.length"
|
||||||
type="primary"
|
type="primary"
|
||||||
>
|
>
|
||||||
{{ $t("save task") }}
|
{{ $t("save task") }}
|
||||||
@@ -160,8 +160,7 @@
|
|||||||
if (this.task) {
|
if (this.task) {
|
||||||
this.taskYaml = YamlUtils.stringify(this.task);
|
this.taskYaml = YamlUtils.stringify(this.task);
|
||||||
if (this.task.type) {
|
if (this.task.type) {
|
||||||
this.$store
|
this.$store.dispatch("plugin/load", {cls: this.task.type})
|
||||||
.dispatch("plugin/load", {cls: this.task.type})
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.taskYaml = "";
|
this.taskYaml = "";
|
||||||
@@ -173,8 +172,7 @@
|
|||||||
handler() {
|
handler() {
|
||||||
const task = YamlUtils.parse(this.taskYaml);
|
const task = YamlUtils.parse(this.taskYaml);
|
||||||
if (task?.type && task.type !== this.type) {
|
if (task?.type && task.type !== this.type) {
|
||||||
this.$store
|
this.$store.dispatch("plugin/load", {cls: task.type})
|
||||||
.dispatch("plugin/load", {cls: task.type})
|
|
||||||
this.type = task.type
|
this.type = task.type
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
type: this.selectedTaskType
|
type: this.selectedTaskType
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.section !== SECTIONS.TRIGGERS) {
|
if (this.section !== SECTIONS.TRIGGERS && this.section !== SECTIONS.TASK_RUNNERS) {
|
||||||
value["id"] = this.taskObject && this.taskObject.id ? this.taskObject.id : "";
|
value["id"] = this.taskObject && this.taskObject.id ? this.taskObject.id : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,17 @@
|
|||||||
<el-col :md="24" :lg="embed ? 24 : 18">
|
<el-col :md="24" :lg="embed ? 24 : 18">
|
||||||
<h4>{{ $t("source") }}</h4>
|
<h4>{{ $t("source") }}</h4>
|
||||||
<el-card>
|
<el-card>
|
||||||
<editor class="position-relative" :read-only="true" :input="true" :full-height="false" :minimap="false" :model-value="blueprint.flow" lang="yaml">
|
<editor
|
||||||
<template #nav>
|
class="position-relative"
|
||||||
|
:read-only="true"
|
||||||
|
:input="true"
|
||||||
|
:full-height="false"
|
||||||
|
:minimap="false"
|
||||||
|
:model-value="blueprint.flow"
|
||||||
|
lang="yaml"
|
||||||
|
:navbar="false"
|
||||||
|
>
|
||||||
|
<template #absolute>
|
||||||
<copy-to-clipboard class="position-absolute" :text="blueprint.flow" />
|
<copy-to-clipboard class="position-absolute" :text="blueprint.flow" />
|
||||||
</template>
|
</template>
|
||||||
</editor>
|
</editor>
|
||||||
@@ -91,7 +100,7 @@
|
|||||||
label: this.$t("blueprints.title"),
|
label: this.$t("blueprints.title"),
|
||||||
link: {
|
link: {
|
||||||
name: "blueprints",
|
name: "blueprints",
|
||||||
params: this.$route.params
|
params: this.$route.params.tab ? this.$route.params.tab : {...this.$route.params, tab: this.tab},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -109,6 +118,10 @@
|
|||||||
tab: {
|
tab: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "community"
|
default: "community"
|
||||||
|
},
|
||||||
|
blueprintBaseUri: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -119,22 +132,26 @@
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: "blueprints",
|
name: "blueprints",
|
||||||
params: {
|
params: {
|
||||||
tenant: this.$route.params.tenant
|
tenant: this.$route.params.tenant,
|
||||||
|
tab: this.tab
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.blueprint = (await this.$http.get(`${this.blueprintBaseUri}/${this.blueprintId}`)).data
|
const TAB = this.$route.query?.tab ?? (this.embed ? this.tab : (this.$route?.params?.tab ?? "community"));
|
||||||
|
const URL = this.blueprintBaseUri ?? `${apiUrl(this.$store)}/blueprints/` + TAB;
|
||||||
|
|
||||||
|
this.blueprint = (await this.$http.get(`${URL}/${this.blueprintId}`)).data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.blueprintBaseUri.endsWith("community")) {
|
if (this.blueprintBaseUri?.endsWith("community")) {
|
||||||
this.flowGraph = (await this.$http.get(`${this.blueprintBaseUri}/${this.blueprintId}/graph`, {
|
this.flowGraph = (await this.$http.get(`${this.blueprintBaseUri}/${this.blueprintId}/graph`, {
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return status === 200;
|
return status === 200;
|
||||||
}
|
}
|
||||||
})).data;
|
}))?.data;
|
||||||
} else {
|
} else {
|
||||||
this.flowGraph = await this.$store.dispatch("flow/getGraphFromSourceResponse", {
|
this.flowGraph = await this.$store.dispatch("flow/getGraphFromSourceResponse", {
|
||||||
flow: this.blueprint.flow, config: {
|
flow: this.blueprint.flow, config: {
|
||||||
@@ -159,9 +176,6 @@
|
|||||||
...YamlUtils.parse(this.blueprint.flow),
|
...YamlUtils.parse(this.blueprint.flow),
|
||||||
source: this.blueprint.flow
|
source: this.blueprint.flow
|
||||||
}
|
}
|
||||||
},
|
|
||||||
blueprintBaseUri() {
|
|
||||||
return `${apiUrl(this.$store)}/blueprints/` + (this.embed ? this.tab : (this.$route?.params?.tab ?? "community"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -257,4 +271,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export default {
|
|||||||
return "condition"
|
return "condition"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (property.$ref.includes("tasks.runners.TaskRunner")) {
|
||||||
|
return "task-runner"
|
||||||
|
}
|
||||||
|
|
||||||
return "complex";
|
return "complex";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
mixins: [Task],
|
mixins: [Task],
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
created() {
|
created() {
|
||||||
if (!Array.isArray(this.modelValue)) {
|
if (!Array.isArray(this.modelValue) && this.modelValue !== undefined) {
|
||||||
this.$emit("update:modelValue", []);
|
this.$emit("update:modelValue", []);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
values() {
|
values() {
|
||||||
if (this.modelValue === undefined || (Array.isArray(this.modelValue) && this.modelValue.length === 0)) {
|
if (this.modelValue === undefined) {
|
||||||
return this.schema.default || [undefined];
|
return this.schema.default || [undefined];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<task-editor
|
<task-editor
|
||||||
ref="editor"
|
ref="editor"
|
||||||
:model-value="taskYaml"
|
:model-value="taskYaml"
|
||||||
:section="SECTIONS.TASKS"
|
:section="section"
|
||||||
@update:model-value="onInput"
|
@update:model-value="onInput"
|
||||||
/>
|
/>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TextSearch from "vue-material-design-icons/TextSearch.vue";
|
import TextSearch from "vue-material-design-icons/TextSearch.vue";
|
||||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||||
import {SECTIONS} from "../../../utils/constants.js";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -42,11 +41,18 @@
|
|||||||
import YamlUtils from "../../../utils/yamlUtils";
|
import YamlUtils from "../../../utils/yamlUtils";
|
||||||
import TaskEditor from "../TaskEditor.vue"
|
import TaskEditor from "../TaskEditor.vue"
|
||||||
import Drawer from "../../Drawer.vue"
|
import Drawer from "../../Drawer.vue"
|
||||||
|
import {SECTIONS as SECTION} from "../../../utils/constants.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [Task],
|
mixins: [Task],
|
||||||
components: {TaskEditor, Drawer},
|
components: {TaskEditor, Drawer},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
|
props: {
|
||||||
|
section: {
|
||||||
|
type: String,
|
||||||
|
default: SECTION.TASKS
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
|||||||
9
ui/src/components/flows/tasks/TaskTaskRunner.vue
Normal file
9
ui/src/components/flows/tasks/TaskTaskRunner.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<task-task @update:model-value="$emit('update:modelValue', $event)" v-bind="$attrs" :section="SECTION.TASK_RUNNERS" />
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {SECTIONS as SECTION} from "../../../utils/constants.js";
|
||||||
|
import TaskTask from "./TaskTask.vue";
|
||||||
|
|
||||||
|
defineEmits(["update:modelValue"]);
|
||||||
|
</script>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</nav>
|
</nav>
|
||||||
|
<slot name="absolute" />
|
||||||
<div class="editor-container" ref="container" :class="containerClass">
|
<div class="editor-container" ref="container" :class="containerClass">
|
||||||
<div ref="editorContainer" class="editor-wrapper position-relative">
|
<div ref="editorContainer" class="editor-wrapper position-relative">
|
||||||
<monaco-editor
|
<monaco-editor
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
components: {
|
components: {
|
||||||
MonacoEditor,
|
MonacoEditor,
|
||||||
},
|
},
|
||||||
emits: ["save", "execute", "focusout", "tab", "update:modelValue", "cursor"],
|
emits: ["save", "execute", "focusout", "tab", "update:modelValue", "cursor", "confirm"],
|
||||||
editor: undefined,
|
editor: undefined,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -262,6 +262,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.editor.addAction({
|
||||||
|
id: "confirm",
|
||||||
|
label: "Confirm",
|
||||||
|
keybindings: [
|
||||||
|
KeyMod.CtrlCmd | KeyCode.Enter,
|
||||||
|
],
|
||||||
|
contextMenuGroupId: "navigation",
|
||||||
|
contextMenuOrder: 1.5,
|
||||||
|
run: (ed) => {
|
||||||
|
this.$emit("confirm", ed.getValue())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TabFocus is global to all editor so revert the behavior on non inputs
|
// TabFocus is global to all editor so revert the behavior on non inputs
|
||||||
this.editor.onDidFocusEditorText?.(() => {
|
this.editor.onDidFocusEditorText?.(() => {
|
||||||
TabFocus.setTabFocusMode(this.input);
|
TabFocus.setTabFocusMode(this.input);
|
||||||
@@ -316,6 +329,7 @@
|
|||||||
|
|
||||||
if (!this.fullHeight) {
|
if (!this.fullHeight) {
|
||||||
editor.onDidContentSizeChange(e => {
|
editor.onDidContentSizeChange(e => {
|
||||||
|
if(!this.$refs.container) return;
|
||||||
this.$refs.container.style.height = (e.contentHeight + this.customHeight) + "px";
|
this.$refs.container.style.height = (e.contentHeight + this.customHeight) + "px";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,10 +120,10 @@
|
|||||||
|
|
||||||
const isCurrentTabFlow = computed(() => currentTab?.value?.extension === undefined)
|
const isCurrentTabFlow = computed(() => currentTab?.value?.extension === undefined)
|
||||||
|
|
||||||
const flowErrors = computed(() => {
|
const isFlow = () => currentTab?.value?.flow || props.isCreating;
|
||||||
const isFlow = currentTab?.value?.flow;
|
|
||||||
|
|
||||||
if (isFlow) {
|
const flowErrors = computed(() => {
|
||||||
|
if (isFlow()) {
|
||||||
const flowExistsError =
|
const flowExistsError =
|
||||||
props.flowValidation?.outdated && props.isCreating
|
props.flowValidation?.outdated && props.isCreating
|
||||||
? [outdatedMessage.value]
|
? [outdatedMessage.value]
|
||||||
@@ -152,9 +152,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const flowWarnings = computed(() => {
|
const flowWarnings = computed(() => {
|
||||||
const isFlow = currentTab?.value?.flow;
|
if (isFlow()) {
|
||||||
|
|
||||||
if (isFlow) {
|
|
||||||
const outdatedWarning =
|
const outdatedWarning =
|
||||||
props.flowValidation?.outdated && !props.isCreating
|
props.flowValidation?.outdated && !props.isCreating
|
||||||
? [outdatedMessage.value]
|
? [outdatedMessage.value]
|
||||||
@@ -283,9 +281,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const flowHaveTasks = (source) => {
|
const flowHaveTasks = (source) => {
|
||||||
const isFlow = currentTab?.value?.flow || props.isCreating;
|
if (isFlow()) {
|
||||||
|
|
||||||
if (isFlow) {
|
|
||||||
const flow = props.isCreating ? props.flow.source : (source ? source : flowYaml.value);
|
const flow = props.isCreating ? props.flow.source : (source ? source : flowYaml.value);
|
||||||
return flow ? YamlUtils.flowHaveTasks(flow) : false;
|
return flow ? YamlUtils.flowHaveTasks(flow) : false;
|
||||||
} else return false;
|
} else return false;
|
||||||
@@ -441,10 +437,10 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEdit = (event, isFlow = false) => {
|
const onEdit = (event, currentIsFlow = false) => {
|
||||||
flowYaml.value = event;
|
flowYaml.value = event;
|
||||||
|
|
||||||
if (isFlow) {
|
if (currentIsFlow) {
|
||||||
if (
|
if (
|
||||||
flowParsed.value &&
|
flowParsed.value &&
|
||||||
!props.isCreating &&
|
!props.isCreating &&
|
||||||
@@ -467,9 +463,20 @@
|
|||||||
|
|
||||||
haveChange.value = true;
|
haveChange.value = true;
|
||||||
store.dispatch("core/isUnsaved", true);
|
store.dispatch("core/isUnsaved", true);
|
||||||
|
|
||||||
|
if(!props.isCreating){
|
||||||
|
store.commit("editor/changeOpenedTabs", {
|
||||||
|
action: "dirty",
|
||||||
|
...currentTab.value,
|
||||||
|
name: currentTab.value?.name ?? "Flow",
|
||||||
|
path: currentTab.value?.path ?? "Flow.yaml",
|
||||||
|
dirty: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
clearTimeout(timer.value);
|
clearTimeout(timer.value);
|
||||||
|
|
||||||
if(!isFlow) return;
|
if(!currentIsFlow) return;
|
||||||
|
|
||||||
return store
|
return store
|
||||||
.dispatch("flow/validateFlow", {flow: yamlWithNextRevision.value})
|
.dispatch("flow/validateFlow", {flow: yamlWithNextRevision.value})
|
||||||
@@ -591,13 +598,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const editorUpdate = (event) => {
|
const editorUpdate = (event) => {
|
||||||
const isFlow = currentTab?.value?.flow;
|
const currentIsFlow = isFlow();
|
||||||
|
|
||||||
updatedFromEditor.value = true;
|
updatedFromEditor.value = true;
|
||||||
flowYaml.value = event;
|
flowYaml.value = event;
|
||||||
|
|
||||||
clearTimeout(timer.value);
|
clearTimeout(timer.value);
|
||||||
timer.value = setTimeout(() => onEdit(event, isFlow), 500);
|
timer.value = setTimeout(() => onEdit(event, currentIsFlow), 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchViewType = (event, shouldPersist = true) => {
|
const switchViewType = (event, shouldPersist = true) => {
|
||||||
@@ -611,7 +618,7 @@
|
|||||||
) {
|
) {
|
||||||
isHorizontal.value = isHorizontalDefault();
|
isHorizontal.value = isHorizontalDefault();
|
||||||
if (updatedFromEditor.value) {
|
if (updatedFromEditor.value) {
|
||||||
onEdit(flowYaml.value);
|
onEdit(flowYaml.value, true);
|
||||||
updatedFromEditor.value = false;
|
updatedFromEditor.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,9 +718,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFlow = currentTab?.value?.flow || props.isCreating;
|
if (isFlow()) {
|
||||||
|
|
||||||
if (isFlow) {
|
|
||||||
onEdit(flowYaml.value, true).then((validation) => {
|
onEdit(flowYaml.value, true).then((validation) => {
|
||||||
if (validation.outdated && !props.isCreating) {
|
if (validation.outdated && !props.isCreating) {
|
||||||
confirmOutdatedSaveDialog.value = true;
|
confirmOutdatedSaveDialog.value = true;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
multiple
|
multiple
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in input.options"
|
v-for="item in (input.values ?? input.options)"
|
||||||
:key="item"
|
:key="item"
|
||||||
:label="item"
|
:label="item"
|
||||||
:value="item"
|
:value="item"
|
||||||
|
|||||||
@@ -712,4 +712,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../styles/layout/root-dark.scss";
|
||||||
|
|
||||||
|
.custom-dark-vs-theme .monaco-editor .sticky-widget {
|
||||||
|
background-color: $input-bg;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="copy-wrapper">
|
<div class="copy-wrapper">
|
||||||
<el-tooltip trigger="click" :content="$t('copied')" placement="left" :auto-close="2000" effect="light">
|
<el-tooltip trigger="click" :content="$t('copied')" placement="left" :auto-close="2000" effect="light">
|
||||||
<el-button text round :icon="ContentCopy" @click="Utils.copy(text)" />
|
<el-button text round :icon="ContentCopy" @click="Utils.copy(text)">
|
||||||
|
<span v-if="label">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,6 +22,11 @@
|
|||||||
text: {
|
text: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState("api", ["version"]),
|
...mapState("api", ["version"]),
|
||||||
|
...mapState("core", ["tutorialFlows"]),
|
||||||
...mapGetters("core", ["guidedProperties"]),
|
...mapGetters("core", ["guidedProperties"]),
|
||||||
...mapGetters("auth", ["user"]),
|
...mapGetters("auth", ["user"]),
|
||||||
displayNavBar() {
|
displayNavBar() {
|
||||||
@@ -131,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
tourEnabled(){
|
tourEnabled(){
|
||||||
// Temporary solution to not showing the tour menu item for EE
|
// Temporary solution to not showing the tour menu item for EE
|
||||||
return !Object.keys(this.user).length
|
return this.tutorialFlows?.length && !Object.keys(this.user).length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="line font-monospace" v-if="filtered">
|
<div class="py-2 line font-monospace" v-if="filtered">
|
||||||
<span :class="levelClass" class="header-badge log-level el-tag noselect fw-bold">{{ log.level }}</span>
|
<span :class="levelClass" class="header-badge log-level el-tag noselect fw-bold">{{ log.level }}</span>
|
||||||
<div class="log-content d-inline-block">
|
<div class="log-content d-inline-block">
|
||||||
<span v-if="title" class="fw-bold">{{ (log.taskId ?? log.flowId ?? "").capitalize() }}</span>
|
<span v-if="title" class="fw-bold">{{ (log.taskId ?? log.flowId ?? "").capitalize() }}</span>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-button v-if="!isLogsListing && logs !== undefined && logs.length > 0" @click="deleteLogs()" class="mb-3 delete-logs-btn">
|
<el-button v-if="shouldDisplayDeleteButton && logs !== undefined && logs.length > 0" @click="deleteLogs()" class="mb-3 delete-logs-btn">
|
||||||
<TrashCan class="me-2" />
|
<TrashCan class="me-2" />
|
||||||
<span>{{ $t("delete logs") }}</span>
|
<span>{{ $t("delete logs") }}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -153,8 +153,8 @@
|
|||||||
isFlowEdit() {
|
isFlowEdit() {
|
||||||
return this.$route.name === "flows/update"
|
return this.$route.name === "flows/update"
|
||||||
},
|
},
|
||||||
isLogsListing() {
|
shouldDisplayDeleteButton() {
|
||||||
return this.$route.name === "logs/list"
|
return this.$route.name === "flows/update"
|
||||||
},
|
},
|
||||||
isNamespaceEdit() {
|
isNamespaceEdit() {
|
||||||
return this.$route.name === "namespaces/update"
|
return this.$route.name === "namespaces/update"
|
||||||
@@ -279,6 +279,7 @@
|
|||||||
border-radius: var(--bs-border-radius-lg);
|
border-radius: var(--bs-border-radius-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: $spacer;
|
padding: $spacer;
|
||||||
|
padding-top: calc($spacer/2);
|
||||||
background-color: var(--bs-white);
|
background-color: var(--bs-white);
|
||||||
border: 1px solid var(--bs-border-color);
|
border: 1px solid var(--bs-border-color);
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,32 @@
|
|||||||
query: {
|
query: {
|
||||||
id: this.$route.query.id
|
id: this.$route.query.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "dependencies",
|
||||||
|
component: NamespaceDependenciesWrapper,
|
||||||
|
title: this.$t("dependencies"),
|
||||||
|
props: {
|
||||||
|
type: "dependencies",
|
||||||
|
tab: "dependencies",
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
id: this.$route.query.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kv",
|
||||||
|
component: NamespaceKV,
|
||||||
|
title: this.$t("kv.name"),
|
||||||
|
props: {
|
||||||
|
addKvModalVisible: this.modalAddKvVisible,
|
||||||
|
},
|
||||||
|
"v-on": {
|
||||||
|
"update:addKvModalVisible": (value) => {
|
||||||
|
this.modalAddKvVisible = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "edit",
|
name: "edit",
|
||||||
component: "",
|
component: "",
|
||||||
@@ -152,19 +177,7 @@
|
|||||||
},
|
},
|
||||||
disabled: true,
|
disabled: true,
|
||||||
locked: true
|
locked: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "dependencies",
|
|
||||||
component: NamespaceDependenciesWrapper,
|
|
||||||
title: this.$t("dependencies"),
|
|
||||||
props: {
|
|
||||||
type: "dependencies",
|
|
||||||
tab: "dependencies",
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
id: this.$route.query.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "secrets",
|
name: "secrets",
|
||||||
component: "",
|
component: "",
|
||||||
@@ -193,20 +206,7 @@
|
|||||||
},
|
},
|
||||||
disabled: true,
|
disabled: true,
|
||||||
locked: true
|
locked: true
|
||||||
},
|
}
|
||||||
{
|
|
||||||
name: "kv",
|
|
||||||
component: NamespaceKV,
|
|
||||||
title: this.$t("kv.name"),
|
|
||||||
props: {
|
|
||||||
addKvModalVisible: this.modalAddKvVisible,
|
|
||||||
},
|
|
||||||
"v-on": {
|
|
||||||
"update:addKvModalVisible": (value) => {
|
|
||||||
this.modalAddKvVisible = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="currentStep(tour).title"
|
v-if="currentStep(tour).title"
|
||||||
class="title"
|
class="title"
|
||||||
:class="{dark: currentStep(tour).keepDark}"
|
:class="{dark: currentStep(tour).keepDark, empty: !flows.length}"
|
||||||
>
|
>
|
||||||
<div v-if="currentStep(tour).icon">
|
<div v-if="currentStep(tour).icon">
|
||||||
<img
|
<img
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
:class="{jump: currentStep(tour).jump}"
|
:class="{jump: currentStep(tour).jump}"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<span v-html="currentStep(tour).title" />
|
<span v-html="tour.currentStep === 1 && !flows.length ? t('onboarding.no_flows') : currentStep(tour).title" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
@@ -68,10 +68,6 @@
|
|||||||
:icons="icons"
|
:icons="icons"
|
||||||
:variable="ICON_COLOR"
|
:variable="ICON_COLOR"
|
||||||
only-icon
|
only-icon
|
||||||
|
|
||||||
:data-cls="task"
|
|
||||||
:data-task-index="taskIndex"
|
|
||||||
:data-key="`flow__${flowIndex}__icon__${taskIndex}`"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,6 +95,7 @@
|
|||||||
![0, 1].includes(tour.currentStep) ||
|
![0, 1].includes(tour.currentStep) ||
|
||||||
!tour.isLast
|
!tour.isLast
|
||||||
"
|
"
|
||||||
|
:disabled="tour.currentStep === 1 && !flows.length"
|
||||||
@click="
|
@click="
|
||||||
tour.isLast
|
tour.isLast
|
||||||
? finishTour(tour.currentStep)
|
? finishTour(tour.currentStep)
|
||||||
@@ -147,7 +144,6 @@
|
|||||||
|
|
||||||
import Finish from "./components/buttons/Finish.vue";
|
import Finish from "./components/buttons/Finish.vue";
|
||||||
|
|
||||||
import {apiUrl} from "override/utils/route";
|
|
||||||
import {pageFromRoute} from "../../utils/eventsRouter";
|
import {pageFromRoute} from "../../utils/eventsRouter";
|
||||||
|
|
||||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||||
@@ -208,7 +204,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const activeFlow = ref(0);
|
const activeFlow = ref(0);
|
||||||
const flows = ref([]);
|
const flows = computed(() => store.state.core.tutorialFlows);
|
||||||
|
|
||||||
const allTasks = (tasks) => {
|
const allTasks = (tasks) => {
|
||||||
const uniqueTypes = new Set();
|
const uniqueTypes = new Set();
|
||||||
@@ -300,7 +296,7 @@
|
|||||||
name: "flows/update",
|
name: "flows/update",
|
||||||
params: {
|
params: {
|
||||||
namespace: "tutorial",
|
namespace: "tutorial",
|
||||||
id: flows.value[activeFlow.value].id,
|
id: flows.value[activeFlow.value]?.id,
|
||||||
tab: "editor",
|
tab: "editor",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -312,7 +308,7 @@
|
|||||||
store.commit("editor/updateOnboarding"),
|
store.commit("editor/updateOnboarding"),
|
||||||
store.commit("core/setGuidedProperties", {
|
store.commit("core/setGuidedProperties", {
|
||||||
tourStarted: true,
|
tourStarted: true,
|
||||||
template: flows.value[activeFlow.value].id,
|
template: flows.value[activeFlow.value]?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
wait(1);
|
wait(1);
|
||||||
@@ -422,11 +418,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const HTTP = getCurrentInstance()?.appContext.config.globalProperties.$http;
|
store.dispatch("core/readTutorialFlows");
|
||||||
|
|
||||||
HTTP.get(`${apiUrl(this)}/flows/tutorial`).then(
|
|
||||||
(response) => (flows.value = response.data),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -530,6 +522,11 @@ $flow-image-size-container: 36px;
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: $color;
|
color: $color;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
& div {
|
& div {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="index" effect="light">
|
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="index" effect="light">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="tasks-tooltips">
|
<div class="tasks-tooltips">
|
||||||
<p v-if="plugin?.tasks.filter(t => t.toLowerCase().includes(searchInput)).length > 0">
|
<p v-if="plugin?.tasks.filter(t => t.toLowerCase().includes(searchInput)).length > 0" class="mb-0">
|
||||||
Tasks
|
Tasks
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<span @click="openPlugin(task)">{{ task }}</span>
|
<span @click="openPlugin(task)">{{ task }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-if="plugin?.triggers.filter(t => t.toLowerCase().includes(searchInput)).length > 0">
|
<p v-if="plugin?.triggers.filter(t => t.toLowerCase().includes(searchInput)).length > 0" class="mb-0">
|
||||||
Triggers
|
Triggers
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<span @click="openPlugin(trigger)">{{ trigger }}</span>
|
<span @click="openPlugin(trigger)">{{ trigger }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-if="plugin?.conditions.filter(t => t.toLowerCase().includes(searchInput)).length > 0">
|
<p v-if="plugin?.conditions.filter(t => t.toLowerCase().includes(searchInput)).length > 0" class="mb-0">
|
||||||
Conditions
|
Conditions
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<span @click="openPlugin(condition)">{{ condition }}</span>
|
<span @click="openPlugin(condition)">{{ condition }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-if="plugin?.taskRunners.filter(t => t.toLowerCase().includes(searchInput)).length > 0">
|
<p v-if="plugin?.taskRunners.filter(t => t.toLowerCase().includes(searchInput)).length > 0" class="mb-0">
|
||||||
Task
|
Task
|
||||||
Runners
|
Runners
|
||||||
</p>
|
</p>
|
||||||
@@ -140,8 +140,8 @@
|
|||||||
plugin.conditions.some(condition => condition.toLowerCase().includes(this.searchInput.toLowerCase())) ||
|
plugin.conditions.some(condition => condition.toLowerCase().includes(this.searchInput.toLowerCase())) ||
|
||||||
plugin.taskRunners.some(taskRunner => taskRunner.toLowerCase().includes(this.searchInput.toLowerCase()))
|
plugin.taskRunners.some(taskRunner => taskRunner.toLowerCase().includes(this.searchInput.toLowerCase()))
|
||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
const nameA = a.group.toLowerCase(),
|
const nameA = a.manifest["X-Kestra-Title"].toLowerCase(),
|
||||||
nameB = b.group.toLowerCase();
|
nameB = b.manifest["X-Kestra-Title"].toLowerCase();
|
||||||
|
|
||||||
return (nameA < nameB ? -1 : (nameA > nameB ? 1 : 0));
|
return (nameA < nameB ? -1 : (nameA > nameB ? 1 : 0));
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -55,12 +55,17 @@
|
|||||||
taskModels() {
|
taskModels() {
|
||||||
const taskModels = [];
|
const taskModels = [];
|
||||||
for (const plugin of this.plugins || []) {
|
for (const plugin of this.plugins || []) {
|
||||||
taskModels.push.apply(taskModels, plugin[this.section.toLowerCase()]);
|
taskModels.push.apply(taskModels, plugin[this.upperSnakeToCamelCase(this.section)]);
|
||||||
}
|
}
|
||||||
return taskModels;
|
return taskModels;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
upperSnakeToCamelCase(str) {
|
||||||
|
return str.toLowerCase().replaceAll(/_([a-z])/g, function (g) {
|
||||||
|
return g[1].toUpperCase();
|
||||||
|
});
|
||||||
|
},
|
||||||
onInput(value) {
|
onInput(value) {
|
||||||
this.$emit("update:modelValue", value);
|
this.$emit("update:modelValue", value);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<top-nav-bar v-if="!embed" :title="routeInfo.title" />
|
<top-nav-bar v-if="!embed" :title="routeInfo.title" />
|
||||||
<blueprints-page-header v-if="!embed" class="ms-0 mw-100" />
|
<blueprints-page-header v-if="!embed" class="ms-0 mw-100" />
|
||||||
<section :class="{'container': !embed}" class="main-container" v-bind="$attrs">
|
<section :class="{'container': !embed}" class="main-container" v-bind="$attrs">
|
||||||
<blueprint-detail v-if="selectedBlueprintId" :embed="embed" :blueprint-id="selectedBlueprintId" @back="selectedBlueprintId = undefined" />
|
<blueprint-detail v-if="selectedBlueprintId" :embed="embed" :blueprint-id="selectedBlueprintId" @back="selectedBlueprintId = undefined" :blueprint-base-uri="blueprintUri" />
|
||||||
<blueprints-browser @loaded="$emit('loaded', $event)" :class="{'d-none': !!selectedBlueprintId}" :embed="embed" :blueprint-base-uri="blueprintUri" @go-to-detail="blueprintId => selectedBlueprintId = blueprintId" />
|
<blueprints-browser @loaded="$emit('loaded', $event)" :class="{'d-none': !!selectedBlueprintId}" :embed="embed" :blueprint-base-uri="blueprintUri" @go-to-detail="blueprintId => selectedBlueprintId = blueprintId" />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<component
|
<component
|
||||||
class="blueprint-link"
|
class="blueprint-link"
|
||||||
:is="embed ? 'div' : 'router-link'"
|
:is="embed ? 'div' : 'router-link'"
|
||||||
:to="embed ? undefined : {name: 'blueprints/view', params: {blueprintId: blueprint.id}}"
|
:to="embed ? undefined : {name: 'blueprints/view', params: {blueprintId: blueprint.id}, query: {tab}}"
|
||||||
>
|
>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<div>
|
<div>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
{{ $t('copy') }}
|
{{ $t('copy') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-button v-else-if="userCanCreateFlow" size="large" text bg @click="blueprintToEditor(blueprint.id)">
|
<el-button v-else-if="userCanCreateFlow" size="large" text bg @click.prevent.stop="blueprintToEditor(blueprint.id)">
|
||||||
{{ $t('use') }}
|
{{ $t('use') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,6 +108,10 @@
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
tab: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
embed: {
|
embed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -146,7 +150,7 @@
|
|||||||
params: {
|
params: {
|
||||||
tenant: this.$route.params.tenant
|
tenant: this.$route.params.tenant
|
||||||
},
|
},
|
||||||
query: {blueprintId: blueprintId}
|
query: {blueprintId: blueprintId, blueprintSource: this.blueprintBaseUri.includes("community") ? "community" : "custom"}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tagsToString(blueprintTags) {
|
tagsToString(blueprintTags) {
|
||||||
@@ -224,7 +228,7 @@
|
|||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
// Handle switch tab while fetching data
|
// Handle switch tab while fetching data
|
||||||
if (this.blueprintBaseUri === beforeLoadBlueprintBaseUri) {
|
if (this.blueprintBaseUri === beforeLoadBlueprintBaseUri && callback) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -273,9 +277,6 @@
|
|||||||
this.load(this.onDataLoaded);
|
this.load(this.onDataLoaded);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
blueprintBaseUri() {
|
|
||||||
this.hardReload();
|
|
||||||
},
|
|
||||||
tags() {
|
tags() {
|
||||||
if(!Object.prototype.hasOwnProperty.call(this.tags, this.selectedTag)) {
|
if(!Object.prototype.hasOwnProperty.call(this.tags, this.selectedTag)) {
|
||||||
this.selectedTag = 0;
|
this.selectedTag = 0;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {apiUrl} from "override/utils/route";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@@ -10,7 +12,8 @@ export default {
|
|||||||
template: undefined,
|
template: undefined,
|
||||||
},
|
},
|
||||||
monacoYamlConfigured: false,
|
monacoYamlConfigured: false,
|
||||||
autocompletionSource: undefined
|
autocompletionSource: undefined,
|
||||||
|
tutorialFlows: []
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
showMessage({commit}, message) {
|
showMessage({commit}, message) {
|
||||||
@@ -21,7 +24,10 @@ export default {
|
|||||||
},
|
},
|
||||||
isUnsaved({commit}, unsavedChange) {
|
isUnsaved({commit}, unsavedChange) {
|
||||||
commit("setUnsavedChange", unsavedChange)
|
commit("setUnsavedChange", unsavedChange)
|
||||||
}
|
},
|
||||||
|
readTutorialFlows({commit}) {
|
||||||
|
return this.$http.get(`${apiUrl(this)}/flows/tutorial`).then((response) => commit("setTutorialFlows", response.data))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setMessage(state, message) {
|
setMessage(state, message) {
|
||||||
@@ -41,7 +47,10 @@ export default {
|
|||||||
},
|
},
|
||||||
setAutocompletionSource(state, autocompletionSource) {
|
setAutocompletionSource(state, autocompletionSource) {
|
||||||
state.autocompletionSource = autocompletionSource
|
state.autocompletionSource = autocompletionSource
|
||||||
}
|
},
|
||||||
|
setTutorialFlows(state, flows) {
|
||||||
|
state.tutorialFlows = flows
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
unsavedChange(state) {
|
unsavedChange(state) {
|
||||||
|
|||||||
@@ -633,6 +633,7 @@
|
|||||||
"MAIN": "Parent Executions"
|
"MAIN": "Parent Executions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"show chart": "Show Chart",
|
||||||
"namespace files": {
|
"namespace files": {
|
||||||
"toggle": {
|
"toggle": {
|
||||||
"show": "Show namespace files",
|
"show": "Show namespace files",
|
||||||
@@ -790,6 +791,7 @@
|
|||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"skip": "Skip Tutorial",
|
"skip": "Skip Tutorial",
|
||||||
|
"no_flows": "No flows available under tutorial namespace.",
|
||||||
"steps": {
|
"steps": {
|
||||||
"0": {
|
"0": {
|
||||||
"title": "Welcome to Kestra!",
|
"title": "Welcome to Kestra!",
|
||||||
|
|||||||
@@ -767,6 +767,7 @@
|
|||||||
"previous": "Précédent",
|
"previous": "Précédent",
|
||||||
"finish": "Finir",
|
"finish": "Finir",
|
||||||
"skip": "Passer le tutoriel",
|
"skip": "Passer le tutoriel",
|
||||||
|
"no_flows": "Aucun flux disponible dans l'espace de noms du tutoriel.",
|
||||||
"steps": {
|
"steps": {
|
||||||
"0": {
|
"0": {
|
||||||
"title": "Bienvenue chez Kestra!",
|
"title": "Bienvenue chez Kestra!",
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function chartClick(moment, router, route, event) {
|
|||||||
const query = {};
|
const query = {};
|
||||||
|
|
||||||
if (event.date) {
|
if (event.date) {
|
||||||
const formattedDate = moment(event.date, "DD/MM/YYYY");
|
const formattedDate = moment(event.date, moment.localeData().longDateFormat("L"));
|
||||||
query.startDate = formattedDate.toISOString(true);
|
query.startDate = formattedDate.toISOString(true);
|
||||||
query.endDate = formattedDate.add(1, "d").toISOString(true);
|
query.endDate = formattedDate.add(1, "d").toISOString(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const SECTIONS = {
|
export const SECTIONS = {
|
||||||
TASKS: "TASKS",
|
TASKS: "TASKS",
|
||||||
TRIGGERS: "TRIGGERS",
|
TRIGGERS: "TRIGGERS",
|
||||||
|
TASK_RUNNERS: "TASK_RUNNERS",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stateGlobalChartTypes = {
|
export const stateGlobalChartTypes = {
|
||||||
@@ -29,6 +30,7 @@ export const storageKeys = {
|
|||||||
DISPLAY_FLOW_EXECUTIONS_COLUMNS: "displayFlowExecutionsColumns",
|
DISPLAY_FLOW_EXECUTIONS_COLUMNS: "displayFlowExecutionsColumns",
|
||||||
SELECTED_TENANT: "selectedTenant",
|
SELECTED_TENANT: "selectedTenant",
|
||||||
EXECUTE_FLOW_BEHAVIOUR: "executeFlowBehaviour",
|
EXECUTE_FLOW_BEHAVIOUR: "executeFlowBehaviour",
|
||||||
|
SHOW_CHART: "showChart",
|
||||||
DEFAULT_NAMESPACE: "defaultNamespace",
|
DEFAULT_NAMESPACE: "defaultNamespace",
|
||||||
LATEST_NAMESPACE: "latestNamespace",
|
LATEST_NAMESPACE: "latestNamespace",
|
||||||
PAGINATION_SIZE: "paginationSize",
|
PAGINATION_SIZE: "paginationSize",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {createRouter, createWebHistory} from "vue-router";
|
|||||||
import VueGtag from "vue-gtag";
|
import VueGtag from "vue-gtag";
|
||||||
import {createI18n} from "vue-i18n";
|
import {createI18n} from "vue-i18n";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import "moment/locale/fr"
|
import "moment/dist/locale/fr"
|
||||||
import {extendMoment} from "moment-range";
|
import {extendMoment} from "moment-range";
|
||||||
import VueSidebarMenu from "vue-sidebar-menu";
|
import VueSidebarMenu from "vue-sidebar-menu";
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +50,7 @@ import TaskSubflowInputs from "../components/flows/tasks/TaskSubflowInputs.vue";
|
|||||||
import LeftMenuLink from "../components/LeftMenuLink.vue";
|
import LeftMenuLink from "../components/LeftMenuLink.vue";
|
||||||
import RouterMd from "../components/utils/RouterMd.vue";
|
import RouterMd from "../components/utils/RouterMd.vue";
|
||||||
import Utils from "./utils";
|
import Utils from "./utils";
|
||||||
|
import TaskTaskRunner from "../components/flows/tasks/TaskTaskRunner.vue";
|
||||||
|
|
||||||
export default (app, routes, stores, translations) => {
|
export default (app, routes, stores, translations) => {
|
||||||
// charts
|
// charts
|
||||||
@@ -151,6 +152,7 @@ export default (app, routes, stores, translations) => {
|
|||||||
app.component("TaskSubflowNamespace", TaskSubflowNamespace)
|
app.component("TaskSubflowNamespace", TaskSubflowNamespace)
|
||||||
app.component("TaskSubflowId", TaskSubflowId)
|
app.component("TaskSubflowId", TaskSubflowId)
|
||||||
app.component("TaskSubflowInputs", TaskSubflowInputs)
|
app.component("TaskSubflowInputs", TaskSubflowInputs)
|
||||||
|
app.component("TaskTaskRunner", TaskTaskRunner)
|
||||||
app.component("LeftMenuLink", LeftMenuLink)
|
app.component("LeftMenuLink", LeftMenuLink)
|
||||||
app.component("RouterMd", RouterMd)
|
app.component("RouterMd", RouterMd)
|
||||||
|
|
||||||
|
|||||||
@@ -3,49 +3,43 @@ import mark from "markdown-it-mark";
|
|||||||
import meta from "markdown-it-meta";
|
import meta from "markdown-it-meta";
|
||||||
import anchor from "markdown-it-anchor";
|
import anchor from "markdown-it-anchor";
|
||||||
import container from "markdown-it-container";
|
import container from "markdown-it-container";
|
||||||
import {fromHighlighter} from "@shikijs/markdown-it/core"
|
import {fromHighlighter} from "@shikijs/markdown-it/core";
|
||||||
import {getHighlighterCore} from "shiki/core"
|
import {createHighlighterCore} from "shiki/core";
|
||||||
import githubDark from "shiki/themes/github-dark.mjs";
|
import githubDark from "shiki/themes/github-dark.mjs";
|
||||||
import githubLight from "shiki/themes/github-light.mjs";
|
import githubLight from "shiki/themes/github-light.mjs";
|
||||||
import {linkTag} from "./markdown_plugins/link";
|
import {linkTag} from "./markdown_plugins/link";
|
||||||
|
|
||||||
|
let highlighter = null;
|
||||||
|
|
||||||
|
async function getHighlighter() {
|
||||||
|
if (!highlighter) {
|
||||||
|
highlighter = createHighlighterCore({
|
||||||
|
langs: [import("shiki/langs/yaml.mjs"), import("shiki/langs/python.mjs"), import("shiki/langs/javascript.mjs")],
|
||||||
|
themes: [githubDark, githubLight],
|
||||||
|
loadWasm: import("shiki/wasm"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return highlighter;
|
||||||
|
}
|
||||||
|
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
static async render(markdown, options) {
|
static async render(markdown, options) {
|
||||||
|
const highlighter = await getHighlighter();
|
||||||
|
|
||||||
githubDark["colors"]["editor.background"] = "var(--bs-gray-500)";
|
githubDark["colors"]["editor.background"] = "var(--bs-gray-500)";
|
||||||
githubLight["colors"]["editor.background"] = "var(--bs-white)";
|
githubLight["colors"]["editor.background"] = "var(--bs-white)";
|
||||||
|
|
||||||
const highlighter = await getHighlighterCore({
|
options = options || {};
|
||||||
themes: [
|
|
||||||
githubDark,
|
|
||||||
githubLight
|
|
||||||
],
|
|
||||||
langs: [
|
|
||||||
import("shiki/langs/yaml.mjs"),
|
|
||||||
import("shiki/langs/python.mjs"),
|
|
||||||
import("shiki/langs/javascript.mjs")
|
|
||||||
],
|
|
||||||
loadWasm: import("shiki/wasm")
|
|
||||||
})
|
|
||||||
|
|
||||||
options = options || {}
|
|
||||||
|
|
||||||
const darkTheme = document.getElementsByTagName("html")[0].className.indexOf("dark") >= 0;
|
const darkTheme = document.getElementsByTagName("html")[0].className.indexOf("dark") >= 0;
|
||||||
|
|
||||||
// noinspection JSPotentiallyInvalidConstructorUsage
|
let md = new markdownIt()
|
||||||
let md = new markdownIt() // jshint ignore:line
|
|
||||||
.use(mark)
|
.use(mark)
|
||||||
.use(meta)
|
.use(meta)
|
||||||
.use(anchor, {
|
.use(anchor, {permalink: options.permalink ? anchor.permalink.ariaHidden({placement: "before"}) : undefined})
|
||||||
permalink: options.permalink ? anchor.permalink.ariaHidden({
|
|
||||||
placement: "before"
|
|
||||||
}) : undefined
|
|
||||||
})
|
|
||||||
// if more alert types are used inside the task documentation, they need to be configured here also
|
|
||||||
.use(container, "warning")
|
.use(container, "warning")
|
||||||
.use(container, "info")
|
.use(container, "info")
|
||||||
.use(fromHighlighter(highlighter, {
|
.use(fromHighlighter(highlighter, {theme: darkTheme ? "github-dark" : "github-light"}))
|
||||||
theme: darkTheme ? "github-dark" : "github-light",
|
|
||||||
}))
|
|
||||||
.use(linkTag);
|
.use(linkTag);
|
||||||
|
|
||||||
md.set({
|
md.set({
|
||||||
@@ -56,12 +50,10 @@ export default class Markdown {
|
|||||||
typographer: true,
|
typographer: true,
|
||||||
langPrefix: "language-",
|
langPrefix: "language-",
|
||||||
quotes: "“”‘’",
|
quotes: "“”‘’",
|
||||||
})
|
});
|
||||||
|
|
||||||
md.renderer.rules.table_open = () => "<table class=\"table\">\n"
|
md.renderer.rules.table_open = () => "<table class=\"table\">\n";
|
||||||
|
|
||||||
return md.render(
|
return md.render(markdown);
|
||||||
markdown
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import {defineConfig} from "vite";
|
|||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import {visualizer} from "rollup-plugin-visualizer";
|
import {visualizer} from "rollup-plugin-visualizer";
|
||||||
import eslintPlugin from "vite-plugin-eslint";
|
import eslintPlugin from "vite-plugin-eslint";
|
||||||
|
import * as sass from "sass"
|
||||||
|
|
||||||
import {filename} from "./plugins/filename"
|
import {filename} from "./plugins/filename"
|
||||||
import {details} from "./plugins/details"
|
import {commit} from "./plugins/commit"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "",
|
base: "",
|
||||||
@@ -22,16 +23,18 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
visualizer(),
|
visualizer(),
|
||||||
eslintPlugin({
|
eslintPlugin({failOnWarning: true, failOnError: true}),
|
||||||
failOnWarning: true,
|
|
||||||
failOnError: true
|
|
||||||
}),
|
|
||||||
filename(),
|
filename(),
|
||||||
details()
|
commit()
|
||||||
],
|
],
|
||||||
assetsInclude: ["**/*.md"],
|
assetsInclude: ["**/*.md"],
|
||||||
css: {
|
css: {
|
||||||
devSourcemap: true
|
devSourcemap: true,
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
logger: sass.Logger.silent
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import io.micronaut.http.HttpStatus;
|
|||||||
import io.micronaut.http.annotation.Controller;
|
import io.micronaut.http.annotation.Controller;
|
||||||
import io.micronaut.http.annotation.Error;
|
import io.micronaut.http.annotation.Error;
|
||||||
import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
||||||
|
import io.micronaut.http.exceptions.HttpStatusException;
|
||||||
import io.micronaut.http.hateoas.JsonError;
|
import io.micronaut.http.hateoas.JsonError;
|
||||||
import io.micronaut.http.hateoas.Link;
|
import io.micronaut.http.hateoas.Link;
|
||||||
import io.micronaut.web.router.exceptions.UnsatisfiedBodyRouteException;
|
import io.micronaut.web.router.exceptions.UnsatisfiedBodyRouteException;
|
||||||
@@ -138,6 +139,11 @@ public class ErrorController {
|
|||||||
return jsonError(request, e, HttpStatus.UNPROCESSABLE_ENTITY, "Invalid route params");
|
return jsonError(request, e, HttpStatus.UNPROCESSABLE_ENTITY, "Invalid route params");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Error(global = true)
|
||||||
|
public HttpResponse<JsonError> error(HttpRequest<?> request, HttpStatusException e) {
|
||||||
|
return jsonError(request, e, e.getStatus(), e.getStatus().getReason());
|
||||||
|
}
|
||||||
|
|
||||||
@Error(global = true)
|
@Error(global = true)
|
||||||
public HttpResponse<JsonError> error(HttpRequest<?> request, Throwable e) {
|
public HttpResponse<JsonError> error(HttpRequest<?> request, Throwable e) {
|
||||||
return jsonError(request, e, HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error");
|
return jsonError(request, e, HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error");
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import io.micronaut.http.sse.Event;
|
|||||||
import io.micronaut.scheduling.TaskExecutors;
|
import io.micronaut.scheduling.TaskExecutors;
|
||||||
import io.micronaut.scheduling.annotation.ExecuteOn;
|
import io.micronaut.scheduling.annotation.ExecuteOn;
|
||||||
import io.micronaut.validation.Validated;
|
import io.micronaut.validation.Validated;
|
||||||
|
import io.netty.util.IllegalReferenceCountException;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
@@ -458,7 +459,7 @@ public class ExecutionController {
|
|||||||
HttpRequest<String> request
|
HttpRequest<String> request
|
||||||
) {
|
) {
|
||||||
if (maybeFlow.isEmpty()) {
|
if (maybeFlow.isEmpty()) {
|
||||||
return HttpResponse.notFound();
|
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Flow not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var flow = maybeFlow.get();
|
var flow = maybeFlow.get();
|
||||||
@@ -489,13 +490,13 @@ public class ExecutionController {
|
|||||||
.findFirst();
|
.findFirst();
|
||||||
|
|
||||||
if (webhook.isEmpty()) {
|
if (webhook.isEmpty()) {
|
||||||
return HttpResponse.notFound();
|
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Webhook not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Execution> execution = webhook.get().evaluate(request, flow);
|
Optional<Execution> execution = webhook.get().evaluate(request, flow);
|
||||||
|
|
||||||
if (execution.isEmpty()) {
|
if (execution.isEmpty()) {
|
||||||
return HttpResponse.notFound();
|
throw new HttpStatusException(HttpStatus.NOT_FOUND, "No execution triggered");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = execution.get();
|
var result = execution.get();
|
||||||
@@ -588,13 +589,21 @@ public class ExecutionController {
|
|||||||
sink.success(item);
|
sink.success(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
sink.onDispose(() -> receive.run());
|
|
||||||
|
sink.onDispose(receive::run);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
sink.error(new RuntimeException(e));
|
sink.error(new RuntimeException(e));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.doOnError(t -> Flux.from(inputs).subscribeOn(Schedulers.boundedElastic()).blockLast()); // need to consume the inputs in case of error;
|
.doOnError(t -> {
|
||||||
|
// need to consume the inputs in case of error, that can failed, but we ignored
|
||||||
|
try {
|
||||||
|
Flux.from(inputs).subscribeOn(Schedulers.boundedElastic()).blockLast();
|
||||||
|
} catch (IllegalReferenceCountException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Label> parseLabels(List<String> labels) {
|
private List<Label> parseLabels(List<String> labels) {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ public class LogController {
|
|||||||
@Parameter(description = "The min log level filter") @Nullable @QueryValue Level minLevel
|
@Parameter(description = "The min log level filter") @Nullable @QueryValue Level minLevel
|
||||||
) {
|
) {
|
||||||
AtomicReference<Runnable> cancel = new AtomicReference<>();
|
AtomicReference<Runnable> cancel = new AtomicReference<>();
|
||||||
List<String> levels = LogEntry.findLevelsByMin(minLevel);
|
List<String> levels = LogEntry.findLevelsByMin(minLevel).stream().map(level -> level.name()).toList();
|
||||||
|
|
||||||
return Flux
|
return Flux
|
||||||
.<Event<LogEntry>>create(emitter -> {
|
.<Event<LogEntry>>create(emitter -> {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class ExecutionControllerTest extends JdbcH2ControllerTest {
|
|||||||
Execution.class
|
Execution.class
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MultipartBody createInputsFlowBody() {
|
private MultipartBody createInputsFlowBody() {
|
||||||
// Trigger execution
|
// Trigger execution
|
||||||
File applicationFile = new File(Objects.requireNonNull(
|
File applicationFile = new File(Objects.requireNonNull(
|
||||||
@@ -188,6 +189,25 @@ class ExecutionControllerTest extends JdbcH2ControllerTest {
|
|||||||
assertThat(notFound.getStatus(), is(HttpStatus.NOT_FOUND));
|
assertThat(notFound.getStatus(), is(HttpStatus.NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidInputs() {
|
||||||
|
MultipartBody.Builder builder = MultipartBody.builder()
|
||||||
|
.addPart("validatedString", "B-failed");
|
||||||
|
inputs.forEach((s, o) -> builder.addPart(s, o instanceof String ? (String) o : null));
|
||||||
|
|
||||||
|
HttpClientResponseException e = assertThrows(
|
||||||
|
HttpClientResponseException.class,
|
||||||
|
() -> triggerExecution(TESTS_FLOW_NS, "inputs", builder.build(), false)
|
||||||
|
);
|
||||||
|
|
||||||
|
String response = e.getResponse().getBody(String.class).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(response, containsString("Invalid entity"));
|
||||||
|
assertThat(response, containsString("Invalid input for `validatedString`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void triggerAndWait() {
|
void triggerAndWait() {
|
||||||
Execution result = triggerInputsFlowExecution(true);
|
Execution result = triggerInputsFlowExecution(true);
|
||||||
@@ -697,6 +717,70 @@ class ExecutionControllerTest extends JdbcH2ControllerTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void webhookFlowNotFound() {
|
||||||
|
HttpClientResponseException exception = assertThrows(HttpClientResponseException.class,
|
||||||
|
() -> client.toBlocking().retrieve(
|
||||||
|
HttpRequest
|
||||||
|
.POST(
|
||||||
|
"/api/v1/executions/webhook/not-found/webhook/not-found?name=john&age=12&age=13",
|
||||||
|
ImmutableMap.of("a", 1, "b", true)
|
||||||
|
),
|
||||||
|
Execution.class
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertThat(exception.getStatus(), is(HttpStatus.NOT_FOUND));
|
||||||
|
assertThat(exception.getMessage(), containsString("Not Found: Flow not found"));
|
||||||
|
|
||||||
|
exception = assertThrows(HttpClientResponseException.class,
|
||||||
|
() -> client.toBlocking().retrieve(
|
||||||
|
HttpRequest
|
||||||
|
.PUT(
|
||||||
|
"/api/v1/executions/webhook/not-found/webhook/not-found?name=john&age=12&age=13",
|
||||||
|
Collections.singletonList(ImmutableMap.of("a", 1, "b", true))
|
||||||
|
),
|
||||||
|
Execution.class
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertThat(exception.getStatus(), is(HttpStatus.NOT_FOUND));
|
||||||
|
assertThat(exception.getMessage(), containsString("Not Found: Flow not found"));
|
||||||
|
|
||||||
|
exception = assertThrows(HttpClientResponseException.class,
|
||||||
|
() -> client.toBlocking().retrieve(
|
||||||
|
HttpRequest
|
||||||
|
.POST(
|
||||||
|
"/api/v1/executions/webhook/not-found/webhook/not-found?name=john&age=12&age=13",
|
||||||
|
"bla"
|
||||||
|
),
|
||||||
|
Execution.class
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertThat(exception.getStatus(), is(HttpStatus.NOT_FOUND));
|
||||||
|
assertThat(exception.getMessage(), containsString("Not Found: Flow not found"));
|
||||||
|
|
||||||
|
exception = assertThrows(HttpClientResponseException.class,
|
||||||
|
() -> client.toBlocking().retrieve(
|
||||||
|
GET("/api/v1/executions/webhook/not-found/webhook/not-found?name=john&age=12&age=13"),
|
||||||
|
Execution.class
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertThat(exception.getStatus(), is(HttpStatus.NOT_FOUND));
|
||||||
|
assertThat(exception.getMessage(), containsString("Not Found: Flow not found"));
|
||||||
|
|
||||||
|
exception = assertThrows(HttpClientResponseException.class,
|
||||||
|
() -> client.toBlocking().retrieve(
|
||||||
|
HttpRequest
|
||||||
|
.POST(
|
||||||
|
"/api/v1/executions/webhook/not-found/webhook/not-found?name=john&age=12&age=13",
|
||||||
|
"{\\\"a\\\":\\\"\\\",\\\"b\\\":{\\\"c\\\":{\\\"d\\\":{\\\"e\\\":\\\"\\\",\\\"f\\\":\\\"1\\\"}}}}"
|
||||||
|
),
|
||||||
|
Execution.class
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertThat(exception.getStatus(), is(HttpStatus.NOT_FOUND));
|
||||||
|
assertThat(exception.getMessage(), containsString("Not Found: Flow not found"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void webhookDynamicKey() {
|
void webhookDynamicKey() {
|
||||||
Execution execution = client.toBlocking().retrieve(
|
Execution execution = client.toBlocking().retrieve(
|
||||||
|
|||||||
Reference in New Issue
Block a user