mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 14:00:23 -05:00
Compare commits
81 Commits
dependabot
...
v0.23.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90a576490f | ||
|
|
2cdd968100 | ||
|
|
adfc3bf526 | ||
|
|
3a61f9b1ba | ||
|
|
64e3014426 | ||
|
|
1f68e5f4ed | ||
|
|
9bfa888e36 | ||
|
|
691a77538a | ||
|
|
b07086f553 | ||
|
|
ee12c884e9 | ||
|
|
712d6da84f | ||
|
|
fcc5fa2056 | ||
|
|
dace30ded7 | ||
|
|
2b578f0f94 | ||
|
|
91f958b26b | ||
|
|
d7fc6894fe | ||
|
|
c286348d27 | ||
|
|
de4ec49721 | ||
|
|
1966ac6012 | ||
|
|
a293a37ec9 | ||
|
|
f295724bb6 | ||
|
|
06505ad977 | ||
|
|
cb31ef642f | ||
|
|
c320323371 | ||
|
|
a190cdd0e7 | ||
|
|
0678f7c5e9 | ||
|
|
f39ba5c95e | ||
|
|
b4e334c5d8 | ||
|
|
561380c942 | ||
|
|
68b4867b5a | ||
|
|
cb7f99d107 | ||
|
|
efac7146ff | ||
|
|
11de42c0b8 | ||
|
|
b58d9e10dd | ||
|
|
e25e70d37e | ||
|
|
f2dac28997 | ||
|
|
0ac8819d95 | ||
|
|
d261de0df3 | ||
|
|
02cac65614 | ||
|
|
5064687b7e | ||
|
|
7c8419b266 | ||
|
|
84e4c62c6d | ||
|
|
9aa605e23b | ||
|
|
faa77aed79 | ||
|
|
fdce552528 | ||
|
|
a028a61792 | ||
|
|
023a77a320 | ||
|
|
bfee04bca2 | ||
|
|
3756f01bdf | ||
|
|
c1240d7391 | ||
|
|
ac37ae6032 | ||
|
|
9e51b100b0 | ||
|
|
bc81e01608 | ||
|
|
9f2162c942 | ||
|
|
97992d99ee | ||
|
|
f90f6b8429 | ||
|
|
0f7360ae81 | ||
|
|
938590f31f | ||
|
|
b2d1c84a86 | ||
|
|
d7ca302830 | ||
|
|
8656e852cc | ||
|
|
cc72336350 | ||
|
|
316d89764e | ||
|
|
4873bf4d36 | ||
|
|
204bf7f5e1 | ||
|
|
1e0950fdf8 | ||
|
|
4cddc704f4 | ||
|
|
f2f0e29f93 | ||
|
|
95011e022e | ||
|
|
65503b708a | ||
|
|
876b8cb2e6 | ||
|
|
f3b7592dfa | ||
|
|
4dbeaf86bb | ||
|
|
f98e78399d | ||
|
|
71dac0f311 | ||
|
|
3077d0ac7a | ||
|
|
9504bbaffe | ||
|
|
159c9373ad | ||
|
|
55b9088b55 | ||
|
|
601d1a0abb | ||
|
|
4a1cf98f26 |
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@@ -43,6 +43,9 @@ jobs:
|
|||||||
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
||||||
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
||||||
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
|
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
|
||||||
|
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||||
|
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||||
|
|
||||||
|
|
||||||
end:
|
end:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
6
.github/workflows/setversion-tag.yml
vendored
6
.github/workflows/setversion-tag.yml
vendored
@@ -22,11 +22,11 @@ jobs:
|
|||||||
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)(\.[0-9]+)-(rc[0-9])?(-SNAPSHOT)?$"
|
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)(\.[0-9]+)-(rc[0-9])?(-SNAPSHOT)?$"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract the major and minor versions
|
# Extract the major and minor versions
|
||||||
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
|
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
|
||||||
RELEASE_BRANCH="refs/heads/releases/v${BASE_VERSION}.x"
|
RELEASE_BRANCH="refs/heads/releases/v${BASE_VERSION}.x"
|
||||||
|
|
||||||
CURRENT_BRANCH="$GITHUB_REF"
|
CURRENT_BRANCH="$GITHUB_REF"
|
||||||
if ! [[ "$CURRENT_BRANCH" == "$RELEASE_BRANCH" ]]; then
|
if ! [[ "$CURRENT_BRANCH" == "$RELEASE_BRANCH" ]]; then
|
||||||
echo "Invalid release branch. Expected $RELEASE_BRANCH, was $CURRENT_BRANCH"
|
echo "Invalid release branch. Expected $RELEASE_BRANCH, was $CURRENT_BRANCH"
|
||||||
@@ -54,4 +54,4 @@ jobs:
|
|||||||
git commit -m"chore(version): update to version '$RELEASE_VERSION'"
|
git commit -m"chore(version): update to version '$RELEASE_VERSION'"
|
||||||
git push
|
git push
|
||||||
git tag -a "v$RELEASE_VERSION" -m"v$RELEASE_VERSION"
|
git tag -a "v$RELEASE_VERSION" -m"v$RELEASE_VERSION"
|
||||||
git push origin "v$RELEASE_VERSION"
|
git push --tags
|
||||||
38
.github/workflows/workflow-github-release.yml
vendored
38
.github/workflows/workflow-github-release.yml
vendored
@@ -6,23 +6,15 @@ on:
|
|||||||
GH_PERSONAL_TOKEN:
|
GH_PERSONAL_TOKEN:
|
||||||
description: "The Github personal token."
|
description: "The Github personal token."
|
||||||
required: true
|
required: true
|
||||||
push:
|
SLACK_RELEASES_WEBHOOK_URL:
|
||||||
tags:
|
description: "The Slack webhook URL."
|
||||||
- '*'
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
name: Github - Release
|
name: Github - Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Download Exec
|
|
||||||
- name: Artifacts - Download executable
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
with:
|
|
||||||
name: exe
|
|
||||||
path: build/executable
|
|
||||||
|
|
||||||
# Check out
|
# Check out
|
||||||
- name: Checkout - Repository
|
- name: Checkout - Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -36,11 +28,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repository: kestra-io/actions
|
repository: kestra-io/actions
|
||||||
sparse-checkout-cone-mode: true
|
sparse-checkout-cone-mode: true
|
||||||
ref: fix/core-release
|
|
||||||
path: actions
|
path: actions
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
.github/actions
|
.github/actions
|
||||||
|
|
||||||
|
# Download Exec
|
||||||
|
# Must be done after checkout actions
|
||||||
|
- name: Artifacts - Download executable
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
with:
|
||||||
|
name: exe
|
||||||
|
path: build/executable
|
||||||
|
|
||||||
|
|
||||||
# GitHub Release
|
# GitHub Release
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
uses: ./actions/.github/actions/github-release
|
uses: ./actions/.github/actions/github-release
|
||||||
@@ -49,3 +50,16 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||||
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||||
|
|
||||||
|
# Trigger gha workflow to bump helm chart version
|
||||||
|
- name: GitHub - Trigger the Helm chart version bump
|
||||||
|
uses: peter-evans/repository-dispatch@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||||
|
repository: kestra-io/helm-charts
|
||||||
|
event-type: update-helm-chart-version
|
||||||
|
client-payload: |-
|
||||||
|
{
|
||||||
|
"new_version": "${{ github.ref_name }}",
|
||||||
|
"github_repository": "${{ github.repository }}",
|
||||||
|
"github_actor": "${{ github.actor }}"
|
||||||
|
}
|
||||||
9
.github/workflows/workflow-release.yml
vendored
9
.github/workflows/workflow-release.yml
vendored
@@ -42,6 +42,12 @@ on:
|
|||||||
SONATYPE_GPG_FILE:
|
SONATYPE_GPG_FILE:
|
||||||
description: "The Sonatype GPG file."
|
description: "The Sonatype GPG file."
|
||||||
required: true
|
required: true
|
||||||
|
GH_PERSONAL_TOKEN:
|
||||||
|
description: "The Github personal token."
|
||||||
|
required: true
|
||||||
|
SLACK_RELEASES_WEBHOOK_URL:
|
||||||
|
description: "The Slack webhook URL."
|
||||||
|
required: true
|
||||||
jobs:
|
jobs:
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
name: Build - Artifacts
|
name: Build - Artifacts
|
||||||
@@ -77,4 +83,5 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: ./.github/workflows/workflow-github-release.yml
|
uses: ./.github/workflows/workflow-github-release.yml
|
||||||
secrets:
|
secrets:
|
||||||
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||||
|
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||||
1
.plugins
1
.plugins
@@ -61,6 +61,7 @@
|
|||||||
#plugin-jenkins:io.kestra.plugin:plugin-jenkins:LATEST
|
#plugin-jenkins:io.kestra.plugin:plugin-jenkins:LATEST
|
||||||
#plugin-jira:io.kestra.plugin:plugin-jira:LATEST
|
#plugin-jira:io.kestra.plugin:plugin-jira:LATEST
|
||||||
#plugin-kafka:io.kestra.plugin:plugin-kafka:LATEST
|
#plugin-kafka:io.kestra.plugin:plugin-kafka:LATEST
|
||||||
|
#plugin-kestra:io.kestra.plugin:plugin-kestra:LATEST
|
||||||
#plugin-kubernetes:io.kestra.plugin:plugin-kubernetes:LATEST
|
#plugin-kubernetes:io.kestra.plugin:plugin-kubernetes:LATEST
|
||||||
#plugin-langchain4j:io.kestra.plugin:plugin-langchain4j:LATEST
|
#plugin-langchain4j:io.kestra.plugin:plugin-langchain4j:LATEST
|
||||||
#plugin-ldap:io.kestra.plugin:plugin-ldap:LATEST
|
#plugin-ldap:io.kestra.plugin:plugin-ldap:LATEST
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import io.kestra.core.services.PluginDefaultService;
|
|||||||
import io.micronaut.context.annotation.Requires;
|
import io.micronaut.context.annotation.Requires;
|
||||||
import io.micronaut.context.annotation.Value;
|
import io.micronaut.context.annotation.Value;
|
||||||
import io.micronaut.scheduling.io.watch.FileWatchConfiguration;
|
import io.micronaut.scheduling.io.watch.FileWatchConfiguration;
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
import jakarta.validation.ConstraintViolationException;
|
import jakarta.validation.ConstraintViolationException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -26,6 +26,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Requires(property = "micronaut.io.watch.enabled", value = "true")
|
@Requires(property = "micronaut.io.watch.enabled", value = "true")
|
||||||
@@ -111,6 +113,8 @@ public class FileChangedEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void startListening(List<Path> paths) throws IOException, InterruptedException {
|
public void startListening(List<Path> paths) throws IOException, InterruptedException {
|
||||||
|
String tenantId = this.tenantId != null ? this.tenantId : MAIN_TENANT;
|
||||||
|
|
||||||
for (Path path : paths) {
|
for (Path path : paths) {
|
||||||
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
|
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
|
||||||
}
|
}
|
||||||
@@ -189,6 +193,8 @@ public class FileChangedEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadFlowsFromFolder(Path folder) {
|
private void loadFlowsFromFolder(Path folder) {
|
||||||
|
String tenantId = this.tenantId != null ? this.tenantId : MAIN_TENANT;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
|
Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -232,6 +238,8 @@ public class FileChangedEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<FlowWithSource> parseFlow(String content, Path entry) {
|
private Optional<FlowWithSource> parseFlow(String content, Path entry) {
|
||||||
|
String tenantId = this.tenantId != null ? this.tenantId : MAIN_TENANT;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FlowWithSource flow = pluginDefaultService.parseFlowWithAllDefaults(tenantId, content, false);
|
FlowWithSource flow = pluginDefaultService.parseFlowWithAllDefaults(tenantId, content, false);
|
||||||
modelValidator.validate(flow);
|
modelValidator.validate(flow);
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ micronaut:
|
|||||||
static:
|
static:
|
||||||
paths: classpath:static
|
paths: classpath:static
|
||||||
mapping: /static/**
|
mapping: /static/**
|
||||||
|
root:
|
||||||
|
paths: classpath:root
|
||||||
|
mapping: /**
|
||||||
server:
|
server:
|
||||||
max-request-size: 10GB
|
max-request-size: 10GB
|
||||||
multipart:
|
multipart:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
class PluginDocCommandTest {
|
class PluginDocCommandTest {
|
||||||
|
|
||||||
public static final String PLUGIN_TEMPLATE_TEST = "plugin-template-test-0.18.0-SNAPSHOT.jar";
|
public static final String PLUGIN_TEMPLATE_TEST = "plugin-template-test-0.24.0-SNAPSHOT.jar";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void run() throws IOException, URISyntaxException {
|
void run() throws IOException, URISyntaxException {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
class PluginListCommandTest {
|
class PluginListCommandTest {
|
||||||
|
|
||||||
private static final String PLUGIN_TEMPLATE_TEST = "plugin-template-test-0.18.0-SNAPSHOT.jar";
|
private static final String PLUGIN_TEMPLATE_TEST = "plugin-template-test-0.24.0-SNAPSHOT.jar";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldListPluginsInstalledLocally() throws IOException, URISyntaxException {
|
void shouldListPluginsInstalledLocally() throws IOException, URISyntaxException {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,11 +1,14 @@
|
|||||||
package io.kestra.core.metrics;
|
package io.kestra.core.metrics;
|
||||||
|
|
||||||
|
import io.kestra.core.models.ServerType;
|
||||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||||
import io.micronaut.configuration.metrics.aggregator.MeterRegistryConfigurer;
|
import io.micronaut.configuration.metrics.aggregator.MeterRegistryConfigurer;
|
||||||
import io.micronaut.context.annotation.Requires;
|
import io.micronaut.context.annotation.Requires;
|
||||||
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import io.micronaut.context.annotation.Value;
|
||||||
|
import io.micronaut.core.annotation.Nullable;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
@@ -15,20 +18,26 @@ public class GlobalTagsConfigurer implements MeterRegistryConfigurer<SimpleMeter
|
|||||||
@Inject
|
@Inject
|
||||||
MetricConfig metricConfig;
|
MetricConfig metricConfig;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Value("${kestra.server-type}")
|
||||||
|
ServerType serverType;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure(SimpleMeterRegistry meterRegistry) {
|
public void configure(SimpleMeterRegistry meterRegistry) {
|
||||||
if (metricConfig.getTags() != null) {
|
String[] tags = Stream
|
||||||
meterRegistry
|
.concat(
|
||||||
.config()
|
metricConfig.getTags() != null ? metricConfig.getTags()
|
||||||
.commonTags(
|
.entrySet()
|
||||||
metricConfig.getTags()
|
.stream()
|
||||||
.entrySet()
|
.flatMap(e -> Stream.of(e.getKey(), e.getValue())) : Stream.empty(),
|
||||||
.stream()
|
serverType != null ? Stream.of("server_type", serverType.name()) : Stream.empty()
|
||||||
.flatMap(e -> Stream.of(e.getKey(), e.getValue()))
|
)
|
||||||
.toList()
|
.toList()
|
||||||
.toArray(String[]::new)
|
.toArray(String[]::new);
|
||||||
);
|
|
||||||
}
|
meterRegistry
|
||||||
|
.config()
|
||||||
|
.commonTags(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import jakarta.validation.constraints.NotNull;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Setting {
|
public class Setting {
|
||||||
public static final String INSTANCE_UUID = "instance.uuid";
|
public static final String INSTANCE_UUID = "instance.uuid";
|
||||||
|
public static final String INSTANCE_VERSION = "instance.version";
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private String key;
|
private String key;
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package io.kestra.core.models.flows;
|
package io.kestra.core.models.flows;
|
||||||
|
|
||||||
import io.micronaut.core.annotation.Introspected;
|
import io.micronaut.core.annotation.Introspected;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Positive;
|
|
||||||
|
|
||||||
@SuperBuilder
|
@SuperBuilder
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Introspected
|
@Introspected
|
||||||
public class Concurrency {
|
public class Concurrency {
|
||||||
@Positive
|
@Min(1)
|
||||||
@NotNull
|
@NotNull
|
||||||
private Integer limit;
|
private Integer limit;
|
||||||
|
|
||||||
|
|||||||
@@ -329,6 +329,14 @@ public class DefaultPluginRegistry implements PluginRegistry {
|
|||||||
pluginClassByIdentifier.clear();
|
pluginClassByIdentifier.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
**/
|
||||||
|
@Override
|
||||||
|
public boolean isVersioningSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public record PluginBundleIdentifier(@Nullable URL location) {
|
public record PluginBundleIdentifier(@Nullable URL location) {
|
||||||
|
|
||||||
public static PluginBundleIdentifier CORE = new PluginBundleIdentifier(null);
|
public static PluginBundleIdentifier CORE = new PluginBundleIdentifier(null);
|
||||||
|
|||||||
@@ -116,4 +116,11 @@ public interface PluginRegistry {
|
|||||||
default void clear() {
|
default void clear() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether plugin-versioning is supported by this registry.
|
||||||
|
*
|
||||||
|
* @return {@code true} if supported. Otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
boolean isVersioningSupported();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.FileSystemNotFoundException;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -202,19 +204,13 @@ public class PluginScanner {
|
|||||||
|
|
||||||
var guidesDirectory = classLoader.getResource("doc/guides");
|
var guidesDirectory = classLoader.getResource("doc/guides");
|
||||||
if (guidesDirectory != null) {
|
if (guidesDirectory != null) {
|
||||||
try (var fileSystem = FileSystems.newFileSystem(guidesDirectory.toURI(), Collections.emptyMap())) {
|
try {
|
||||||
var root = fileSystem.getPath("/doc/guides");
|
var root = Path.of(guidesDirectory.toURI());
|
||||||
try (var stream = Files.walk(root, 1)) {
|
addGuides(root, guides);
|
||||||
stream
|
|
||||||
.skip(1) // first element is the root element
|
|
||||||
.sorted(Comparator.comparing(path -> path.getName(path.getParent().getNameCount()).toString()))
|
|
||||||
.forEach(guide -> {
|
|
||||||
var guideName = guide.getName(guide.getParent().getNameCount()).toString();
|
|
||||||
guides.add(guideName.substring(0, guideName.lastIndexOf('.')));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (IOException | URISyntaxException e) {
|
} catch (IOException | URISyntaxException e) {
|
||||||
// silently fail
|
// silently fail
|
||||||
|
} catch (FileSystemNotFoundException e) {
|
||||||
|
addGuidesThroughNewFileSystem(guidesDirectory, guides);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +239,27 @@ public class PluginScanner {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addGuidesThroughNewFileSystem(URL guidesDirectory, List<String> guides) {
|
||||||
|
try (var fileSystem = FileSystems.newFileSystem(guidesDirectory.toURI(), Collections.emptyMap())) {
|
||||||
|
var root = fileSystem.getPath("doc/guides");
|
||||||
|
addGuides(root, guides);
|
||||||
|
} catch (IOException | URISyntaxException e) {
|
||||||
|
// silently fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addGuides(Path root, List<String> guides) throws IOException {
|
||||||
|
try (var stream = Files.walk(root, 1)) {
|
||||||
|
stream
|
||||||
|
.skip(1) // first element is the root element
|
||||||
|
.sorted(Comparator.comparing(path -> path.getName(path.getParent().getNameCount()).toString()))
|
||||||
|
.forEach(guide -> {
|
||||||
|
var guideName = guide.getName(guide.getParent().getNameCount()).toString();
|
||||||
|
guides.add(guideName.substring(0, guideName.lastIndexOf('.')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Manifest getManifest(ClassLoader classLoader) {
|
public static Manifest getManifest(ClassLoader classLoader) {
|
||||||
try {
|
try {
|
||||||
URL url = classLoader.getResource(JarFile.MANIFEST_NAME);
|
URL url = classLoader.getResource(JarFile.MANIFEST_NAME);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
|||||||
DeserializationContext context) throws IOException {
|
DeserializationContext context) throws IOException {
|
||||||
Class<? extends Plugin> pluginType = null;
|
Class<? extends Plugin> pluginType = null;
|
||||||
|
|
||||||
final String identifier = extractPluginRawIdentifier(node);
|
final String identifier = extractPluginRawIdentifier(node, pluginRegistry.isVersioningSupported());
|
||||||
if (identifier != null) {
|
if (identifier != null) {
|
||||||
log.trace("Looking for Plugin for: {}",
|
log.trace("Looking for Plugin for: {}",
|
||||||
identifier
|
identifier
|
||||||
@@ -103,7 +103,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (DataChart.class.isAssignableFrom(pluginType)) {
|
if (DataChart.class.isAssignableFrom(pluginType)) {
|
||||||
final Class<? extends Plugin> dataFilterClass = pluginRegistry.findClassByIdentifier(extractPluginRawIdentifier(node.get("data")));
|
final Class<? extends Plugin> dataFilterClass = pluginRegistry.findClassByIdentifier(extractPluginRawIdentifier(node.get("data"), pluginRegistry.isVersioningSupported()));
|
||||||
ParameterizedType genericDataFilterClass = (ParameterizedType) dataFilterClass.getGenericSuperclass();
|
ParameterizedType genericDataFilterClass = (ParameterizedType) dataFilterClass.getGenericSuperclass();
|
||||||
Type dataFieldsEnum = genericDataFilterClass.getActualTypeArguments()[0];
|
Type dataFieldsEnum = genericDataFilterClass.getActualTypeArguments()[0];
|
||||||
TypeFactory typeFactory = JacksonMapper.ofJson().getTypeFactory();
|
TypeFactory typeFactory = JacksonMapper.ofJson().getTypeFactory();
|
||||||
@@ -142,7 +142,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String extractPluginRawIdentifier(final JsonNode node) {
|
static String extractPluginRawIdentifier(final JsonNode node, final boolean isVersioningSupported) {
|
||||||
String type = Optional.ofNullable(node.get(TYPE)).map(JsonNode::textValue).orElse(null);
|
String type = Optional.ofNullable(node.get(TYPE)).map(JsonNode::textValue).orElse(null);
|
||||||
String version = Optional.ofNullable(node.get(VERSION)).map(JsonNode::textValue).orElse(null);
|
String version = Optional.ofNullable(node.get(VERSION)).map(JsonNode::textValue).orElse(null);
|
||||||
|
|
||||||
@@ -150,6 +150,6 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return version != null && !version.isEmpty() ? type + ":" + version : type;
|
return isVersioningSupported && version != null && !version.isEmpty() ? type + ":" + version : type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ public class ExecutorService {
|
|||||||
@Inject
|
@Inject
|
||||||
private WorkerGroupExecutorInterface workerGroupExecutorInterface;
|
private WorkerGroupExecutorInterface workerGroupExecutorInterface;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private WorkerJobRunningStateStore workerJobRunningStateStore;
|
||||||
|
|
||||||
protected FlowMetaStoreInterface flowExecutorInterface;
|
protected FlowMetaStoreInterface flowExecutorInterface;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -1072,6 +1075,25 @@ public class ExecutorService {
|
|||||||
newExecution = executionService.killParentTaskruns(taskRun, newExecution);
|
newExecution = executionService.killParentTaskruns(taskRun, newExecution);
|
||||||
}
|
}
|
||||||
executor.withExecution(newExecution, "addWorkerTaskResult");
|
executor.withExecution(newExecution, "addWorkerTaskResult");
|
||||||
|
if (taskRun.getState().isTerminated()) {
|
||||||
|
log.trace("TaskRun terminated: {}", taskRun);
|
||||||
|
workerJobRunningStateStore.deleteByKey(taskRun.getId());
|
||||||
|
metricRegistry
|
||||||
|
.counter(
|
||||||
|
MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_COUNT,
|
||||||
|
MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_COUNT_DESCRIPTION,
|
||||||
|
metricRegistry.tags(workerTaskResult)
|
||||||
|
)
|
||||||
|
.increment();
|
||||||
|
|
||||||
|
metricRegistry
|
||||||
|
.timer(
|
||||||
|
MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_DURATION,
|
||||||
|
MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_DURATION_DESCRIPTION,
|
||||||
|
metricRegistry.tags(workerTaskResult)
|
||||||
|
)
|
||||||
|
.record(taskRun.getState().getDuration());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: as the flow is only used in an error branch and it can take time to load, we pass it thought a Supplier
|
// Note: as the flow is only used in an error branch and it can take time to load, we pass it thought a Supplier
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package io.kestra.core.runners;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State store containing all workers' jobs in RUNNING state.
|
||||||
|
*
|
||||||
|
* @see WorkerJob
|
||||||
|
*/
|
||||||
|
public interface WorkerJobRunningStateStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a running worker job for the given key.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A key can be a {@link WorkerTask} Task Run ID.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param key the key of the worker job to be deleted.
|
||||||
|
*/
|
||||||
|
void deleteByKey(String key);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.amazon.ion.IonSystem;
|
|||||||
import com.amazon.ion.system.*;
|
import com.amazon.ion.system.*;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.StreamReadConstraints;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
@@ -36,6 +37,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import static com.fasterxml.jackson.core.StreamReadConstraints.DEFAULT_MAX_STRING_LEN;
|
||||||
|
|
||||||
public final class JacksonMapper {
|
public final class JacksonMapper {
|
||||||
public static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {};
|
public static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {};
|
||||||
public static final TypeReference<List<Object>> LIST_TYPE_REFERENCE = new TypeReference<>() {};
|
public static final TypeReference<List<Object>> LIST_TYPE_REFERENCE = new TypeReference<>() {};
|
||||||
@@ -43,6 +46,12 @@ public final class JacksonMapper {
|
|||||||
|
|
||||||
private JacksonMapper() {}
|
private JacksonMapper() {}
|
||||||
|
|
||||||
|
static {
|
||||||
|
StreamReadConstraints.overrideDefaultStreamReadConstraints(
|
||||||
|
StreamReadConstraints.builder().maxNameLength(DEFAULT_MAX_STRING_LEN).build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static final ObjectMapper MAPPER = JacksonMapper.configure(
|
private static final ObjectMapper MAPPER = JacksonMapper.configure(
|
||||||
new ObjectMapper()
|
new ObjectMapper()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ public class FlowService {
|
|||||||
previous :
|
previous :
|
||||||
FlowWithSource.of(flowToImport.toBuilder().revision(previous.getRevision() + 1).build(), source)
|
FlowWithSource.of(flowToImport.toBuilder().revision(previous.getRevision() + 1).build(), source)
|
||||||
)
|
)
|
||||||
.orElseGet(() -> FlowWithSource.of(flowToImport, source).toBuilder().revision(1).build());
|
.orElseGet(() -> FlowWithSource.of(flowToImport, source).toBuilder().tenantId(tenantId).revision(1).build());
|
||||||
} else {
|
} else {
|
||||||
return maybeExisting
|
return maybeExisting
|
||||||
.map(previous -> repository().update(flow, previous))
|
.map(previous -> repository().update(flow, previous))
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ import io.kestra.core.test.TestState;
|
|||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record UnitTestResult(
|
public record UnitTestResult(
|
||||||
@NotNull
|
@NotNull
|
||||||
String unitTestId,
|
String testId,
|
||||||
@NotNull
|
@NotNull
|
||||||
String unitTestType,
|
String testType,
|
||||||
@NotNull
|
@NotNull
|
||||||
String executionId,
|
String executionId,
|
||||||
@NotNull
|
@NotNull
|
||||||
|
URI url,
|
||||||
|
@NotNull
|
||||||
TestState state,
|
TestState state,
|
||||||
@NotNull
|
@NotNull
|
||||||
List<AssertionResult> assertionResults,
|
List<AssertionResult> assertionResults,
|
||||||
@@ -22,14 +25,13 @@ public record UnitTestResult(
|
|||||||
List<AssertionRunError> errors,
|
List<AssertionRunError> errors,
|
||||||
Fixtures fixtures
|
Fixtures fixtures
|
||||||
) {
|
) {
|
||||||
|
public static UnitTestResult of(String unitTestId, String unitTestType, String executionId, URI url, List<AssertionResult> results, List<AssertionRunError> errors, @Nullable Fixtures fixtures) {
|
||||||
public static UnitTestResult of(String unitTestId, String unitTestType, String executionId, List<AssertionResult> results, List<AssertionRunError> errors, @Nullable Fixtures fixtures) {
|
|
||||||
TestState state;
|
TestState state;
|
||||||
if(!errors.isEmpty()){
|
if(!errors.isEmpty()){
|
||||||
state = TestState.ERROR;
|
state = TestState.ERROR;
|
||||||
} else {
|
} else {
|
||||||
state = results.stream().anyMatch(assertion -> !assertion.isSuccess()) ? TestState.FAILED : TestState.SUCCESS;
|
state = results.stream().anyMatch(assertion -> !assertion.isSuccess()) ? TestState.FAILED : TestState.SUCCESS;
|
||||||
}
|
}
|
||||||
return new UnitTestResult(unitTestId, unitTestType, executionId, state, results, errors, fixtures);
|
return new UnitTestResult(unitTestId, unitTestType, executionId, url, state, results, errors, fixtures);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package io.kestra.core.utils;
|
package io.kestra.core.utils;
|
||||||
|
|
||||||
|
import io.kestra.core.models.Setting;
|
||||||
|
import io.kestra.core.repositories.SettingRepositoryInterface;
|
||||||
import io.micronaut.context.env.Environment;
|
import io.micronaut.context.env.Environment;
|
||||||
import io.micronaut.context.env.PropertiesPropertySourceLoader;
|
import io.micronaut.context.env.PropertiesPropertySourceLoader;
|
||||||
import io.micronaut.context.env.PropertySource;
|
import io.micronaut.context.env.PropertySource;
|
||||||
@@ -29,6 +31,9 @@ public class VersionProvider {
|
|||||||
@Inject
|
@Inject
|
||||||
private Environment environment;
|
private Environment environment;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Optional<SettingRepositoryInterface> settingRepository; // repositories are not always there on unit tests
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void start() {
|
public void start() {
|
||||||
final Optional<PropertySource> gitProperties = new PropertiesPropertySourceLoader()
|
final Optional<PropertySource> gitProperties = new PropertiesPropertySourceLoader()
|
||||||
@@ -40,6 +45,18 @@ public class VersionProvider {
|
|||||||
this.revision = loadRevision(gitProperties);
|
this.revision = loadRevision(gitProperties);
|
||||||
this.date = loadTime(gitProperties);
|
this.date = loadTime(gitProperties);
|
||||||
this.version = loadVersion(buildProperties, gitProperties);
|
this.version = loadVersion(buildProperties, gitProperties);
|
||||||
|
|
||||||
|
// check the version in the settings and update if needed, we did't use it would allow us to detect incompatible update later if needed
|
||||||
|
if (settingRepository.isPresent()) {
|
||||||
|
Optional<Setting> versionSetting = settingRepository.get().findByKey(Setting.INSTANCE_VERSION);
|
||||||
|
if (versionSetting.isEmpty() || !versionSetting.get().getValue().equals(this.version)) {
|
||||||
|
settingRepository.get().save(Setting.builder()
|
||||||
|
.key(Setting.INSTANCE_VERSION)
|
||||||
|
.value(this.version)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String loadVersion(final Optional<PropertySource> buildProperties,
|
private String loadVersion(final Optional<PropertySource> buildProperties,
|
||||||
|
|||||||
@@ -29,8 +29,13 @@ import java.util.concurrent.TimeUnit;
|
|||||||
@Plugin(
|
@Plugin(
|
||||||
examples = {
|
examples = {
|
||||||
@Example(
|
@Example(
|
||||||
|
full = true,
|
||||||
code = """
|
code = """
|
||||||
id: sleep
|
id: sleep
|
||||||
|
namespace: company.team
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- id: sleep
|
||||||
type: io.kestra.plugin.core.flow.Sleep
|
type: io.kestra.plugin.core.flow.Sleep
|
||||||
duration: "PT5S"
|
duration: "PT5S"
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ class DocumentationGeneratorTest {
|
|||||||
assertThat(render).contains("description: \"Short description for this task\"");
|
assertThat(render).contains("description: \"Short description for this task\"");
|
||||||
assertThat(render).contains("`VALUE_1`");
|
assertThat(render).contains("`VALUE_1`");
|
||||||
assertThat(render).contains("`VALUE_2`");
|
assertThat(render).contains("`VALUE_2`");
|
||||||
assertThat(render).contains("This plugin is exclusively available on the Cloud and Enterprise editions of Kestra.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
|
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.fasterxml.jackson.databind.node.TextNode;
|
import com.fasterxml.jackson.databind.node.TextNode;
|
||||||
import io.kestra.core.models.Plugin;
|
import io.kestra.core.models.Plugin;
|
||||||
import io.kestra.core.plugins.PluginRegistry;
|
import io.kestra.core.plugins.PluginRegistry;
|
||||||
@@ -15,12 +16,14 @@ import org.mockito.Mockito;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.mockito.stubbing.Answer;
|
import org.mockito.stubbing.Answer;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class PluginDeserializerTest {
|
class PluginDeserializerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private PluginRegistry registry;
|
private PluginRegistry registry;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSucceededDeserializePluginGivenValidType() throws JsonProcessingException {
|
void shouldSucceededDeserializePluginGivenValidType() throws JsonProcessingException {
|
||||||
// Given
|
// Given
|
||||||
@@ -38,8 +41,9 @@ class PluginDeserializerTest {
|
|||||||
|
|
||||||
TestPluginHolder deserialized = om.readValue(input, TestPluginHolder.class);
|
TestPluginHolder deserialized = om.readValue(input, TestPluginHolder.class);
|
||||||
// Then
|
// Then
|
||||||
Assertions.assertEquals(TestPlugin.class.getCanonicalName(), deserialized.plugin().getType());
|
assertThat(TestPlugin.class.getCanonicalName()).isEqualTo(deserialized.plugin().getType());
|
||||||
Mockito.verify(registry, Mockito.only()).findClassByIdentifier(identifier);
|
Mockito.verify(registry, Mockito.times(1)).isVersioningSupported();
|
||||||
|
Mockito.verify(registry, Mockito.times(1)).findClassByIdentifier(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -57,17 +61,33 @@ class PluginDeserializerTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assertions.assertEquals("io.kestra.core.plugins.serdes.Unknown", exception.getTypeId());
|
assertThat("io.kestra.core.plugins.serdes.Unknown").isEqualTo(exception.getTypeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnNullPluginIdentifierGivenNullType() {
|
void shouldReturnNullPluginIdentifierGivenNullType() {
|
||||||
Assertions.assertNull(PluginDeserializer.extractPluginRawIdentifier(new TextNode(null)));
|
assertThat(PluginDeserializer.extractPluginRawIdentifier(new TextNode(null), true)).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnNullPluginIdentifierGivenEmptyType() {
|
void shouldReturnNullPluginIdentifierGivenEmptyType() {
|
||||||
Assertions.assertNull(PluginDeserializer.extractPluginRawIdentifier(new TextNode("")));
|
assertThat(PluginDeserializer.extractPluginRawIdentifier(new TextNode(""), true)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTypeWithVersionGivenSupportedVersionTrue() {
|
||||||
|
ObjectNode jsonNodes = new ObjectNode(new ObjectMapper().getNodeFactory());
|
||||||
|
jsonNodes.set("type", new TextNode("io.kestra.core.plugins.serdes.Unknown"));
|
||||||
|
jsonNodes.set("version", new TextNode("1.0.0"));
|
||||||
|
assertThat(PluginDeserializer.extractPluginRawIdentifier(jsonNodes, true)).isEqualTo("io.kestra.core.plugins.serdes.Unknown:1.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTypeWithVersionGivenSupportedVersionFalse() {
|
||||||
|
ObjectNode jsonNodes = new ObjectNode(new ObjectMapper().getNodeFactory());
|
||||||
|
jsonNodes.set("type", new TextNode("io.kestra.core.plugins.serdes.Unknown"));
|
||||||
|
jsonNodes.set("version", new TextNode("1.0.0"));
|
||||||
|
assertThat(PluginDeserializer.extractPluginRawIdentifier(jsonNodes, false)).isEqualTo("io.kestra.core.plugins.serdes.Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
public record TestPluginHolder(Plugin plugin) {
|
public record TestPluginHolder(Plugin plugin) {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
200
dev-tools/update-plugin-kestra-version.sh
Executable file
200
dev-tools/update-plugin-kestra-version.sh
Executable file
@@ -0,0 +1,200 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#===============================================================================
|
||||||
|
# SCRIPT: update-plugin-kestra-version.sh
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# This script can be used to update the gradle 'kestraVersion' property on each kestra plugin repository.
|
||||||
|
# By default, if no `GITHUB_PAT` environment variable exist, the script will attempt to clone GitHub repositories using SSH_KEY.
|
||||||
|
#
|
||||||
|
#USAGE:
|
||||||
|
# ./dev-tools/update-plugin-kestra-version.sh --branch <branch> --version <version> [plugin-repositories...]
|
||||||
|
#
|
||||||
|
#OPTIONS:
|
||||||
|
# --branch <branch> Specify the branch on which to update the kestraCoreVersion (default: master).
|
||||||
|
# --version <version> Specify the Kestra core version (required).
|
||||||
|
# --plugin-file File containing the plugin list (default: .plugins)
|
||||||
|
# --dry-run Specify to run in DRY_RUN.
|
||||||
|
# -y, --yes Automatically confirm prompts (non-interactive).
|
||||||
|
# -h, --help Show this help message and exit.
|
||||||
|
|
||||||
|
|
||||||
|
# EXAMPLES:
|
||||||
|
# To release all plugins:
|
||||||
|
# ./update-plugin-kestra-version.sh --branch=releases/v0.23.x --version="[0.23,0.24)"
|
||||||
|
# To release a specific plugin:
|
||||||
|
# ./update-plugin-kestra-version.sh --branch=releases/v0.23.x --version="[0.23,0.24)" plugin-kubernetes
|
||||||
|
# To release specific plugins from file:
|
||||||
|
# ./update-plugin-kestra-version.sh --branch=releases/v0.23.x --version="[0.23,0.24)" --plugin-file .plugins
|
||||||
|
#===============================================================================
|
||||||
|
|
||||||
|
set -e;
|
||||||
|
|
||||||
|
###############################################################
|
||||||
|
# Global vars
|
||||||
|
###############################################################
|
||||||
|
BASEDIR=$(dirname "$(readlink -f $0)")
|
||||||
|
SCRIPT_NAME=$(basename "$0")
|
||||||
|
SCRIPT_NAME="${SCRIPT_NAME%.*}"
|
||||||
|
WORKING_DIR="/tmp/kestra-$SCRIPT_NAME-$(date +%s)"
|
||||||
|
PLUGIN_FILE="$BASEDIR/../.plugins"
|
||||||
|
GIT_BRANCH=master
|
||||||
|
|
||||||
|
###############################################################
|
||||||
|
# Functions
|
||||||
|
###############################################################
|
||||||
|
|
||||||
|
# Function to display the help message
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 --branch <branch> --version <version> [plugin-repositories...]"
|
||||||
|
echo
|
||||||
|
echo "Options:"
|
||||||
|
echo " --branch <branch> Specify the branch on which to update the kestraCoreVersion (default: master)."
|
||||||
|
echo " --version <version> Specify the Kestra core version (required)."
|
||||||
|
echo " --plugin-file File containing the plugin list (default: .plugins)"
|
||||||
|
echo " --dry-run Specify to run in DRY_RUN."
|
||||||
|
echo " -y, --yes Automatically confirm prompts (non-interactive)."
|
||||||
|
echo " -h, --help Show this help message and exit."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to ask to continue
|
||||||
|
function askToContinue() {
|
||||||
|
read -p "Are you sure you want to continue? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Operation cancelled."; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################
|
||||||
|
# Options
|
||||||
|
###############################################################
|
||||||
|
|
||||||
|
PLUGINS_ARGS=()
|
||||||
|
AUTO_YES=false
|
||||||
|
DRY_RUN=false
|
||||||
|
# Get the options
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--branch)
|
||||||
|
GIT_BRANCH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--branch=*)
|
||||||
|
GIT_BRANCH="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--version)
|
||||||
|
VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--version=*)
|
||||||
|
VERSION="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--plugin-file)
|
||||||
|
PLUGIN_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--plugin-file=*)
|
||||||
|
PLUGIN_FILE="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-y|--yes)
|
||||||
|
AUTO_YES=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
PLUGINS_ARGS+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
## Check options
|
||||||
|
if [[ -z "$VERSION" ]]; then
|
||||||
|
echo -e "Missing required argument: --version\n";
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
## Get plugin list
|
||||||
|
if [[ "${#PLUGINS_ARGS[@]}" -eq 0 ]]; then
|
||||||
|
if [ -f "$PLUGIN_FILE" ]; then
|
||||||
|
PLUGINS=$(cat "$PLUGIN_FILE" | grep "io\\.kestra\\." | sed -e '/#/s/^.//' | cut -d':' -f1 | uniq | sort);
|
||||||
|
PLUGINS_COUNT=$(echo "$PLUGINS" | wc -l);
|
||||||
|
PLUGINS_ARRAY=$(echo "$PLUGINS" | xargs || echo '');
|
||||||
|
PLUGINS_ARRAY=($PLUGINS_ARRAY);
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
PLUGINS_ARRAY=("${PLUGINS_ARGS[@]}")
|
||||||
|
PLUGINS_COUNT="${#PLUGINS_ARGS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
## Get plugin list
|
||||||
|
echo "VERSION=$RELEASE_VERSION"
|
||||||
|
echo "GIT_BRANCH=$GIT_BRANCH"
|
||||||
|
echo "DRY_RUN=$DRY_RUN"
|
||||||
|
echo "Found ($PLUGINS_COUNT) plugin repositories:";
|
||||||
|
|
||||||
|
for PLUGIN in "${PLUGINS_ARRAY[@]}"; do
|
||||||
|
echo "$PLUGIN"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$AUTO_YES" == false ]]; then
|
||||||
|
askToContinue
|
||||||
|
fi
|
||||||
|
|
||||||
|
###############################################################
|
||||||
|
# Main
|
||||||
|
###############################################################
|
||||||
|
mkdir -p $WORKING_DIR
|
||||||
|
|
||||||
|
COUNTER=1;
|
||||||
|
for PLUGIN in "${PLUGINS_ARRAY[@]}"
|
||||||
|
do
|
||||||
|
cd $WORKING_DIR;
|
||||||
|
|
||||||
|
echo "---------------------------------------------------------------------------------------"
|
||||||
|
echo "[$COUNTER/$PLUGINS_COUNT] Update Plugin: $PLUGIN"
|
||||||
|
echo "---------------------------------------------------------------------------------------"
|
||||||
|
if [[ -z "${GITHUB_PAT}" ]]; then
|
||||||
|
git clone git@github.com:kestra-io/$PLUGIN
|
||||||
|
else
|
||||||
|
echo "Clone git repository using GITHUB PAT"
|
||||||
|
git clone https://${GITHUB_PAT}@github.com/kestra-io/$PLUGIN.git
|
||||||
|
fi
|
||||||
|
cd "$PLUGIN";
|
||||||
|
|
||||||
|
if [[ "$PLUGIN" == "plugin-transform" ]] && [[ "$GIT_BRANCH" == "master" ]]; then # quickfix
|
||||||
|
git checkout main;
|
||||||
|
else
|
||||||
|
git checkout "$GIT_BRANCH";
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_BRANCH=$(git branch --show-current);
|
||||||
|
|
||||||
|
echo "Update kestraVersion for plugin: $PLUGIN on branch $CURRENT_BRANCH:";
|
||||||
|
# Update the kestraVersion property
|
||||||
|
sed -i "s/^kestraVersion=.*/kestraVersion=${VERSION}/" ./gradle.properties
|
||||||
|
# Display diff
|
||||||
|
git diff --exit-code --unified=0 ./gradle.properties | grep -E '^\+|^-' | grep -v -E '^\+\+\+|^---'
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
|
if [[ "$AUTO_YES" == false ]]; then
|
||||||
|
askToContinue
|
||||||
|
fi
|
||||||
|
git add ./gradle.properties
|
||||||
|
git commit -m"chore(deps): update kestraVersion to ${VERSION}."
|
||||||
|
git push
|
||||||
|
else
|
||||||
|
echo "Skip git commit/push [DRY_RUN=true]";
|
||||||
|
fi
|
||||||
|
COUNTER=$(( COUNTER + 1 ));
|
||||||
|
done;
|
||||||
|
|
||||||
|
exit 0;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
version=0.24.0-SNAPSHOT
|
version=0.23.1
|
||||||
|
|
||||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.priority=low
|
org.gradle.priority=low
|
||||||
|
|||||||
@@ -39,4 +39,13 @@ public class H2TenantMigration extends AbstractJdbcTenantMigration {
|
|||||||
|
|
||||||
return context.execute(query);
|
return context.execute(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int deleteTutorialFlows(Table<?> table, DSLContext context) {
|
||||||
|
String query = """
|
||||||
|
DELETE FROM "%s"
|
||||||
|
WHERE JQ_STRING("value", '.namespace') = ?
|
||||||
|
""".formatted(table.getName());
|
||||||
|
return context.execute(query, "tutorial");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import io.kestra.core.exceptions.DeserializationException;
|
|||||||
import io.kestra.core.models.triggers.TriggerContext;
|
import io.kestra.core.models.triggers.TriggerContext;
|
||||||
import io.kestra.core.queues.QueueFactoryInterface;
|
import io.kestra.core.queues.QueueFactoryInterface;
|
||||||
import io.kestra.core.queues.QueueInterface;
|
import io.kestra.core.queues.QueueInterface;
|
||||||
|
import io.kestra.core.runners.WorkerJobRunningStateStore;
|
||||||
import io.kestra.core.runners.WorkerTriggerResult;
|
import io.kestra.core.runners.WorkerTriggerResult;
|
||||||
import io.kestra.core.utils.Either;
|
import io.kestra.core.utils.Either;
|
||||||
import io.kestra.jdbc.repository.AbstractJdbcWorkerJobRunningRepository;
|
|
||||||
import io.kestra.jdbc.runner.JdbcQueue;
|
import io.kestra.jdbc.runner.JdbcQueue;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import io.micronaut.inject.qualifiers.Qualifiers;
|
import io.micronaut.inject.qualifiers.Qualifiers;
|
||||||
@@ -30,7 +30,8 @@ public class JdbcWorkerTriggerResultQueueService implements Closeable {
|
|||||||
private final JdbcQueue<WorkerTriggerResult> workerTriggerResultQueue;
|
private final JdbcQueue<WorkerTriggerResult> workerTriggerResultQueue;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private AbstractJdbcWorkerJobRunningRepository jdbcWorkerJobRunningRepository;
|
private WorkerJobRunningStateStore workerJobRunningStateStore;
|
||||||
|
|
||||||
private final AtomicReference<Runnable> disposable = new AtomicReference<>();
|
private final AtomicReference<Runnable> disposable = new AtomicReference<>();
|
||||||
|
|
||||||
private final AtomicBoolean isClosed = new AtomicBoolean(false);
|
private final AtomicBoolean isClosed = new AtomicBoolean(false);
|
||||||
@@ -52,14 +53,14 @@ public class JdbcWorkerTriggerResultQueueService implements Closeable {
|
|||||||
try {
|
try {
|
||||||
JsonNode json = MAPPER.readTree(either.getRight().getRecord());
|
JsonNode json = MAPPER.readTree(either.getRight().getRecord());
|
||||||
var triggerContext = MAPPER.treeToValue(json.get("triggerContext"), TriggerContext.class);
|
var triggerContext = MAPPER.treeToValue(json.get("triggerContext"), TriggerContext.class);
|
||||||
jdbcWorkerJobRunningRepository.deleteByKey(triggerContext.uid());
|
workerJobRunningStateStore.deleteByKey(triggerContext.uid());
|
||||||
} catch (JsonProcessingException | DeserializationException e) {
|
} catch (JsonProcessingException | DeserializationException e) {
|
||||||
// ignore the message if we cannot do anything about it
|
// ignore the message if we cannot do anything about it
|
||||||
log.error("Unexpected exception when trying to handle a deserialization error", e);
|
log.error("Unexpected exception when trying to handle a deserialization error", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
WorkerTriggerResult workerTriggerResult = either.getLeft();
|
WorkerTriggerResult workerTriggerResult = either.getLeft();
|
||||||
jdbcWorkerJobRunningRepository.deleteByKey(workerTriggerResult.getTriggerContext().uid());
|
workerJobRunningStateStore.deleteByKey(workerTriggerResult.getTriggerContext().uid());
|
||||||
}
|
}
|
||||||
consumer.accept(either);
|
consumer.accept(either);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ public abstract class AbstractJdbcTenantMigration implements TenantMigrationInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!dryRun) {
|
if (!dryRun) {
|
||||||
|
if ("flows".equalsIgnoreCase(table.getName()) || "triggers".equalsIgnoreCase(table.getName())){
|
||||||
|
log.info("🔸 Delete tutorial flows to prevent duplication");
|
||||||
|
int deleted = dslContextWrapper.transactionResult(configuration -> {
|
||||||
|
DSLContext context = DSL.using(configuration);
|
||||||
|
return deleteTutorialFlows(table, context);
|
||||||
|
});
|
||||||
|
log.info("✅ {} tutorial flows have been deleted", deleted);
|
||||||
|
}
|
||||||
|
|
||||||
int updated;
|
int updated;
|
||||||
if (tableWithKey(table.getName())){
|
if (tableWithKey(table.getName())){
|
||||||
updated = dslContextWrapper.transactionResult(configuration -> {
|
updated = dslContextWrapper.transactionResult(configuration -> {
|
||||||
@@ -93,4 +102,9 @@ public abstract class AbstractJdbcTenantMigration implements TenantMigrationInte
|
|||||||
|
|
||||||
protected abstract int updateTenantIdFieldAndKey(Table<?> table, DSLContext context);
|
protected abstract int updateTenantIdFieldAndKey(Table<?> table, DSLContext context);
|
||||||
|
|
||||||
|
protected int deleteTutorialFlows(Table<?> table, DSLContext context){
|
||||||
|
String query = "DELETE FROM %s WHERE namespace = ?".formatted(table.getName());
|
||||||
|
return context.execute(query, "tutorial");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.kestra.jdbc.repository;
|
|||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import io.kestra.core.repositories.WorkerJobRunningRepositoryInterface;
|
import io.kestra.core.repositories.WorkerJobRunningRepositoryInterface;
|
||||||
import io.kestra.core.runners.WorkerJobRunning;
|
import io.kestra.core.runners.WorkerJobRunning;
|
||||||
|
import io.kestra.core.runners.WorkerJobRunningStateStore;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jooq.DSLContext;
|
import org.jooq.DSLContext;
|
||||||
import org.jooq.Record1;
|
import org.jooq.Record1;
|
||||||
@@ -13,7 +14,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractJdbcWorkerJobRunningRepository extends AbstractJdbcRepository implements WorkerJobRunningRepositoryInterface {
|
public abstract class AbstractJdbcWorkerJobRunningRepository extends AbstractJdbcRepository implements WorkerJobRunningRepositoryInterface, WorkerJobRunningStateStore {
|
||||||
protected io.kestra.jdbc.AbstractJdbcRepository<WorkerJobRunning> jdbcRepository;
|
protected io.kestra.jdbc.AbstractJdbcRepository<WorkerJobRunning> jdbcRepository;
|
||||||
|
|
||||||
public AbstractJdbcWorkerJobRunningRepository(io.kestra.jdbc.AbstractJdbcRepository<WorkerJobRunning> jdbcRepository) {
|
public AbstractJdbcWorkerJobRunningRepository(io.kestra.jdbc.AbstractJdbcRepository<WorkerJobRunning> jdbcRepository) {
|
||||||
@@ -26,9 +27,15 @@ public abstract class AbstractJdbcWorkerJobRunningRepository extends AbstractJdb
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteByKey(String uid) {
|
public void deleteByKey(String key) {
|
||||||
Optional<WorkerJobRunning> workerJobRunning = this.findByKey(uid);
|
this.jdbcRepository.getDslContextWrapper()
|
||||||
workerJobRunning.ifPresent(jobRunning -> this.jdbcRepository.delete(jobRunning));
|
.transaction(configuration ->
|
||||||
|
DSL
|
||||||
|
.using(configuration)
|
||||||
|
.deleteFrom(this.jdbcRepository.getTable())
|
||||||
|
.where(field("key").eq(key))
|
||||||
|
.execute()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -718,22 +718,6 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
|||||||
try {
|
try {
|
||||||
// process worker task result
|
// process worker task result
|
||||||
executorService.addWorkerTaskResult(current, () -> findFlow(execution), message);
|
executorService.addWorkerTaskResult(current, () -> findFlow(execution), message);
|
||||||
|
|
||||||
// send metrics on terminated
|
|
||||||
TaskRun taskRun = message.getTaskRun();
|
|
||||||
if (taskRun.getState().isTerminated()) {
|
|
||||||
metricRegistry
|
|
||||||
.counter(MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_COUNT, MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_COUNT_DESCRIPTION, metricRegistry.tags(message))
|
|
||||||
.increment();
|
|
||||||
|
|
||||||
metricRegistry
|
|
||||||
.timer(MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_DURATION, MetricRegistry.METRIC_EXECUTOR_TASKRUN_ENDED_DURATION_DESCRIPTION, metricRegistry.tags(message))
|
|
||||||
.record(taskRun.getState().getDuration());
|
|
||||||
|
|
||||||
log.trace("TaskRun terminated: {}", taskRun);
|
|
||||||
workerJobRunningRepository.deleteByKey(taskRun.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
// join worker result
|
// join worker result
|
||||||
return Pair.of(
|
return Pair.of(
|
||||||
current,
|
current,
|
||||||
@@ -1166,7 +1150,7 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Handle paused tasks
|
// Handle paused tasks
|
||||||
if (executionDelay.getDelayType().equals(ExecutionDelay.DelayType.RESUME_FLOW)) {
|
if (executionDelay.getDelayType().equals(ExecutionDelay.DelayType.RESUME_FLOW) && !pair.getLeft().getState().isTerminated()) {
|
||||||
FlowInterface flow = flowMetaStore.findByExecution(pair.getLeft()).orElseThrow();
|
FlowInterface flow = flowMetaStore.findByExecution(pair.getLeft()).orElseThrow();
|
||||||
if (executionDelay.getTaskRunId() == null) {
|
if (executionDelay.getTaskRunId() == null) {
|
||||||
// if taskRunId is null, this means we restart a flow that was delayed at startup (scheduled on)
|
// if taskRunId is null, this means we restart a flow that was delayed at startup (scheduled on)
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
|
|||||||
String remotePath = windowsToUnixPath(taskCommands.getWorkingDirectory().toString());
|
String remotePath = windowsToUnixPath(taskCommands.getWorkingDirectory().toString());
|
||||||
|
|
||||||
// first, create an archive
|
// first, create an archive
|
||||||
Path fileArchive = runContext.workingDir().createFile("inputFiles.tart");
|
Path fileArchive = runContext.workingDir().createFile("inputFiles.tar");
|
||||||
try (FileOutputStream fos = new FileOutputStream(fileArchive.toString());
|
try (FileOutputStream fos = new FileOutputStream(fileArchive.toString());
|
||||||
TarArchiveOutputStream out = new TarArchiveOutputStream(fos)) {
|
TarArchiveOutputStream out = new TarArchiveOutputStream(fos)) {
|
||||||
out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); // allow long file name
|
out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); // allow long file name
|
||||||
@@ -827,8 +827,23 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
|
|||||||
.longValue();
|
.longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getImageNameWithoutTag(String fullImageName) {
|
||||||
|
if (fullImageName == null || fullImageName.isEmpty()) {
|
||||||
|
return fullImageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastColonIndex = fullImageName.lastIndexOf(':');
|
||||||
|
int firstSlashIndex = fullImageName.indexOf('/');
|
||||||
|
if (lastColonIndex > -1 && (firstSlashIndex == -1 || lastColonIndex > firstSlashIndex)) {
|
||||||
|
return fullImageName.substring(0, lastColonIndex);
|
||||||
|
} else {
|
||||||
|
return fullImageName; // No tag found or the colon is part of the registry host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void pullImage(DockerClient dockerClient, String image, PullPolicy policy, Logger logger) {
|
private void pullImage(DockerClient dockerClient, String image, PullPolicy policy, Logger logger) {
|
||||||
NameParser.ReposTag imageParse = NameParser.parseRepositoryTag(image);
|
var imageNameWithoutTag = getImageNameWithoutTag(image);
|
||||||
|
var parsedTagFromImage = NameParser.parseRepositoryTag(image);
|
||||||
|
|
||||||
if (policy.equals(PullPolicy.IF_NOT_PRESENT)) {
|
if (policy.equals(PullPolicy.IF_NOT_PRESENT)) {
|
||||||
try {
|
try {
|
||||||
@@ -839,7 +854,9 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try (PullImageCmd pull = dockerClient.pullImageCmd(image)) {
|
// pullImageCmd without the tag (= repository) to avoid being redundant with withTag below
|
||||||
|
// and prevent errors with Podman trying to pull "image:tag:tag"
|
||||||
|
try (var pull = dockerClient.pullImageCmd(imageNameWithoutTag)) {
|
||||||
new RetryUtils().<Boolean, InternalServerErrorException>of(
|
new RetryUtils().<Boolean, InternalServerErrorException>of(
|
||||||
Exponential.builder()
|
Exponential.builder()
|
||||||
.delayFactor(2.0)
|
.delayFactor(2.0)
|
||||||
@@ -851,8 +868,8 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
|
|||||||
(bool, throwable) -> throwable instanceof InternalServerErrorException ||
|
(bool, throwable) -> throwable instanceof InternalServerErrorException ||
|
||||||
throwable.getCause() instanceof ConnectionClosedException,
|
throwable.getCause() instanceof ConnectionClosedException,
|
||||||
() -> {
|
() -> {
|
||||||
String tag = !imageParse.tag.isEmpty() ? imageParse.tag : "latest";
|
var tag = !parsedTagFromImage.tag.isEmpty() ? parsedTagFromImage.tag : "latest";
|
||||||
String repository = pull.getRepository().contains(":") ? pull.getRepository().split(":")[0] : pull.getRepository();
|
var repository = pull.getRepository().contains(":") ? pull.getRepository().split(":")[0] : pull.getRepository();
|
||||||
pull
|
pull
|
||||||
.withTag(tag)
|
.withTag(tag)
|
||||||
.exec(new PullImageResultCallback())
|
.exec(new PullImageResultCallback())
|
||||||
|
|||||||
@@ -1,12 +1,40 @@
|
|||||||
package io.kestra.plugin.scripts.runner.docker;
|
package io.kestra.plugin.scripts.runner.docker;
|
||||||
|
|
||||||
|
import io.kestra.core.models.property.Property;
|
||||||
import io.kestra.core.models.tasks.runners.AbstractTaskRunnerTest;
|
import io.kestra.core.models.tasks.runners.AbstractTaskRunnerTest;
|
||||||
import io.kestra.core.models.tasks.runners.TaskRunner;
|
import io.kestra.core.models.tasks.runners.TaskRunner;
|
||||||
|
import io.kestra.plugin.scripts.exec.scripts.runners.CommandsWrapper;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
|
||||||
class DockerTest extends AbstractTaskRunnerTest {
|
class DockerTest extends AbstractTaskRunnerTest {
|
||||||
@Override
|
@Override
|
||||||
protected TaskRunner<?> taskRunner() {
|
protected TaskRunner<?> taskRunner() {
|
||||||
return Docker.builder().image("rockylinux:9.3-minimal").build();
|
return Docker.builder().image("rockylinux:9.3-minimal").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotHaveTagInDockerPullButJustInWithTag() throws Exception {
|
||||||
|
var runContext = runContext(this.runContextFactory);
|
||||||
|
|
||||||
|
var docker = Docker.builder()
|
||||||
|
.image("ghcr.io/kestra-io/kestrapy:latest")
|
||||||
|
.pullPolicy(Property.ofValue(PullPolicy.ALWAYS))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var taskCommands = new CommandsWrapper(runContext).withCommands(Property.ofValue(List.of(
|
||||||
|
"/bin/sh", "-c",
|
||||||
|
"echo Hello World!"
|
||||||
|
)));
|
||||||
|
var result = docker.run(runContext, taskCommands, Collections.emptyList());
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getExitCode()).isZero();
|
||||||
|
Assertions.assertThat(result.getLogConsumer().getStdOutCount()).isEqualTo(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
|
|
||||||
@KestraTest
|
@KestraTest
|
||||||
public abstract class AbstractTaskRunnerTest {
|
public abstract class AbstractTaskRunnerTest {
|
||||||
@Inject private TestRunContextFactory runContextFactory;
|
@Inject protected TestRunContextFactory runContextFactory;
|
||||||
@Inject private StorageInterface storage;
|
@Inject private StorageInterface storage;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
import type {StorybookConfig} from "@storybook/vue3-vite";
|
import type {StorybookConfig} from "@storybook/vue3-vite";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: [
|
stories: [
|
||||||
"../tests/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
"../tests/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||||
],
|
],
|
||||||
addons: [
|
addons: [
|
||||||
"@storybook/addon-essentials",
|
"@storybook/addon-themes",
|
||||||
"@storybook/addon-themes",
|
"@storybook/addon-vitest",
|
||||||
"@storybook/experimental-addon-test"
|
],
|
||||||
],
|
framework: {
|
||||||
framework: {
|
name: "@storybook/vue3-vite",
|
||||||
name: "@storybook/vue3-vite",
|
options: {},
|
||||||
options: {},
|
},
|
||||||
},
|
async viteFinal(config) {
|
||||||
async viteFinal(config) {
|
const {default: viteJSXPlugin} = await import("@vitejs/plugin-vue-jsx")
|
||||||
const {default: viteJSXPlugin} = await import("@vitejs/plugin-vue-jsx")
|
config.plugins = [
|
||||||
config.plugins = [
|
...(config.plugins ?? []),
|
||||||
...(config.plugins ?? []),
|
viteJSXPlugin(),
|
||||||
viteJSXPlugin(),
|
];
|
||||||
];
|
|
||||||
return config;
|
if (config.resolve) {
|
||||||
},
|
config.resolve.alias = {
|
||||||
|
"override/services/filterLanguagesProvider": path.resolve(__dirname, "../tests/storybook/mocks/services/filterLanguagesProvider.mock.ts"),
|
||||||
|
...config.resolve?.alias
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {setup} from "@storybook/vue3";
|
import {setup} from "@storybook/vue3-vite";
|
||||||
import {withThemeByClassName} from "@storybook/addon-themes";
|
import {withThemeByClassName} from "@storybook/addon-themes";
|
||||||
import initApp from "../src/utils/init";
|
import initApp from "../src/utils/init";
|
||||||
import stores from "../src/stores/store";
|
import stores from "../src/stores/store";
|
||||||
@@ -11,7 +11,7 @@ window.KESTRA_BASE_PATH = "/ui";
|
|||||||
window.KESTRA_UI_PATH = "./";
|
window.KESTRA_UI_PATH = "./";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('@storybook/vue3').Preview}
|
* @type {import('@storybook/vue3-vite').Preview}
|
||||||
*/
|
*/
|
||||||
const preview = {
|
const preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|||||||
42
ui/.storybook/vitest.config.js
Normal file
42
ui/.storybook/vitest.config.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import {defineProject, mergeConfig} from "vitest/config";
|
||||||
|
|
||||||
|
import {storybookTest} from "@storybook/addon-vitest/vitest-plugin";
|
||||||
|
import initialConfig from "../vite.config.js"
|
||||||
|
|
||||||
|
|
||||||
|
// More info at: https://storybook.js.org/docs/writing-tests/test-addon
|
||||||
|
export default mergeConfig(
|
||||||
|
// We need to define a side first project to set up the alias for the filterLanguagesProvider mock because otherwise the `override` alias will take precedence over this one (first match rule)
|
||||||
|
defineProject({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"override/services/filterLanguagesProvider": path.resolve(__dirname, "../tests/storybook/mocks/services/filterLanguagesProvider.mock.ts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
mergeConfig(
|
||||||
|
initialConfig,
|
||||||
|
defineProject({
|
||||||
|
plugins: [
|
||||||
|
// The plugin will run tests for the stories defined in your Storybook config
|
||||||
|
// See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest
|
||||||
|
storybookTest({configDir: path.join(__dirname)}),
|
||||||
|
],
|
||||||
|
test: {
|
||||||
|
name: "storybook",
|
||||||
|
browser: {
|
||||||
|
enabled: true,
|
||||||
|
headless: true,
|
||||||
|
provider: "playwright",
|
||||||
|
instances: [{browser: "chromium"}],
|
||||||
|
},
|
||||||
|
setupFiles: ["vitest.setup.ts"],
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
"process.env.RUN_TEST_WITH_PERSISTENT": JSON.stringify("false"), // Disable persistent mode for tests
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {beforeAll} from "vitest";
|
import {beforeAll} from "vitest";
|
||||||
import {setProjectAnnotations} from "@storybook/vue3";
|
import {setProjectAnnotations} from "@storybook/vue3-vite";
|
||||||
import * as projectAnnotations from "./preview";
|
import * as projectAnnotations from "./preview";
|
||||||
|
|
||||||
// This is an important step to apply the right configuration when testing your stories.
|
// This is an important step to apply the right configuration when testing your stories.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default [
|
|||||||
"**/*.spec.ts",
|
"**/*.spec.ts",
|
||||||
"vite.config.js",
|
"vite.config.js",
|
||||||
"vitest.config.js",
|
"vitest.config.js",
|
||||||
"vitest.workspace.js",
|
".storybook/vitest.config.js",
|
||||||
],
|
],
|
||||||
languageOptions: {globals: globals.node},
|
languageOptions: {globals: globals.node},
|
||||||
},
|
},
|
||||||
|
|||||||
3124
ui/package-lock.json
generated
3124
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,22 +22,22 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@js-joda/core": "^5.6.5",
|
"@js-joda/core": "^5.6.5",
|
||||||
"@kestra-io/ui-libs": "^0.0.203",
|
"@kestra-io/ui-libs": "^0.0.205",
|
||||||
"@vue-flow/background": "^1.3.2",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@vue-flow/controls": "^1.1.2",
|
"@vue-flow/controls": "^1.1.2",
|
||||||
"@vue-flow/core": "^1.44.0",
|
"@vue-flow/core": "^1.45.0",
|
||||||
"@vueuse/core": "^13.2.0",
|
"@vueuse/core": "^13.3.0",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"bootstrap": "^5.3.6",
|
"bootstrap": "^5.3.6",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
"core-js": "^3.42.0",
|
"core-js": "^3.43.0",
|
||||||
"cronstrue": "^2.61.0",
|
"cronstrue": "^2.61.0",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"el-table-infinite-scroll": "^3.0.6",
|
"el-table-infinite-scroll": "^3.0.6",
|
||||||
"element-plus": "^2.9.10",
|
"element-plus": "^2.10.2",
|
||||||
"humanize-duration": "^3.32.2",
|
"humanize-duration": "^3.33.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
@@ -50,21 +50,21 @@
|
|||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-range": "^4.0.2",
|
"moment-range": "^4.0.2",
|
||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.46",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pdfjs-dist": "^5.2.133",
|
"pdfjs-dist": "^5.3.31",
|
||||||
"posthog-js": "^1.245.1",
|
"posthog-js": "^1.250.1",
|
||||||
"rapidoc": "^9.3.8",
|
"rapidoc": "^9.3.8",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
"shiki": "^3.4.2",
|
"shiki": "^3.6.0",
|
||||||
"splitpanes": "^3.2.0",
|
"splitpanes": "^3.2.0",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.16",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-chartjs": "^5.3.2",
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-gtag": "^2.1.0",
|
"vue-gtag": "^2.1.0",
|
||||||
"vue-i18n": "^11.1.3",
|
"vue-i18n": "^11.1.5",
|
||||||
"vue-material-design-icons": "^5.3.1",
|
"vue-material-design-icons": "^5.3.1",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-sidebar-menu": "^5.7.0",
|
"vue-sidebar-menu": "^5.7.0",
|
||||||
@@ -80,60 +80,59 @@
|
|||||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||||
"@eslint/js": "^9.27.0",
|
"@eslint/js": "^9.27.0",
|
||||||
"@rushstack/eslint-patch": "^1.11.0",
|
"@rushstack/eslint-patch": "^1.11.0",
|
||||||
"@shikijs/markdown-it": "^3.4.2",
|
"@shikijs/markdown-it": "^3.6.0",
|
||||||
"@storybook/addon-essentials": "^8.6.14",
|
"@storybook/addon-themes": "^9.0.8",
|
||||||
"@storybook/addon-themes": "^8.6.14",
|
"@storybook/addon-vitest": "^9.0.8",
|
||||||
"@storybook/blocks": "^8.6.14",
|
"@storybook/test-runner": "^0.22.1",
|
||||||
"@storybook/experimental-addon-test": "^8.6.14",
|
"@storybook/vue3-vite": "^9.0.8",
|
||||||
"@storybook/test": "^8.6.14",
|
|
||||||
"@storybook/test-runner": "^0.22.0",
|
|
||||||
"@storybook/vue3": "^8.6.14",
|
|
||||||
"@storybook/vue3-vite": "^8.6.14",
|
|
||||||
"@types/humanize-duration": "^3.27.4",
|
"@types/humanize-duration": "^3.27.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/moment": "^2.11.29",
|
||||||
"@types/path-browserify": "^1.0.3",
|
"@types/path-browserify": "^1.0.3",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@types/testing-library__jest-dom": "^5.14.9",
|
||||||
|
"@types/testing-library__user-event": "^4.1.1",
|
||||||
|
"@typescript-eslint/parser": "^8.34.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.2.0",
|
"@vitejs/plugin-vue-jsx": "^4.2.0",
|
||||||
"@vitest/browser": "^3.1.4",
|
"@vitest/browser": "^3.2.3",
|
||||||
"@vitest/coverage-v8": "^3.1.4",
|
"@vitest/coverage-v8": "^3.2.3",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vueuse/router": "^13.2.0",
|
"@vueuse/router": "^13.3.0",
|
||||||
"change-case": "4.1.2",
|
"change-case": "5.4.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-plugin-storybook": "^0.12.0",
|
"eslint-plugin-storybook": "^9.0.8",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"globals": "^16.1.0",
|
"globals": "^16.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"lint-staged": "^15.5.2",
|
"lint-staged": "^16.1.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"monaco-yaml": "5.3.1",
|
"monaco-yaml": "5.3.1",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.53.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"sass": "^1.89.0",
|
"sass": "^1.89.2",
|
||||||
"storybook": "^8.6.14",
|
"storybook": "^9.0.8",
|
||||||
"storybook-vue3-router": "^5.0.0",
|
"storybook-vue3-router": "^5.0.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.32.1",
|
"typescript-eslint": "^8.34.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vitest": "^3.1.4"
|
"vitest": "^3.2.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/darwin-arm64": "^0.25.4",
|
"@esbuild/darwin-arm64": "^0.25.5",
|
||||||
"@esbuild/darwin-x64": "^0.25.4",
|
"@esbuild/darwin-x64": "^0.25.5",
|
||||||
"@esbuild/linux-x64": "^0.25.4",
|
"@esbuild/linux-x64": "^0.25.5",
|
||||||
"@rollup/rollup-darwin-arm64": "^4.41.0",
|
"@rollup/rollup-darwin-arm64": "^4.43.0",
|
||||||
"@rollup/rollup-darwin-x64": "^4.41.0",
|
"@rollup/rollup-darwin-x64": "^4.43.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "^4.41.0",
|
"@rollup/rollup-linux-x64-gnu": "^4.43.0",
|
||||||
"@swc/core-darwin-arm64": "^1.11.24",
|
"@swc/core-darwin-arm64": "^1.12.0",
|
||||||
"@swc/core-darwin-x64": "^1.11.24",
|
"@swc/core-darwin-x64": "^1.12.0",
|
||||||
"@swc/core-linux-x64-gnu": "^1.11.24"
|
"@swc/core-linux-x64-gnu": "^1.12.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"bootstrap": {
|
"bootstrap": {
|
||||||
@@ -141,7 +140,8 @@
|
|||||||
},
|
},
|
||||||
"el-table-infinite-scroll": {
|
"el-table-infinite-scroll": {
|
||||||
"vue": "$vue"
|
"vue": "$vue"
|
||||||
}
|
},
|
||||||
|
"storybook": "$storybook"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.{js,mjs,cjs,ts,vue}": "eslint --fix"
|
"**/*.{js,mjs,cjs,ts,vue}": "eslint --fix"
|
||||||
|
|||||||
@@ -15,19 +15,15 @@
|
|||||||
<slot v-else />
|
<slot v-else />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script lang="ts" setup>
|
||||||
export default {
|
withDefaults(
|
||||||
props:{
|
defineProps<{
|
||||||
tooltip: {
|
tooltip?: string;
|
||||||
type: String,
|
placement?: string;
|
||||||
default: ""
|
}>(),{
|
||||||
},
|
tooltip: "",
|
||||||
placement:{
|
placement: "",
|
||||||
type: String,
|
});
|
||||||
default: "top"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Splitpanes class="default-theme" @resize="onResize">
|
<Splitpanes class="default-theme" v-bind="$attrs" @resize="onResize">
|
||||||
<Pane
|
<Pane
|
||||||
v-for="(panel, panelIndex) in panels"
|
v-for="(panel, panelIndex) in panels"
|
||||||
min-size="10"
|
min-size="10"
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
@dragleave.prevent
|
@dragleave.prevent
|
||||||
:data-tab-id="tab.value"
|
:data-tab-id="tab.value"
|
||||||
@click="panel.activeTab = tab"
|
@click="panel.activeTab = tab"
|
||||||
|
@mouseup="middleMouseClose($event, panelIndex, tab)"
|
||||||
>
|
>
|
||||||
<component :is="tab.button.icon" class="tab-icon" />
|
<component :is="tab.button.icon" class="tab-icon" />
|
||||||
{{ tab.button.label }}
|
{{ tab.button.label }}
|
||||||
@@ -131,10 +132,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showDropZones"
|
||||||
|
class="absolute-drop-zones-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="new-panel-drop-zone left-drop-zone"
|
||||||
|
:class="{'panel-dragover': leftPanelDragover}"
|
||||||
|
@dragover.prevent="leftPanelDragOver"
|
||||||
|
@dragleave.prevent="leftPanelDragLeave"
|
||||||
|
@drop.prevent="(e) => newPanelDrop(e, 'left')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="new-panel-drop-zone right-drop-zone"
|
||||||
|
:class="{'panel-dragover': rightPanelDragover}"
|
||||||
|
@dragover.prevent="rightPanelDragOver"
|
||||||
|
@dragleave.prevent="rightPanelDragLeave"
|
||||||
|
@drop.prevent="(e) => newPanelDrop(e, 'right')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {nextTick, ref, watch, provide} from "vue";
|
import {nextTick, ref, watch, provide, computed} from "vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
import "splitpanes/dist/splitpanes.css"
|
import "splitpanes/dist/splitpanes.css"
|
||||||
@@ -206,6 +228,15 @@
|
|||||||
const dragging = ref(false);
|
const dragging = ref(false);
|
||||||
const tabContainerRefs = ref<HTMLDivElement[]>([]);
|
const tabContainerRefs = ref<HTMLDivElement[]>([]);
|
||||||
const draggingPanel = ref<number | null>(null);
|
const draggingPanel = ref<number | null>(null);
|
||||||
|
const realDragging = ref(false);
|
||||||
|
const leftPanelDragover = ref(false);
|
||||||
|
const rightPanelDragover = ref(false);
|
||||||
|
|
||||||
|
const showDropZones = computed(() =>
|
||||||
|
realDragging.value &&
|
||||||
|
movedTabInfo.value &&
|
||||||
|
!draggingPanel.value
|
||||||
|
);
|
||||||
|
|
||||||
function onResize(e: {size:number}[]) {
|
function onResize(e: {size:number}[]) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -222,7 +253,10 @@
|
|||||||
|
|
||||||
function cleanUp(){
|
function cleanUp(){
|
||||||
dragging.value = false;
|
dragging.value = false;
|
||||||
|
realDragging.value = false;
|
||||||
mouseXRef.value = -1;
|
mouseXRef.value = -1;
|
||||||
|
leftPanelDragover.value = false;
|
||||||
|
rightPanelDragover.value = false;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
movedTabInfo.value = null
|
movedTabInfo.value = null
|
||||||
for(const panel of panels.value) {
|
for(const panel of panels.value) {
|
||||||
@@ -244,6 +278,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dragover(e: DragEvent) {
|
function dragover(e: DragEvent) {
|
||||||
|
// Ensure we set the realDragging flag when a drag operation is in progress
|
||||||
|
if (movedTabInfo.value) {
|
||||||
|
realDragging.value = true;
|
||||||
|
dragging.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
// if mouse has not moved vertically, stop the processing
|
// if mouse has not moved vertically, stop the processing
|
||||||
// this will be triggered every few ms so perf and readability will be paramount
|
// this will be triggered every few ms so perf and readability will be paramount
|
||||||
if(mouseXRef.value === e.clientX){
|
if(mouseXRef.value === e.clientX){
|
||||||
@@ -381,6 +421,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newPanelDrop(e: DragEvent, direction: "left" | "right") {
|
||||||
|
if (!movedTabInfo.value) return;
|
||||||
|
|
||||||
|
const {tab: movedTab} = movedTabInfo.value;
|
||||||
|
|
||||||
|
// Create a new panel with the dragged tab
|
||||||
|
const newPanel = {
|
||||||
|
tabs: [movedTab],
|
||||||
|
activeTab: movedTab
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the new panel based on the drop direction, not relative to original panel
|
||||||
|
if (direction === "left") {
|
||||||
|
panels.value.splice(0, 0, newPanel);
|
||||||
|
} else {
|
||||||
|
panels.value.push(newPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the tab from the original panel
|
||||||
|
// After adding the new panel, the original panel's index may have changed
|
||||||
|
// Find it again by looking for the tab in all panels
|
||||||
|
for (let i = 0; i < panels.value.length; i++) {
|
||||||
|
const panel = panels.value[i];
|
||||||
|
const tabIndex = panel.tabs.findIndex(t => t.value === movedTab.value);
|
||||||
|
|
||||||
|
if (i === 0 && direction === "left") continue;
|
||||||
|
if (i === panels.value.length - 1 && direction === "right") continue;
|
||||||
|
|
||||||
|
if (tabIndex !== -1) {
|
||||||
|
panel.tabs.splice(tabIndex, 1);
|
||||||
|
|
||||||
|
if (panel.activeTab.value === movedTab.value && panel.tabs.length > 0) {
|
||||||
|
panel.activeTab = tabIndex > 0
|
||||||
|
? panel.tabs[tabIndex - 1]
|
||||||
|
: panel.tabs[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
function closeAllTabs(panelIndex: number){
|
function closeAllTabs(panelIndex: number){
|
||||||
panels.value[panelIndex].tabs = [];
|
panels.value[panelIndex].tabs = [];
|
||||||
}
|
}
|
||||||
@@ -463,6 +546,36 @@
|
|||||||
panelsCopy.splice(newIndex, 0, movedPanel);
|
panelsCopy.splice(newIndex, 0, movedPanel);
|
||||||
panels.value = panelsCopy;
|
panels.value = panelsCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rightPanelDragOver() {
|
||||||
|
if (!movedTabInfo.value) return;
|
||||||
|
rightPanelDragover.value = true;
|
||||||
|
leftPanelDragover.value = false;
|
||||||
|
removeAllPotentialTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rightPanelDragLeave() {
|
||||||
|
rightPanelDragover.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function leftPanelDragOver() {
|
||||||
|
if (!movedTabInfo.value) return;
|
||||||
|
leftPanelDragover.value = true;
|
||||||
|
rightPanelDragover.value = false;
|
||||||
|
removeAllPotentialTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function leftPanelDragLeave() {
|
||||||
|
leftPanelDragover.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function middleMouseClose(event:MouseEvent, panelIndex:number, tab: Tab) {
|
||||||
|
// Middle mouse button
|
||||||
|
if (event.button === 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
destroyTab(panelIndex, tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -620,4 +733,46 @@
|
|||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.absolute-drop-zones-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-panel-drop-zone {
|
||||||
|
position: relative;
|
||||||
|
width: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(30, 30, 30, 0.5);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 2px dashed var(--ks-border-primary, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 8px;
|
||||||
|
pointer-events: auto;
|
||||||
|
height: calc(100% - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-panel-drop-zone:hover,
|
||||||
|
.new-panel-drop-zone.panel-dragover {
|
||||||
|
background-color: rgba(40, 40, 40, 0.8);
|
||||||
|
border-color: var(--ks-border-active, #888);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-drop-zone {
|
||||||
|
border-right-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-drop-zone {
|
||||||
|
border-left-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
<section v-if="isEditorActiveTab || activeTab.component" data-component="FILENAME_PLACEHOLDER#container" ref="container" v-bind="$attrs" :class="{...containerClass, 'maximized': activeTab.maximized}">
|
||||||
<section v-if="isEditorActiveTab || activeTab.component" data-component="FILENAME_PLACEHOLDER#container" ref="container" v-bind="$attrs" :class="{...containerClass, 'd-flex flex-row': isEditorActiveTab, 'maximized': activeTab.maximized}">
|
|
||||||
<EditorSidebar v-if="isEditorActiveTab" ref="sidebar" :style="`flex: 0 0 calc(${explorerWidth}% - 11px);`" :current-n-s="namespace" v-show="explorerVisible" />
|
<EditorSidebar v-if="isEditorActiveTab" ref="sidebar" :style="`flex: 0 0 calc(${explorerWidth}% - 11px);`" :current-n-s="namespace" v-show="explorerVisible" />
|
||||||
<div v-if="isEditorActiveTab && explorerVisible" @mousedown.prevent.stop="dragSidebar" class="slider" />
|
<div v-if="isEditorActiveTab && explorerVisible" @mousedown.prevent.stop="dragSidebar" class="slider" />
|
||||||
<div v-if="isEditorActiveTab" :style="`flex: 1 1 ${100 - (isEditorActiveTab && explorerVisible ? explorerWidth : 0)}%;`">
|
<div v-if="isEditorActiveTab" :style="`flex: 1 1 ${100 - (isEditorActiveTab && explorerVisible ? explorerWidth : 0)}%;`">
|
||||||
@@ -246,7 +245,6 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-tabs__nav-next),
|
:deep(.el-tabs__nav-next),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-100 overflow-y-auto no-code">
|
<div class="no-code">
|
||||||
<Breadcrumbs />
|
<Breadcrumbs />
|
||||||
|
|
||||||
<hr class="m-0">
|
<hr class="m-0">
|
||||||
@@ -19,11 +19,11 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BREADCRUMB_INJECTION_KEY, CLOSE_TASK_FUNCTION_INJECTION_KEY,
|
BREADCRUMB_INJECTION_KEY, CLOSE_TASK_FUNCTION_INJECTION_KEY,
|
||||||
CREATE_TASK_FUNCTION_INJECTION_KEY, CREATING_TASK_INJECTION_KEY,
|
CREATING_TASK_INJECTION_KEY, BLOCKTYPE_INJECT_KEY,
|
||||||
EDIT_TASK_FUNCTION_INJECTION_KEY, BLOCKTYPE_INJECT_KEY,
|
|
||||||
PANEL_INJECTION_KEY, POSITION_INJECTION_KEY,
|
PANEL_INJECTION_KEY, POSITION_INJECTION_KEY,
|
||||||
REF_PATH_INJECTION_KEY, PARENT_PATH_INJECTION_KEY,
|
REF_PATH_INJECTION_KEY, PARENT_PATH_INJECTION_KEY,
|
||||||
FLOW_INJECTION_KEY,
|
FLOW_INJECTION_KEY,
|
||||||
|
EDITING_TASK_INJECTION_KEY,
|
||||||
} from "./injectionKeys";
|
} from "./injectionKeys";
|
||||||
import Breadcrumbs from "./components/Breadcrumbs.vue";
|
import Breadcrumbs from "./components/Breadcrumbs.vue";
|
||||||
import Editor from "./segments/Editor.vue";
|
import Editor from "./segments/Editor.vue";
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
(e: "updateMetadata", value: {[key: string]: any}): void
|
(e: "updateMetadata", value: {[key: string]: any}): void
|
||||||
(e: "reorder", yaml: string): void
|
(e: "reorder", yaml: string): void
|
||||||
(e: "createTask", blockType: string, parentPath: string, refPath: number | undefined, position?: "before" | "after"): boolean | void
|
(e: "createTask", blockType: string, parentPath: string, refPath: number | undefined, position?: "before" | "after"): boolean | void
|
||||||
(e: "editTask", blockType: string, parentPath: string, refPath: number): boolean | void
|
(e: "editTask", blockType: string, parentPath: string, refPath?: number): boolean | void
|
||||||
(e: "closeTask"): boolean | void
|
(e: "closeTask"): boolean | void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -55,9 +55,11 @@
|
|||||||
*/
|
*/
|
||||||
refPath?: number;
|
refPath?: number;
|
||||||
creatingTask?: boolean;
|
creatingTask?: boolean;
|
||||||
|
editingTask?: boolean;
|
||||||
position?: "before" | "after";
|
position?: "before" | "after";
|
||||||
}>(), {
|
}>(), {
|
||||||
creatingTask: false,
|
creatingTask: false,
|
||||||
|
editingTask: false,
|
||||||
position: "after",
|
position: "after",
|
||||||
refPath: undefined,
|
refPath: undefined,
|
||||||
blockType: undefined,
|
blockType: undefined,
|
||||||
@@ -66,7 +68,6 @@
|
|||||||
|
|
||||||
const metadata = computed(() => YAML_UTILS.getMetadata(props.flow));
|
const metadata = computed(() => YAML_UTILS.getMetadata(props.flow));
|
||||||
|
|
||||||
const creatingTaskRef = ref(props.creatingTask)
|
|
||||||
const breadcrumbs = ref<Breadcrumb[]>([])
|
const breadcrumbs = ref<Breadcrumb[]>([])
|
||||||
const panel = ref()
|
const panel = ref()
|
||||||
|
|
||||||
@@ -77,13 +78,9 @@
|
|||||||
provide(BREADCRUMB_INJECTION_KEY, breadcrumbs);
|
provide(BREADCRUMB_INJECTION_KEY, breadcrumbs);
|
||||||
provide(BLOCKTYPE_INJECT_KEY, props.blockType);
|
provide(BLOCKTYPE_INJECT_KEY, props.blockType);
|
||||||
provide(POSITION_INJECTION_KEY, props.position);
|
provide(POSITION_INJECTION_KEY, props.position);
|
||||||
provide(CREATING_TASK_INJECTION_KEY, computed(() => creatingTaskRef.value));
|
provide(CREATING_TASK_INJECTION_KEY, props.creatingTask);
|
||||||
provide(CREATE_TASK_FUNCTION_INJECTION_KEY, (blockType, parentPath, refPath) => {
|
provide(EDITING_TASK_INJECTION_KEY, props.editingTask);
|
||||||
emit("createTask", blockType, parentPath, refPath)
|
|
||||||
});
|
|
||||||
provide(EDIT_TASK_FUNCTION_INJECTION_KEY, (blockType, parentPath, refPath) => {
|
|
||||||
emit("editTask", blockType, parentPath, refPath)
|
|
||||||
});
|
|
||||||
provide(CLOSE_TASK_FUNCTION_INJECTION_KEY, () => {
|
provide(CLOSE_TASK_FUNCTION_INJECTION_KEY, () => {
|
||||||
if (breadcrumbs.value[breadcrumbs.value.length - 1].component) {
|
if (breadcrumbs.value[breadcrumbs.value.length - 1].component) {
|
||||||
breadcrumbs.value.pop();
|
breadcrumbs.value.pop();
|
||||||
@@ -95,4 +92,13 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss" src="./styles/code.scss" />
|
<style lang="scss" scoped>
|
||||||
|
.no-code {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<NoCode
|
||||||
<NoCode
|
:flow="lastValidFlowYaml"
|
||||||
:flow="lastValidFlowYaml"
|
:parent-path="parentPath"
|
||||||
:parent-path="parentPath"
|
:ref-path="refPath"
|
||||||
:ref-path="refPath"
|
:block-type="blockType"
|
||||||
:block-type="blockType"
|
:creating-task="creatingTask"
|
||||||
:creating-task="creatingTask"
|
:editing-task="editingTask"
|
||||||
:position
|
:position
|
||||||
@update-metadata="(e) => onUpdateMetadata(e)"
|
@update-metadata="(e) => onUpdateMetadata(e)"
|
||||||
@update-task="(e) => editorUpdate(e)"
|
@update-task="(e) => editorUpdate(e)"
|
||||||
@reorder="(yaml) => handleReorder(yaml)"
|
@reorder="(yaml) => handleReorder(yaml)"
|
||||||
@create-task="(blockType, parentPath, refPath) => emit('createTask', blockType, parentPath, refPath, 'after')"
|
@close-task="() => emit('closeTask')"
|
||||||
@close-task="() => emit('closeTask')"
|
/>
|
||||||
@edit-task="(blockType, parentPath, refPath) => emit('editTask', blockType, parentPath, refPath)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref} from "vue";
|
import {computed, provide, ref} from "vue";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
import NoCode from "./NoCode.vue";
|
import NoCode from "./NoCode.vue";
|
||||||
import {BlockType} from "./utils/types";
|
import {BlockType} from "./utils/types";
|
||||||
|
import {CREATE_TASK_FUNCTION_INJECTION_KEY, EDIT_TASK_FUNCTION_INJECTION_KEY} from "./injectionKeys";
|
||||||
|
|
||||||
export interface NoCodeProps {
|
export interface NoCodeProps {
|
||||||
creatingTask?: boolean;
|
creatingTask?: boolean;
|
||||||
|
editingTask?: boolean;
|
||||||
blockType?: BlockType | "pluginDefaults";
|
blockType?: BlockType | "pluginDefaults";
|
||||||
parentPath?: string;
|
parentPath?: string;
|
||||||
refPath?: number;
|
refPath?: number;
|
||||||
@@ -37,10 +36,17 @@
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "createTask", blockType: string, parentPath: string, refPath: number | undefined, position: "after" | "before"): boolean | void;
|
(e: "createTask", blockType: string, parentPath: string, refPath: number | undefined, position: "after" | "before"): boolean | void;
|
||||||
(e: "editTask", blockType: string, parentPath: string, refPath: number): boolean | void;
|
(e: "editTask", blockType: string, parentPath: string, refPath?: number): boolean | void;
|
||||||
(e: "closeTask"): boolean | void;
|
(e: "closeTask"): boolean | void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
provide(CREATE_TASK_FUNCTION_INJECTION_KEY, (blockType, parentPath, refPath) => {
|
||||||
|
emit("createTask", blockType, parentPath, refPath, "after")
|
||||||
|
});
|
||||||
|
provide(EDIT_TASK_FUNCTION_INJECTION_KEY, (blockType, parentPath, refPath) => {
|
||||||
|
emit("editTask", blockType, parentPath, refPath)
|
||||||
|
});
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const flowYaml = computed<string>(() => store.getters["flow/flowYaml"]);
|
const flowYaml = computed<string>(() => store.getters["flow/flowYaml"]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="emits('add', props.what)" class="py-2 adding">
|
<button @click="emit('add', what)" class="py-2 adding" type="button">
|
||||||
{{
|
{{
|
||||||
props.what
|
what
|
||||||
? t("no_code.adding", {what: props.what})
|
? t("no_code.adding", {what})
|
||||||
: t("no_code.adding_default")
|
: t("no_code.adding_default")
|
||||||
}}
|
}}
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
|
|
||||||
const emits = defineEmits(["add"]);
|
const emit = defineEmits<{
|
||||||
const props = defineProps({what: {type: String, default: undefined}});
|
(e: "add", what: string | undefined): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
what?: string;
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "../styles/code.scss";
|
@import "../styles/code.scss";
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.adding {
|
.adding {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--ks-content-secondary);
|
color: var(--ks-content-secondary);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
const breadcrumbs = inject(BREADCRUMB_INJECTION_KEY, ref([]));
|
const breadcrumbs = inject(BREADCRUMB_INJECTION_KEY, ref([]));
|
||||||
const flowYaml = inject(FLOW_INJECTION_KEY, ref(""));
|
const flowYaml = inject(FLOW_INJECTION_KEY, ref(""));
|
||||||
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
||||||
const taskCreation = inject(CREATING_TASK_INJECTION_KEY, ref(false));
|
const taskCreation = inject(CREATING_TASK_INJECTION_KEY, false);
|
||||||
const blockType = inject(BLOCKTYPE_INJECT_KEY, undefined);
|
const blockType = inject(BLOCKTYPE_INJECT_KEY, undefined);
|
||||||
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
|
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
|
||||||
|
|
||||||
@@ -49,11 +49,11 @@
|
|||||||
label: parentPath,
|
label: parentPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(taskCreation.value || (refPath?.length && refPath.length > 0)){
|
if(taskCreation || (refPath !== undefined && refPath >= 0)) {
|
||||||
breadcrumbs.value[index] = {
|
breadcrumbs.value[index] = {
|
||||||
label: taskCreation.value
|
label: taskCreation
|
||||||
? t(`no_code.creation.${blockType}`)
|
? t(`no_code.creation.${blockType}`)
|
||||||
: refPath ?? ""
|
: refPath?.toString() ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,13 +61,13 @@
|
|||||||
|
|
||||||
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
|
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
|
||||||
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
||||||
const creatingTask = inject(CREATING_TASK_INJECTION_KEY, ref(false));
|
const creatingTask = inject(CREATING_TASK_INJECTION_KEY, false);
|
||||||
|
|
||||||
const parentPathComplete = computed(() => {
|
const parentPathComplete = computed(() => {
|
||||||
return `${[
|
return `${[
|
||||||
[
|
[
|
||||||
parentPath,
|
parentPath,
|
||||||
creatingTask.value && refPath !== undefined
|
creatingTask && refPath !== undefined
|
||||||
? `[${refPath + 1}]`
|
? `[${refPath + 1}]`
|
||||||
: refPath !== undefined
|
: refPath !== undefined
|
||||||
? `[${refPath}]`
|
? `[${refPath}]`
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
class="border-0"
|
class="border-0"
|
||||||
/>
|
/>
|
||||||
<div v-if="blockType !== 'pluginDefaults'" class="d-flex flex-column">
|
<div v-if="blockType !== 'pluginDefaults' && elementIndex !== undefined" class="d-flex flex-column">
|
||||||
<ChevronUp @click.prevent.stop="emits('moveElement', 'up')" />
|
<ChevronUp @click.prevent.stop="emits('moveElement', 'up')" />
|
||||||
<ChevronDown @click.prevent.stop="emits('moveElement', 'down')" />
|
<ChevronDown @click.prevent.stop="emits('moveElement', 'down')" />
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
elementIndex: number;
|
elementIndex?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<span v-if="required" class="me-1 text-danger">*</span>
|
<span v-if="required" class="me-1 text-danger">*</span>
|
||||||
<span v-if="label" class="label">{{ label }}</span>
|
<span v-if="label" class="label">{{ label }}</span>
|
||||||
|
<el-alert
|
||||||
|
v-if="alertState.visible"
|
||||||
|
:title="alertState.message"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
<div class="mt-1 mb-2 w-100 wrapper">
|
<div class="mt-1 mb-2 w-100 wrapper">
|
||||||
<el-row
|
<el-row
|
||||||
v-for="(value, key, index) in props.modelValue"
|
v-for="(pair, index) in internalPairs"
|
||||||
:key="index"
|
:key="index"
|
||||||
:gutter="10"
|
:gutter="10"
|
||||||
>
|
>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<InputText
|
<InputText
|
||||||
:model-value="key"
|
:model-value="pair[0]"
|
||||||
:placeholder="t('key')"
|
:placeholder="t('key')"
|
||||||
@update:model-value="(changed) => updateKey(key, changed)"
|
@update:model-value="(changed) => handleKeyInput(index, changed)"
|
||||||
|
:have-error="duplicatedKeys.includes(pair[0])"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="16" class="d-flex">
|
<el-col :span="16" class="d-flex">
|
||||||
<InputText
|
<InputText
|
||||||
:model-value="value"
|
:model-value="pair[1]"
|
||||||
:placeholder="t('value')"
|
:placeholder="t('value')"
|
||||||
@update:model-value="(changed) => updateValue(key, changed)"
|
@update:model-value="(changed) => updateValue(index, changed)"
|
||||||
class="w-100 me-2"
|
class="w-100 me-2"
|
||||||
/>
|
/>
|
||||||
<DeleteOutline @click="removePair(key)" class="delete" />
|
<DeleteOutline @click="removePair(index)" class="delete" />
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
@@ -30,8 +39,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {PropType} from "vue";
|
import {watch, computed, ref} from "vue";
|
||||||
|
|
||||||
import {PairField} from "../../utils/types";
|
import {PairField} from "../../utils/types";
|
||||||
|
|
||||||
import {DeleteOutline} from "../../utils/icons";
|
import {DeleteOutline} from "../../utils/icons";
|
||||||
@@ -47,56 +55,78 @@
|
|||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
const props = defineProps({
|
const props = defineProps<{
|
||||||
modelValue: {
|
modelValue?: PairField["value"],
|
||||||
type: Object as PropType<PairField["value"]>,
|
label?: string,
|
||||||
default: undefined,
|
property?: string,
|
||||||
},
|
required?: boolean
|
||||||
label: {type: String, default: undefined},
|
}>();
|
||||||
property: {type: String, default: undefined},
|
|
||||||
required: {type: Boolean, default: false},
|
const internalPairs = ref<[string, string | undefined][]>([])
|
||||||
|
|
||||||
|
// this flag will avoid updating the modelValue when the
|
||||||
|
// change was initiated in the component itself
|
||||||
|
const localEdit = ref(false);
|
||||||
|
|
||||||
|
const duplicatedKeys = computed(() => {
|
||||||
|
return internalPairs.value.map(pair => pair[0])
|
||||||
|
.filter((key, index, self) =>
|
||||||
|
self.indexOf(key) !== index
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const addPair = () => {
|
const alertState = computed(() => {
|
||||||
emits("update:modelValue", {...props.modelValue, "": ""});
|
if(duplicatedKeys.value.length > 0){
|
||||||
};
|
return {
|
||||||
const removePair = (key: any) => {
|
visible: true,
|
||||||
const values = {...props.modelValue};
|
message: t("duplicate-pair", {label: props.label ?? t("key"), key: duplicatedKeys.value[0]}),
|
||||||
delete values[key];
|
}
|
||||||
|
|
||||||
emits("update:modelValue", values);
|
|
||||||
};
|
|
||||||
const updateKey = (old, changed) => {
|
|
||||||
const values = {...props.modelValue};
|
|
||||||
|
|
||||||
// Create an array of key-value pairs and preserve order
|
|
||||||
const entries = Object.entries(values);
|
|
||||||
|
|
||||||
// Find the index of the old key
|
|
||||||
const index = entries.findIndex(([key]) => key === old);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
// Get the value of the old key
|
|
||||||
const [, value] = entries[index];
|
|
||||||
|
|
||||||
// Remove the old key from the entries
|
|
||||||
entries.splice(index, 1);
|
|
||||||
|
|
||||||
// Add the new key with the same value
|
|
||||||
entries.splice(index, 0, [changed, value]);
|
|
||||||
|
|
||||||
// Rebuild the object while keeping the order
|
|
||||||
const updatedValues = Object.fromEntries(entries);
|
|
||||||
|
|
||||||
// Emit the updated object
|
|
||||||
emits("update:modelValue", updatedValues);
|
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
message: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
// If the alert is visible, we don't want to update the pairs
|
||||||
|
// because it would delete problem line silently.
|
||||||
|
if (alertState.value.visible || localEdit.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localEdit.value = false;
|
||||||
|
internalPairs.value = Object.entries(newValue || {});
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function updateModel() {
|
||||||
|
localEdit.value = true;
|
||||||
|
emit("update:modelValue", Object.fromEntries(internalPairs.value.filter(pair => pair[0] !== "" && pair[1] !== undefined)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyInput(index: number, newValue: string) {
|
||||||
|
internalPairs.value[index][0] = newValue.toString();
|
||||||
|
updateModel()
|
||||||
};
|
};
|
||||||
const updateValue = (key, value) => {
|
|
||||||
const values = {...props.modelValue};
|
function addPair() {
|
||||||
values[key] = value;
|
internalPairs.value.push(["", undefined])
|
||||||
emits("update:modelValue", values);
|
updateModel()
|
||||||
|
};
|
||||||
|
|
||||||
|
function removePair (pairId: number) {
|
||||||
|
internalPairs.value.splice(pairId, 1);
|
||||||
|
updateModel()
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateValue (pairId: number, newValue: string){
|
||||||
|
internalPairs.value[pairId][1] = newValue;
|
||||||
|
updateModel()
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
:placeholder
|
:placeholder
|
||||||
:disabled
|
:disabled
|
||||||
:type="disabled ? '' : 'textarea'"
|
:type="disabled ? '' : 'textarea'"
|
||||||
:suffix-icon="Lock"
|
|
||||||
:autosize="{minRows: 1}"
|
:autosize="{minRows: 1}"
|
||||||
|
:input-style="haveError ? {boxShadow: '0 0 6px #ab0009'} : {}"
|
||||||
|
:suffix-icon="disabled ? Lock : undefined"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,18 +32,20 @@
|
|||||||
disabled: {type: Boolean, default: false},
|
disabled: {type: Boolean, default: false},
|
||||||
margin: {type: String, default: "mt-1 mb-2"},
|
margin: {type: String, default: "mt-1 mb-2"},
|
||||||
class: {type: String, default: undefined},
|
class: {type: String, default: undefined},
|
||||||
|
haveError: {type: Boolean, default: false}
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = computed({
|
const input = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
emits("update:modelValue", value);
|
emits("update:modelValue", value);
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "../../styles/code.scss";
|
@import "../../styles/code.scss";
|
||||||
|
|
||||||
:deep(.el-input__icon) {
|
:deep(.el-input__icon) {
|
||||||
.lock-icon {
|
.lock-icon {
|
||||||
color: var(--ks-content-inactive);
|
color: var(--ks-content-inactive);
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export const POSITION_INJECTION_KEY = Symbol("position-injection-key") as Inject
|
|||||||
* Tells if the task is being created or edited. Used to discriminate when a section is specified
|
* Tells if the task is being created or edited. Used to discriminate when a section is specified
|
||||||
* NOTE: different from the `isCreating` flag coming from the store. `isCreating` refers to the Complete flow being in creation
|
* NOTE: different from the `isCreating` flag coming from the store. `isCreating` refers to the Complete flow being in creation
|
||||||
*/
|
*/
|
||||||
export const CREATING_TASK_INJECTION_KEY = Symbol("creating-injection-key") as InjectionKey<ComputedRef<boolean>>
|
export const CREATING_TASK_INJECTION_KEY = Symbol("creating-injection-key") as InjectionKey<boolean>
|
||||||
|
export const EDITING_TASK_INJECTION_KEY = Symbol("editing-injection-key") as InjectionKey<boolean>
|
||||||
/**
|
/**
|
||||||
* Call this when starting to create a new task, when the user clicks on the add button
|
* Call this when starting to create a new task, when the user clicks on the add button
|
||||||
* to start the addition process
|
* to start the addition process
|
||||||
@@ -36,7 +37,7 @@ export const CREATE_TASK_FUNCTION_INJECTION_KEY = Symbol("creating-function-inje
|
|||||||
* Call this when starting to edit a task, when the user clicks on the task line
|
* Call this when starting to edit a task, when the user clicks on the task line
|
||||||
* to start the edition process
|
* to start the edition process
|
||||||
*/
|
*/
|
||||||
export const EDIT_TASK_FUNCTION_INJECTION_KEY = Symbol("edit-function-injection-key") as InjectionKey<(blockType: BlockType | "pluginDefaults", parentPath: string, refPath: number) => void>
|
export const EDIT_TASK_FUNCTION_INJECTION_KEY = Symbol("edit-function-injection-key") as InjectionKey<(blockType: BlockType | "pluginDefaults", parentPath: string, refPath?: number) => void>
|
||||||
/**
|
/**
|
||||||
* Call this when closing a task, when the user clicks on the close button
|
* Call this when closing a task, when the user clicks on the close button
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<template v-if="panel">
|
<template v-if="panel">
|
||||||
<component
|
<MetadataInputsContent
|
||||||
:is="panel.type"
|
:inputs="metadata.inputs"
|
||||||
:model-value="panel.props.modelValue"
|
:label="t('inputs')"
|
||||||
v-bind="panel.props"
|
:selected-index="panel.props.selectedIndex"
|
||||||
@update:model-value="
|
@update:inputs="
|
||||||
(value: any) => emits('updateMetadata', 'inputs', value)
|
(value: any) => emits('updateMetadata', 'inputs', value)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="!creatingTask && refPath === undefined">
|
<template v-else-if="!creatingTask && !editingTask">
|
||||||
<el-form label-position="top">
|
<el-form label-position="top" v-if="fieldsFromSchema.length">
|
||||||
<component
|
<TaskWrapper :key="v.root" v-for="(v) in fieldsFromSchema.slice(0, 3)" :merge="shouldMerge(v.schema)">
|
||||||
v-for="(v, k) in mainFields"
|
<template #tasks>
|
||||||
:key="k"
|
<TaskObjectField
|
||||||
:is="v.component"
|
v-bind="v"
|
||||||
v-model="v.value"
|
@update:model-value="updateMetadata(v.root, $event)"
|
||||||
v-bind="trimmed(v)"
|
/>
|
||||||
@update:model-value="emits('updateMetadata', k, v.value)"
|
</template>
|
||||||
|
</TaskWrapper>
|
||||||
|
|
||||||
|
<MetadataInputs
|
||||||
|
v-if="flowSchemaProperties.inputs"
|
||||||
|
:label="t('no_code.fields.general.inputs')"
|
||||||
|
:model-value="metadata.inputs"
|
||||||
|
:required="flowSchema.required?.includes('inputs')"
|
||||||
|
@update:model-value="updateMetadata('inputs', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
@@ -34,15 +42,34 @@
|
|||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
||||||
<component
|
<TaskWrapper :key="v.root" v-for="(v) in fieldsFromSchema.slice(4)" :merge="shouldMerge(v.schema)">
|
||||||
v-for="(v, k) in otherFields"
|
<template #tasks>
|
||||||
:key="k"
|
<TaskObjectField
|
||||||
:is="v.component"
|
v-bind="v"
|
||||||
v-model="v.value"
|
@update:model-value="updateMetadata(v.root, $event)"
|
||||||
v-bind="trimmed(v)"
|
/>
|
||||||
@update:model-value="emits('updateMetadata', k, v.value)"
|
</template>
|
||||||
/>
|
</TaskWrapper>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
<template v-else>
|
||||||
|
<el-skeleton
|
||||||
|
animated
|
||||||
|
:rows="4"
|
||||||
|
:throttle="{leading: 500, initVal: true}"
|
||||||
|
/>
|
||||||
|
<hr class="my-4">
|
||||||
|
<el-skeleton
|
||||||
|
animated
|
||||||
|
:rows="6"
|
||||||
|
:throttle="{leading: 500, initVal: true}"
|
||||||
|
/>
|
||||||
|
<hr class="my-4">
|
||||||
|
<el-skeleton
|
||||||
|
animated
|
||||||
|
:rows="5"
|
||||||
|
:throttle="{leading: 500, initVal: true}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Task
|
<Task
|
||||||
@@ -56,34 +83,34 @@
|
|||||||
import {onMounted, computed, inject, ref} from "vue";
|
import {onMounted, computed, inject, ref} from "vue";
|
||||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
|
|
||||||
import {Field, Fields, CollapseItem, NoCodeElement, BlockType} from "../utils/types";
|
import {CollapseItem, NoCodeElement, BlockType} from "../utils/types";
|
||||||
|
|
||||||
import Collapse from "../components/collapse/Collapse.vue";
|
import Collapse from "../components/collapse/Collapse.vue";
|
||||||
import InputText from "../components/inputs/InputText.vue";
|
|
||||||
import InputSwitch from "../components/inputs/InputSwitch.vue";
|
|
||||||
import InputPair from "../components/inputs/InputPair.vue";
|
|
||||||
|
|
||||||
import Editor from "../../inputs/Editor.vue";
|
|
||||||
import MetadataInputs from "../../flows/MetadataInputs.vue";
|
import MetadataInputs from "../../flows/MetadataInputs.vue";
|
||||||
import MetadataRetry from "../../flows/MetadataRetry.vue";
|
import MetadataInputsContent from "../../flows/MetadataInputsContent.vue";
|
||||||
import MetadataSLA from "../../flows/MetadataSLA.vue";
|
import TaskObjectField from "../../flows/tasks/TaskObjectField.vue";
|
||||||
import TaskBasic from "../../flows/tasks/TaskBasic.vue";
|
import InitialSchema from "./flow-schema.json"
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CREATING_TASK_INJECTION_KEY, FLOW_INJECTION_KEY,
|
CREATING_TASK_INJECTION_KEY, EDITING_TASK_INJECTION_KEY,
|
||||||
PANEL_INJECTION_KEY, REF_PATH_INJECTION_KEY,
|
FLOW_INJECTION_KEY, PANEL_INJECTION_KEY,
|
||||||
} from "../injectionKeys";
|
} from "../injectionKeys";
|
||||||
|
|
||||||
import Task from "./Task.vue";
|
import Task from "./Task.vue";
|
||||||
|
|
||||||
const panel = inject(PANEL_INJECTION_KEY, ref());
|
const panel = inject(PANEL_INJECTION_KEY, ref());
|
||||||
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
|
||||||
|
const editingTask = inject(EDITING_TASK_INJECTION_KEY, false);
|
||||||
|
|
||||||
|
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
|
|
||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
|
import TaskWrapper from "../../flows/tasks/TaskWrapper.vue";
|
||||||
|
import {removeNullAndUndefined} from "../utils/cleanUp";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const emits = defineEmits([
|
const emits = defineEmits([
|
||||||
@@ -100,6 +127,22 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function shouldMerge(schema: any): boolean {
|
||||||
|
const complexObject = ["object", "array"].includes(schema?.type) || schema?.$ref || schema?.oneOf || schema?.anyOf || schema?.allOf;
|
||||||
|
return !complexObject
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMetadata(key: string, val: any) {
|
||||||
|
const realValue = val === null || val === undefined ? undefined :
|
||||||
|
// allow array to be created with null values (specifically for metadata)
|
||||||
|
// metadata do not use a buffer value, so each change needs to be reflected in the code,
|
||||||
|
// for TaskKvPair.vue (object) we added the buffer value in the input component
|
||||||
|
typeof val === "object" && !Array.isArray(val) ? removeNullAndUndefined(val) :
|
||||||
|
val; // Handle null values
|
||||||
|
|
||||||
|
emits("updateMetadata", key, realValue);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", saveEvent);
|
document.addEventListener("keydown", saveEvent);
|
||||||
|
|
||||||
const creatingFlow = computed(() => {
|
const creatingFlow = computed(() => {
|
||||||
@@ -113,111 +156,76 @@
|
|||||||
metadata: {type: Object, required: true},
|
metadata: {type: Object, required: true},
|
||||||
});
|
});
|
||||||
|
|
||||||
const trimmed = (field: Field) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const {component, value, ...rest} = field;
|
|
||||||
|
|
||||||
return rest;
|
|
||||||
};
|
|
||||||
|
|
||||||
function onTaskUpdate(yaml: string) {
|
function onTaskUpdate(yaml: string) {
|
||||||
emits("updateTask", yaml)
|
emits("updateTask", yaml)
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = ref({})
|
const schema = ref<{
|
||||||
|
definitions?: any,
|
||||||
|
$ref?: string,
|
||||||
|
}>(InitialSchema)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.dispatch("plugin/loadSchemaType").then((response) => {
|
await store.dispatch("plugin/loadSchemaType").then((response) => {
|
||||||
schema.value = response;
|
schema.value = response;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const fields = computed<Fields>(() => {
|
const definitions = computed(() => {
|
||||||
return {
|
return schema.value?.definitions ?? {};
|
||||||
id: {
|
});
|
||||||
component: InputText,
|
function removeRefPrefix(ref?: string): string {
|
||||||
value: props.metadata.id,
|
return ref?.replace(/^#\/definitions\//, "") ?? "";
|
||||||
label: t("no_code.fields.main.flow_id"),
|
}
|
||||||
required: true,
|
|
||||||
disabled: !creatingFlow.value,
|
const flowSchema = computed(() => {
|
||||||
},
|
const ref = removeRefPrefix(schema.value?.$ref);
|
||||||
namespace: {
|
return definitions.value?.[ref];
|
||||||
component: InputText,
|
|
||||||
value: props.metadata.namespace,
|
|
||||||
label: t("no_code.fields.main.namespace"),
|
|
||||||
required: true,
|
|
||||||
disabled: !creatingFlow.value,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
component: InputText,
|
|
||||||
value: props.metadata.description,
|
|
||||||
label: t("no_code.fields.main.description"),
|
|
||||||
},
|
|
||||||
retry: {
|
|
||||||
component: MetadataRetry,
|
|
||||||
value: props.metadata.retry,
|
|
||||||
label: t("no_code.fields.general.retry")
|
|
||||||
},
|
|
||||||
labels: {
|
|
||||||
component: InputPair,
|
|
||||||
value: props.metadata.labels,
|
|
||||||
label: t("no_code.fields.general.labels"),
|
|
||||||
property: t("no_code.labels.label"),
|
|
||||||
},
|
|
||||||
inputs: {
|
|
||||||
component: MetadataInputs,
|
|
||||||
value: props.metadata.inputs,
|
|
||||||
label: t("no_code.fields.general.inputs"),
|
|
||||||
inputs: props.metadata.inputs ?? [],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
component: Editor,
|
|
||||||
value: props.metadata.outputs,
|
|
||||||
label: t("no_code.fields.general.outputs"),
|
|
||||||
navbar: false,
|
|
||||||
input: true,
|
|
||||||
lang: "yaml",
|
|
||||||
shouldFocus: false,
|
|
||||||
showScroll: true,
|
|
||||||
style: {height: "100px"},
|
|
||||||
},
|
|
||||||
variables: {
|
|
||||||
component: InputPair,
|
|
||||||
value: props.metadata.variables,
|
|
||||||
label: t("no_code.fields.general.variables"),
|
|
||||||
property: t("no_code.labels.variable"),
|
|
||||||
},
|
|
||||||
concurrency: {
|
|
||||||
component: TaskBasic,
|
|
||||||
value: props.metadata.concurrency,
|
|
||||||
label: t("no_code.fields.general.concurrency"),
|
|
||||||
schema: schema.value?.definitions?.["io.kestra.core.models.flows.Concurrency"] ?? {},
|
|
||||||
root: "concurrency",
|
|
||||||
},
|
|
||||||
sla: {
|
|
||||||
component: MetadataSLA,
|
|
||||||
value: props.metadata.sla ?? [],
|
|
||||||
label: t("no_code.fields.general.sla")
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
component: InputSwitch,
|
|
||||||
value: props.metadata.disabled,
|
|
||||||
label: t("no_code.fields.general.disabled"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainFields = computed(() => {
|
const flowSchemaProperties = computed(() => {
|
||||||
const {id, namespace, description, inputs} = fields.value;
|
return flowSchema.value?.properties ?? {};
|
||||||
|
});
|
||||||
|
|
||||||
return {id, namespace, description, inputs};
|
const METADATA_KEYS = [
|
||||||
})
|
"id",
|
||||||
|
"namespace",
|
||||||
|
"description",
|
||||||
|
"inputs",
|
||||||
|
"retry",
|
||||||
|
"labels",
|
||||||
|
"outputs",
|
||||||
|
"variables",
|
||||||
|
"concurrency",
|
||||||
|
"sla",
|
||||||
|
"disabled"
|
||||||
|
] as const;
|
||||||
|
|
||||||
const otherFields = computed(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const {id, namespace, description, inputs, ...rest} = fields.value;
|
|
||||||
|
|
||||||
return rest;
|
const fieldsFromSchema = computed(() => {
|
||||||
})
|
if( !flowSchema.value || !flowSchemaProperties.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: some labels are not where you would expect them to be
|
||||||
|
const mainLabels: Record<string, string> = {
|
||||||
|
id: t("no_code.fields.main.flow_id"),
|
||||||
|
namespace: t("no_code.fields.main.namespace"),
|
||||||
|
description: t("no_code.fields.main.description"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return METADATA_KEYS.map(f => ({
|
||||||
|
modelValue: props.metadata[f],
|
||||||
|
required: flowSchema.value?.required ?? [],
|
||||||
|
disabled: !creatingFlow.value && (f === "id" || f === "namespace"),
|
||||||
|
schema: flowSchemaProperties.value[f],
|
||||||
|
definitions: definitions.value,
|
||||||
|
label: mainLabels[f] ?? t(`no_code.fields.general.${f}`),
|
||||||
|
fieldKey: f,
|
||||||
|
task: props.metadata,
|
||||||
|
root: f,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
const SECTIONS_IDS = [
|
const SECTIONS_IDS = [
|
||||||
"tasks",
|
"tasks",
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
const position = inject(POSITION_INJECTION_KEY, "after");
|
const position = inject(POSITION_INJECTION_KEY, "after");
|
||||||
const creatingTask = inject(
|
const creatingTask = inject(
|
||||||
CREATING_TASK_INJECTION_KEY,
|
CREATING_TASK_INJECTION_KEY,
|
||||||
ref(false),
|
false,
|
||||||
);
|
);
|
||||||
const exitTaskElement = inject(
|
const exitTaskElement = inject(
|
||||||
CLOSE_TASK_FUNCTION_INJECTION_KEY,
|
CLOSE_TASK_FUNCTION_INJECTION_KEY,
|
||||||
@@ -90,11 +90,16 @@
|
|||||||
|
|
||||||
const yaml = ref("");
|
const yaml = ref("");
|
||||||
|
|
||||||
|
function getPath(parentPath: string, refPath: number | undefined): string {
|
||||||
|
return refPath !== undefined && refPath !== null ? `${parentPath}[${refPath}]` : parentPath;
|
||||||
|
}
|
||||||
|
|
||||||
watch(flow, (source) => {
|
watch(flow, (source) => {
|
||||||
if(!creatingTask.value){
|
if(!creatingTask){
|
||||||
|
const path = getPath(parentPath, refPath);
|
||||||
const taskYaml = YAML_UTILS.extractBlockWithPath({
|
const taskYaml = YAML_UTILS.extractBlockWithPath({
|
||||||
source,
|
source,
|
||||||
path: `${parentPath}[${refPath}]`,
|
path,
|
||||||
}) ?? ""
|
}) ?? ""
|
||||||
|
|
||||||
if(taskYaml === yaml.value){
|
if(taskYaml === yaml.value){
|
||||||
@@ -157,15 +162,16 @@
|
|||||||
const saveTask = () => {
|
const saveTask = () => {
|
||||||
let result: string = flow.value;
|
let result: string = flow.value;
|
||||||
|
|
||||||
if (!creatingTask.value) {
|
if (!creatingTask) {
|
||||||
if(yaml.value){
|
if(yaml.value){
|
||||||
|
const path = getPath(parentPath, refPath);
|
||||||
result = YAML_UTILS.replaceBlockWithPath({
|
result = YAML_UTILS.replaceBlockWithPath({
|
||||||
source: result,
|
source: result,
|
||||||
path: `${parentPath}[${refPath}]`,
|
path,
|
||||||
newContent: yaml.value,
|
newContent: yaml.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}else if(!hasMovedToEdit.value && blockType){
|
} else if(!hasMovedToEdit.value && blockType){
|
||||||
const currentSection = section.value as keyof typeof SECTIONS_MAP;
|
const currentSection = section.value as keyof typeof SECTIONS_MAP;
|
||||||
|
|
||||||
if(!currentSection) {
|
if(!currentSection) {
|
||||||
|
|||||||
415
ui/src/components/code/segments/flow-schema.json
Normal file
415
ui/src/components/code/segments/flow-schema.json
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.Flow",
|
||||||
|
"definitions": {
|
||||||
|
"io.kestra.plugin.core.debug.Echo": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allowFailure": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core",
|
||||||
|
"markdownDescription": "Default value is : `false`"
|
||||||
|
},
|
||||||
|
"allowWarning": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core",
|
||||||
|
"markdownDescription": "Default value is : `false`"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core",
|
||||||
|
"markdownDescription": "Default value is : `false`"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"$dynamic": true
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ERROR",
|
||||||
|
"WARN",
|
||||||
|
"INFO",
|
||||||
|
"DEBUG",
|
||||||
|
"TRACE"
|
||||||
|
],
|
||||||
|
"default": "INFO",
|
||||||
|
"$dynamic": true,
|
||||||
|
"markdownDescription": "Default value is : `INFO`"
|
||||||
|
},
|
||||||
|
"logLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ERROR",
|
||||||
|
"WARN",
|
||||||
|
"INFO",
|
||||||
|
"DEBUG",
|
||||||
|
"TRACE"
|
||||||
|
],
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core"
|
||||||
|
},
|
||||||
|
"logToFile": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core",
|
||||||
|
"markdownDescription": "Default value is : `false`"
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.tasks.retrys.Constant-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.tasks.retrys.Exponential-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.tasks.retrys.Random-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"runIf": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "true",
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core",
|
||||||
|
"markdownDescription": "Default value is : `\"true\"`"
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "duration",
|
||||||
|
"$dynamic": true,
|
||||||
|
"$group": "core"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"const": "io.kestra.plugin.core.debug.Echo"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "The version of the plugin to use.",
|
||||||
|
"pattern": "\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9-]+)?|([a-zA-Z0-9]+)",
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core"
|
||||||
|
},
|
||||||
|
"workerGroup": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.tasks.WorkerGroup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$dynamic": false,
|
||||||
|
"$group": "core"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "Log a message in the task logs (Deprecated).",
|
||||||
|
"$deprecated": "true",
|
||||||
|
"markdownDescription": "This task is deprecated, please use the `io.kestra.plugin.core.log.Log` task instead.##### Examples\n> \n```yaml\nid: echo_flow\nnamespace: company.team\n\ntasks:\n - id: echo\n type: io.kestra.plugin.core.debug.Echo\n level: WARN\n format: \"{{ task.id }} > {{ taskrun.startDate }}\"\n\n```"
|
||||||
|
},
|
||||||
|
"io.kestra.core.models.flows.Flow": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"afterExecution": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.plugin.core.debug.Echo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"concurrency": {
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.Concurrency"
|
||||||
|
},
|
||||||
|
"deleted": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"markdownDescription": "Default value is : `false`"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"markdownDescription": "Default value is : `false`"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finally": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.plugin.core.debug.Echo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*"
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.ArrayInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.BooleanInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.BoolInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.DateInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.DateTimeInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.DurationInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.FileInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.FloatInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.IntInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.JsonInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.SecretInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.StringInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.EnumInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.SelectInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.TimeInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.URIInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.MultiselectInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.YamlInput-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.input.EmailInput-2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"listeners": {
|
||||||
|
"$deprecated": true,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.listeners.Listener"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$deprecated": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 150,
|
||||||
|
"pattern": "^[a-z0-9][a-z0-9._-]*"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"title": "Output values available and exposes to other flows.",
|
||||||
|
"$dynamic": true,
|
||||||
|
"markdownDescription": "Output values make information about the execution of your Flow available and expose for other Kestra flows to use. Output values are similar to return values in programming languages.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.Output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$dynamic": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginDefaults": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.PluginDefault"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.tasks.retrys.Constant-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.tasks.retrys.Exponential-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.tasks.retrys.Random-2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"sla": {
|
||||||
|
"$dynamic": false,
|
||||||
|
"$beta": true,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.sla.types.MaxDurationSLA-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$dynamic": false,
|
||||||
|
"$beta": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.sla.types.ExecutionAssertionSLA-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$dynamic": false,
|
||||||
|
"$beta": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskDefaults": {
|
||||||
|
"$deprecated": true,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/io.kestra.core.models.flows.PluginDefault"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$deprecated": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [{
|
||||||
|
"$ref": "#/definitions/io.kestra.plugin.core.debug.Echo"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tenantId": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9][a-z0-9_-]*"
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"namespace",
|
||||||
|
"tasks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
ui/src/components/code/utils/cleanUp.ts
Normal file
31
ui/src/components/code/utils/cleanUp.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
function isNullOrUndefined(value: any): boolean {
|
||||||
|
return value === null || value === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeNullAndUndefined(obj: any): any {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
const ar = obj
|
||||||
|
.map(item => removeNullAndUndefined(item))
|
||||||
|
.filter(item => isNullOrUndefined(item) === false);
|
||||||
|
|
||||||
|
return ar.length > 0 ? ar : undefined;
|
||||||
|
}
|
||||||
|
if (typeof obj === "object") {
|
||||||
|
const newObj: any = {};
|
||||||
|
let hasValue = false;
|
||||||
|
for (const key in obj) {
|
||||||
|
const rawValue = obj[key]
|
||||||
|
if(isNullOrUndefined(rawValue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const newVal = removeNullAndUndefined(rawValue);
|
||||||
|
if(isNullOrUndefined(newVal)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
hasValue = true;
|
||||||
|
newObj[key] = newVal;
|
||||||
|
}
|
||||||
|
return hasValue ? newObj : undefined;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100 p-4" v-if="currentView === views.DASHBOARD">
|
<div class="w-100 p-4" v-if="currentView === views.DASHBOARD">
|
||||||
<ChartsSection :charts="charts.map(chart => chart.data)" />
|
<ChartsSection :charts="charts.map(chart => chart.data)" show-default />
|
||||||
</div>
|
</div>
|
||||||
<div class="main-editor" v-else>
|
<div class="main-editor" v-else>
|
||||||
<div
|
<div
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
:source="selectedChart.content"
|
:source="selectedChart.content"
|
||||||
:chart="selectedChart"
|
:chart="selectedChart"
|
||||||
:identifier="selectedChart.id"
|
:identifier="selectedChart.id"
|
||||||
is-preview
|
show-default
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
|
|
||||||
<VarValue
|
<VarValue
|
||||||
v-if="displayVarValue()"
|
v-if="displayVarValue()"
|
||||||
:value="selectedValue"
|
:value="selectedValue.uri ? selectedValue.uri : selectedValue"
|
||||||
:execution="execution"
|
:execution="execution"
|
||||||
/>
|
/>
|
||||||
<SubFlowLink
|
<SubFlowLink
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
ref="monacoEditor"
|
ref="monacoEditor"
|
||||||
class="border flex-grow-1 position-relative"
|
class="border flex-grow-1 position-relative"
|
||||||
:language="`${language?.domain === undefined ? '' : (language.domain + '-')}${legacyQuery ? 'legacy-' : ''}filter`"
|
:language="`${language.domain === undefined ? '' : (language.domain + '-')}${legacyQuery ? 'legacy-' : ''}filter`"
|
||||||
:schema-type="language?.domain"
|
:schema-type="language.domain"
|
||||||
:value="filter"
|
:value="filter"
|
||||||
@change="filter = $event"
|
@change="filter = $event"
|
||||||
:theme="themeComputed"
|
:theme="themeComputed"
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
@editor-did-mount="editorDidMount"
|
@editor-did-mount="editorDidMount"
|
||||||
suggestions-on-focus
|
suggestions-on-focus
|
||||||
:placeholder="placeholder ?? t('filters.label')"
|
:placeholder="placeholder ?? t('filters.label')"
|
||||||
|
data-testid="monaco-filter"
|
||||||
/>
|
/>
|
||||||
<el-button-group
|
<el-button-group
|
||||||
class="d-inline-flex"
|
class="d-inline-flex"
|
||||||
@@ -84,6 +85,8 @@
|
|||||||
import {Comparators, getComparator} from "../../composables/monaco/languages/filters/filterCompletion.ts";
|
import {Comparators, getComparator} from "../../composables/monaco/languages/filters/filterCompletion.ts";
|
||||||
import {watchDebounced} from "@vueuse/core";
|
import {watchDebounced} from "@vueuse/core";
|
||||||
import {FilterLanguage} from "../../composables/monaco/languages/filters/filterLanguage.ts";
|
import {FilterLanguage} from "../../composables/monaco/languages/filters/filterLanguage.ts";
|
||||||
|
import DefaultFilterLanguage from "../../composables/monaco/languages/filters/impl/defaultFilterLanguage.ts";
|
||||||
|
import _isEqual from "lodash/isEqual";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
prefix?: string | undefined;
|
prefix?: string | undefined;
|
||||||
language?: FilterLanguage | undefined,
|
language?: FilterLanguage,
|
||||||
propertiesWidth?: number,
|
propertiesWidth?: number,
|
||||||
buttons?: (Omit<Buttons, "settings"> & {
|
buttons?: (Omit<Buttons, "settings"> & {
|
||||||
settings: Omit<Buttons["settings"], "charts"> & { charts?: Buttons["settings"]["charts"] }
|
settings: Omit<Buttons["settings"], "charts"> & { charts?: Buttons["settings"]["charts"] }
|
||||||
@@ -104,7 +107,7 @@
|
|||||||
legacyQuery?: boolean,
|
legacyQuery?: boolean,
|
||||||
}>(), {
|
}>(), {
|
||||||
prefix: undefined,
|
prefix: undefined,
|
||||||
language: undefined,
|
language: () => DefaultFilterLanguage,
|
||||||
propertiesWidth: 144,
|
propertiesWidth: 144,
|
||||||
buttons: () => ({
|
buttons: () => ({
|
||||||
refresh: {
|
refresh: {
|
||||||
@@ -146,7 +149,7 @@
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const itemsPrefix = computed(() => props.prefix ?? route.name?.toString());
|
const itemsPrefix = computed(() => props.prefix ?? route.name?.toString() ?? "fallback-filters");
|
||||||
|
|
||||||
const emits = defineEmits(["dashboard", "updateProperties"]);
|
const emits = defineEmits(["dashboard", "updateProperties"]);
|
||||||
|
|
||||||
@@ -160,14 +163,9 @@
|
|||||||
.map(([key, value]) => [value, key])
|
.map(([key, value]) => [value, key])
|
||||||
);
|
);
|
||||||
|
|
||||||
const EXCLUDED_QUERY_FIELDS = ["sort", "size", "page"];
|
const queryParamsToKeep = ref<string[]>([]);
|
||||||
|
|
||||||
const filteredRouteQuery = computed(() => route.query === undefined
|
watch(() => route.query, (newVal) => {
|
||||||
? undefined
|
|
||||||
: Object.fromEntries(Object.entries(route.query).filter(([key]) => !EXCLUDED_QUERY_FIELDS.includes(key))) as LocationQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(filteredRouteQuery, (newVal) => {
|
|
||||||
if (skipRouteWatcherOnce.value) {
|
if (skipRouteWatcherOnce.value) {
|
||||||
skipRouteWatcherOnce.value = false;
|
skipRouteWatcherOnce.value = false;
|
||||||
return;
|
return;
|
||||||
@@ -177,12 +175,19 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryParamsToKeep.value = [];
|
||||||
|
|
||||||
let query = newVal;
|
let query = newVal;
|
||||||
if (props.queryNamespace !== undefined) {
|
if (props.queryNamespace !== undefined) {
|
||||||
query = Object.fromEntries(
|
query = Object.fromEntries(
|
||||||
Object.entries(newVal)
|
Object.entries(newVal)
|
||||||
.filter(([key]) => {
|
.filter(([key]) => {
|
||||||
return key.startsWith(props.queryNamespace + "[");
|
if (key.startsWith(props.queryNamespace + "[")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParamsToKeep.value.push(key);
|
||||||
|
return false;
|
||||||
})
|
})
|
||||||
.map(([key, value]) =>
|
.map(([key, value]) =>
|
||||||
// We trim the queryNamespace from the key
|
// We trim the queryNamespace from the key
|
||||||
@@ -198,17 +203,32 @@
|
|||||||
*/
|
*/
|
||||||
filter.value = Object.entries(query)
|
filter.value = Object.entries(query)
|
||||||
.flatMap(([key, values]) => {
|
.flatMap(([key, values]) => {
|
||||||
|
const remappedFilterKey = queryRemapper[key] ?? key;
|
||||||
|
|
||||||
|
if (!props.language.keyMatchers()?.some(keyMatcher => keyMatcher.test(FilterLanguage.withNestedKeyPlaceholder(remappedFilterKey)))) {
|
||||||
|
queryParamsToKeep.value.push(key);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(values)) {
|
if (!Array.isArray(values)) {
|
||||||
values = [values];
|
values = [values];
|
||||||
}
|
}
|
||||||
|
|
||||||
return values.map(value => (queryRemapper?.[key] ?? key) + Comparators.EQUALS + value);
|
return values.map(value => remappedFilterKey + Comparators.EQUALS + value);
|
||||||
}).join(" ");
|
}).join(" ");
|
||||||
} else {
|
} else {
|
||||||
|
Object.keys(query).filter((key) => {
|
||||||
|
return !key.startsWith("filters[");
|
||||||
|
}).forEach((key) => {
|
||||||
|
queryParamsToKeep.value.push(key);
|
||||||
|
});
|
||||||
|
|
||||||
filter.value = Object.entries(query)
|
filter.value = Object.entries(query)
|
||||||
.filter(([key]) => key.startsWith("filters["))
|
.filter(([key]) => key.startsWith("filters["))
|
||||||
.flatMap(([key, values]) => {
|
.flatMap(([key, values]) => {
|
||||||
const [_, filterKey, comparator, subKey] = key.match(/filters\[([^\]]+)]\[([^\]]+)](?:\[([^\]]+)])?/) ?? [];
|
const [_, filterKey, comparator, subKey] = key.match(/filters\[([^\]]+)]\[([^\]]+)](?:\[([^\]]+)])?/) ?? [];
|
||||||
|
const remappedFilterKey = queryRemapper[filterKey] ?? filterKey;
|
||||||
|
|
||||||
let maybeSubKeyString;
|
let maybeSubKeyString;
|
||||||
if (subKey === undefined) {
|
if (subKey === undefined) {
|
||||||
maybeSubKeyString = "";
|
maybeSubKeyString = "";
|
||||||
@@ -220,7 +240,7 @@
|
|||||||
values = [values];
|
values = [values];
|
||||||
}
|
}
|
||||||
|
|
||||||
return values.map(value => (queryRemapper?.[filterKey] ?? filterKey) + maybeSubKeyString + getComparator(comparator as Parameters<typeof getComparator>[0]) + value);
|
return values.map(value => remappedFilterKey + maybeSubKeyString + getComparator(comparator as Parameters<typeof getComparator>[0]) + (value!.includes(" ") ? `"${value}"` : value));
|
||||||
})
|
})
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
@@ -243,10 +263,10 @@
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEY_MATCHER = "((?:(?!" + COMPARATORS_REGEX + ")\\S)+?)";
|
const KEY_MATCHER = "((?:(?!" + COMPARATORS_REGEX + ")(?:\\S|\"[^\"]*\"))+?)";
|
||||||
const COMPARATOR_MATCHER = "(" + COMPARATORS_REGEX + ")";
|
const COMPARATOR_MATCHER = "(" + COMPARATORS_REGEX + ")";
|
||||||
const MAYBE_PREVIOUS_VALUE = "(?:(?<=\\S),)?";
|
const MAYBE_PREVIOUS_VALUE = "(?:(?<=\\S),)?";
|
||||||
const VALUE_MATCHER = "((?:" + MAYBE_PREVIOUS_VALUE + "(?:(?:\"[^\\n,]*\")|(?:[^\\s,]*)))+)";
|
const VALUE_MATCHER = "((?:" + MAYBE_PREVIOUS_VALUE + "(?:(?:\"[^\"]*\")|(?:[^\\s,]*)))+)";
|
||||||
const filterMatcher = new RegExp("\\s*(?<!\\S)" +
|
const filterMatcher = new RegExp("\\s*(?<!\\S)" +
|
||||||
"((?:" + KEY_MATCHER + COMPARATOR_MATCHER + VALUE_MATCHER + ")" +
|
"((?:" + KEY_MATCHER + COMPARATOR_MATCHER + VALUE_MATCHER + ")" +
|
||||||
"|\"([^\"]*)\"" +
|
"|\"([^\"]*)\"" +
|
||||||
@@ -259,7 +279,7 @@
|
|||||||
|
|
||||||
// If we're not in a {key}{comparator}{value} format, we assume it's a text search
|
// If we're not in a {key}{comparator}{value} format, we assume it's a text search
|
||||||
if (key === undefined) {
|
if (key === undefined) {
|
||||||
if (props.language?.textFilterSupported && (text === undefined || !props.language?.keyMatchers()?.some(keyMatcher => keyMatcher.test(text)))) {
|
if (props.language.textFilterSupported && (text === undefined || !props.language.keyMatchers()?.some(keyMatcher => keyMatcher.test(text)))) {
|
||||||
filters.push({
|
filters.push({
|
||||||
key: "text",
|
key: "text",
|
||||||
comparator: "EQUALS",
|
comparator: "EQUALS",
|
||||||
@@ -269,15 +289,17 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.language?.keyMatchers()?.some(keyMatcher => keyMatcher.test(key))) {
|
if (!props.language.keyMatchers()?.some(keyMatcher => keyMatcher.test(key))) {
|
||||||
continue; // Skip keys that don't match the language key matchers
|
continue; // Skip keys that don't match the language key matchers
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.language?.comparatorsPerKey()[FilterLanguage.withNestedKeyPlaceholder(key)].some(c => Comparators[c] === comparator)) {
|
if (!props.language.comparatorsPerKey()[FilterLanguage.withNestedKeyPlaceholder(key)].some(c => Comparators[c] === comparator)) {
|
||||||
continue; // Skip comparators that are not valid for the key
|
continue; // Skip comparators that are not valid for the key
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = [...new Set(commaSeparatedValues?.split(",")?.filter(value => value !== "")?.map(value => value.replaceAll("\"", "")) ?? [])];
|
const values = [...new Set(
|
||||||
|
[...commaSeparatedValues?.matchAll(/,?(?:"([^"]*)"|([^",]+))/g) ?? []].map(([_, quotedValue, rawValue]) => quotedValue ?? rawValue) ?? [])
|
||||||
|
];
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
continue; // Skip empty values
|
continue; // Skip empty values
|
||||||
}
|
}
|
||||||
@@ -308,9 +330,9 @@
|
|||||||
|
|
||||||
if (!props.legacyQuery) {
|
if (!props.legacyQuery) {
|
||||||
if (key.includes(".")) {
|
if (key.includes(".")) {
|
||||||
const keyAndSubKeyMatch = queryKey.match(/([^.]+)\.([^.]+)/);
|
const keyAndSubKeyMatch = queryKey.match(/([^.]+)\.(\S+)/);
|
||||||
const rootKey = keyAndSubKeyMatch?.[1];
|
const rootKey = keyAndSubKeyMatch?.[1];
|
||||||
const subKey = keyAndSubKeyMatch?.[2];
|
const subKey = keyAndSubKeyMatch?.[2].replace(/^"([^"]*)"$/, "$1");
|
||||||
if (rootKey === undefined || subKey === undefined) {
|
if (rootKey === undefined || subKey === undefined) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -443,16 +465,23 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
watchDebounced(filterQueryString, () => {
|
watchDebounced(filterQueryString, () => {
|
||||||
|
const newQuery = {
|
||||||
|
...Object.fromEntries(queryParamsToKeep.value.map(key => {
|
||||||
|
return [
|
||||||
|
key,
|
||||||
|
route.query[key]
|
||||||
|
];
|
||||||
|
})),
|
||||||
|
...filterQueryString.value
|
||||||
|
};
|
||||||
|
if (_isEqual(route.query, newQuery)) {
|
||||||
|
return; // Skip if the query hasn't changed
|
||||||
|
}
|
||||||
skipRouteWatcherOnce.value = true;
|
skipRouteWatcherOnce.value = true;
|
||||||
router.push({
|
router.push({
|
||||||
query: {
|
query: newQuery
|
||||||
sort: route.query.sort,
|
|
||||||
size: route.query.size,
|
|
||||||
page: route.query.page,
|
|
||||||
...filterQueryString.value
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, {immediate: true, debounce: 500});
|
}, {immediate: true, debounce: 1000});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -739,7 +739,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (this.namespace) {
|
if (this.namespace) {
|
||||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
queryFilter["filters[namespace][EQUALS]"] = this.$route.params.id || this.namespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _merge(base, queryFilter);
|
return _merge(base, queryFilter);
|
||||||
|
|||||||
@@ -280,7 +280,7 @@
|
|||||||
id: this.newMetadata.id,
|
id: this.newMetadata.id,
|
||||||
namespace: this.newMetadata.namespace,
|
namespace: this.newMetadata.namespace,
|
||||||
description: this.newMetadata.description,
|
description: this.newMetadata.description,
|
||||||
retry: retry,
|
retry: retry && Object.keys(retry).length > 0 ? retry : undefined,
|
||||||
labels: this.arrayToObject(this.newMetadata.labels),
|
labels: this.arrayToObject(this.newMetadata.labels),
|
||||||
inputs: this.newMetadata.inputs.filter(e => e.id && e.type),
|
inputs: this.newMetadata.inputs.filter(e => e.id && e.type),
|
||||||
variables: this.arrayToObject(this.newMetadata.variables),
|
variables: this.arrayToObject(this.newMetadata.variables),
|
||||||
|
|||||||
@@ -19,127 +19,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
|
import {ref, watch, inject} from "vue";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
import InputText from "../code/components/inputs/InputText.vue";
|
import InputText from "../code/components/inputs/InputText.vue";
|
||||||
import Add from "../code/components/Add.vue";
|
import Add from "../code/components/Add.vue";
|
||||||
|
|
||||||
import {DeleteOutline} from "../code/utils/icons";
|
import {DeleteOutline} from "../code/utils/icons";
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import {h} from "vue";
|
|
||||||
import MetadataInputsContent from "./MetadataInputsContent.vue";
|
|
||||||
|
|
||||||
import {mapState} from "vuex";
|
|
||||||
import {BREADCRUMB_INJECTION_KEY, PANEL_INJECTION_KEY} from "../code/injectionKeys";
|
import {BREADCRUMB_INJECTION_KEY, PANEL_INJECTION_KEY} from "../code/injectionKeys";
|
||||||
|
|
||||||
export default {
|
interface InputType {
|
||||||
emits: ["update:modelValue"],
|
type: string;
|
||||||
props: {
|
id?: string;
|
||||||
modelValue: {
|
cls?: string;
|
||||||
type: Array,
|
}
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
inputs: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
label: {type: String, required: true},
|
|
||||||
required: {type: Boolean, default: false},
|
|
||||||
disabled: {type: Boolean, default: false},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState("plugin", ["inputSchema", "inputsType"]),
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.newInputs = this.inputs;
|
|
||||||
|
|
||||||
this.$store
|
const {t} = useI18n();
|
||||||
.dispatch("plugin/loadInputsType")
|
|
||||||
.then((_) => (this.loading = false));
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
newInputs: [],
|
|
||||||
selectedInput: undefined,
|
|
||||||
selectedIndex: undefined,
|
|
||||||
isEditOpen: false,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
inject:{
|
|
||||||
panel: {from: PANEL_INJECTION_KEY},
|
|
||||||
breadcrumbs: {from: BREADCRUMB_INJECTION_KEY}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
selectInput(input, index) {
|
|
||||||
this.loading = true;
|
|
||||||
this.selectedInput = input;
|
|
||||||
this.selectedIndex = index;
|
|
||||||
|
|
||||||
this.loadSchema(input.type);
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue: InputType[];
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>(), {
|
||||||
|
modelValue: () => [],
|
||||||
|
required: false,
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
|
||||||
this.panel = h(MetadataInputsContent, {
|
const emit = defineEmits<{
|
||||||
modelValue: input,
|
(e: "update:modelValue", value: InputType[]): void
|
||||||
inputs: this.inputs,
|
}>();
|
||||||
label: this.$t("inputs"),
|
|
||||||
selectedIndex: index,
|
|
||||||
"onUpdate:modelValue": this.updateSelected,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.breadcrumbs.push(
|
const panel = inject(PANEL_INJECTION_KEY, ref());
|
||||||
{
|
const breadcrumbs = inject(BREADCRUMB_INJECTION_KEY, ref([]));
|
||||||
label: this.$t("inputs").toLowerCase(),
|
|
||||||
});
|
const newInputs = ref<InputType[]>([]);
|
||||||
},
|
const selectedInput = ref<InputType | undefined>();
|
||||||
getCls(type) {
|
const selectedIndex = ref<number | undefined>();
|
||||||
return this.inputsType.find((e) => e.type === type).cls;
|
const loading = ref(false);
|
||||||
},
|
|
||||||
getType(cls) {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
return this.inputsType.find((e) => e.cls === cls).type;
|
newInputs.value = newValue;
|
||||||
},
|
}, {deep: true, immediate: true});
|
||||||
loadSchema(type) {
|
|
||||||
this.$store
|
const selectInput = async (input: InputType, index: number) => {
|
||||||
.dispatch("plugin/loadInputSchema", {type: type})
|
loading.value = true;
|
||||||
.then((_) => (this.loading = false));
|
selectedInput.value = input;
|
||||||
},
|
selectedIndex.value = index;
|
||||||
update() {
|
|
||||||
if (
|
panel.value = {
|
||||||
this.newInputs.map((e) => e.id).length !==
|
props: {
|
||||||
new Set(this.newInputs.map((e) => e.id)).size
|
selectedIndex: index,
|
||||||
) {
|
}
|
||||||
this.$store.dispatch("core/showMessage", {
|
};
|
||||||
variant: "error",
|
|
||||||
title: this.$t("error"),
|
breadcrumbs.value.push({
|
||||||
message: this.$t("duplicate input id"),
|
label: t("inputs".toLowerCase()),
|
||||||
});
|
});
|
||||||
} else {
|
};
|
||||||
this.isEditOpen = false;
|
|
||||||
this.$emit("update:modelValue", this.newInputs);
|
const deleteInput = (index: number) => {
|
||||||
}
|
newInputs.value.splice(index, 1);
|
||||||
},
|
emit("update:modelValue", newInputs.value);
|
||||||
updateSelected(value) {
|
};
|
||||||
this.newInputs = value;
|
|
||||||
},
|
const addInput = () => {
|
||||||
deleteInput(index) {
|
newInputs.value.push({type: "STRING"});
|
||||||
this.newInputs.splice(index, 1);
|
selectInput(newInputs.value[newInputs.value.length - 1], newInputs.value.length - 1);
|
||||||
this.$emit("update:modelValue", this.newInputs);
|
|
||||||
},
|
|
||||||
addInput() {
|
|
||||||
this.newInputs.push({type: "STRING"});
|
|
||||||
this.selectInput(this.newInputs.at(-1), this.newInputs.length - 1);
|
|
||||||
},
|
|
||||||
onChangeType(value) {
|
|
||||||
this.loading = true;
|
|
||||||
this.selectedInput = {
|
|
||||||
type: value,
|
|
||||||
id: this.newInputs[this.selectedIndex].id,
|
|
||||||
};
|
|
||||||
this.newInputs[this.selectedIndex] = this.selectedInput;
|
|
||||||
this.loadSchema(value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="selectedInput.type"
|
:model-value="selectedInput?.type"
|
||||||
@update:model-value="onChangeType"
|
@update:model-value="onChangeType"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
@@ -25,116 +25,101 @@
|
|||||||
<Save @click="update" what="input" class="w-100 mt-3" />
|
<Save @click="update" what="input" class="w-100 mt-3" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
|
import {ref, computed, watch, onMounted, inject} from "vue";
|
||||||
|
import {useStore} from "vuex";
|
||||||
import TaskObject from "./tasks/TaskObject.vue";
|
import TaskObject from "./tasks/TaskObject.vue";
|
||||||
import Save from "../code/components/Save.vue";
|
import Save from "../code/components/Save.vue";
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import {mapState, mapGetters} from "vuex";
|
|
||||||
import {BREADCRUMB_INJECTION_KEY, PANEL_INJECTION_KEY} from "../code/injectionKeys";
|
import {BREADCRUMB_INJECTION_KEY, PANEL_INJECTION_KEY} from "../code/injectionKeys";
|
||||||
|
|
||||||
export default {
|
interface InputType {
|
||||||
emits: ["update:modelValue"],
|
type: string;
|
||||||
props: {
|
id?: string;
|
||||||
modelValue: {
|
}
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
const props = withDefaults(defineProps<{
|
||||||
},
|
inputs: InputType[];
|
||||||
inputs: {
|
label: string;
|
||||||
type: Array,
|
selectedIndex: number;
|
||||||
default: () => [],
|
required?: boolean;
|
||||||
},
|
disabled?: boolean;
|
||||||
label: {type: String, required: true},
|
}>(), {
|
||||||
selectedIndex: {type: Number, required: true},
|
inputs: () => [],
|
||||||
required: {type: Boolean, default: false},
|
required: false,
|
||||||
disabled: {type: Boolean, default: false},
|
disabled: false
|
||||||
},
|
});
|
||||||
computed: {
|
|
||||||
...mapState("plugin", ["inputSchema", "inputsType"]),
|
const store = useStore();
|
||||||
...mapGetters("flow", ["flowYamlMetadata"]),
|
|
||||||
},
|
const inputSchema = computed(() => store.state.plugin.inputSchema);
|
||||||
created() {
|
const inputsType = computed(() => store.state.plugin.inputsType);
|
||||||
if (this.inputs && this.inputs.length > 0) {
|
|
||||||
this.newInputs = this.inputs;
|
const emit = defineEmits<{
|
||||||
|
(e: "update:inputs", value?: InputType[]): void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const panel = inject(PANEL_INJECTION_KEY, ref());
|
||||||
|
const breadcrumbs = inject(BREADCRUMB_INJECTION_KEY, ref([]));
|
||||||
|
|
||||||
|
const newInputs = ref<InputType[]>([{type: "STRING"}]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const loadSchema = async (type: string) => {
|
||||||
|
loading.value = true;
|
||||||
|
await store.dispatch("plugin/loadInputSchema", {type});
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loading.value = true;
|
||||||
|
store.dispatch("plugin/loadInputsType")
|
||||||
|
.then(() => loading.value = false);
|
||||||
|
|
||||||
|
if(selectedInput.value.type) {
|
||||||
|
loadSchema(selectedInput.value.type);
|
||||||
|
} else {
|
||||||
|
loadSchema("STRING");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.inputs, (val) => {
|
||||||
|
if (val?.length) {
|
||||||
|
newInputs.value = props.inputs;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedInput = computed(() => {
|
||||||
|
return props.inputs[props.selectedIndex] ?? {type: "STRING"};
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
panel.value = undefined;
|
||||||
|
breadcrumbs.value.pop();
|
||||||
|
const value = newInputs.value.filter(input => input?.id);
|
||||||
|
emit("update:inputs", value.length ? value : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSelected = (value: InputType, index: number) => {
|
||||||
|
if (index >= 0) {
|
||||||
|
if (index >= 0) {
|
||||||
|
newInputs.value[index] = value;
|
||||||
|
emit("update:inputs", [...newInputs.value]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.selectedInput = this.modelValue ?? {type: "STRING"};
|
const onChangeType = (type: string) => {
|
||||||
|
// Resetting the selected input if the type changes, but keeping the ID if it exists
|
||||||
|
const id = selectedInput.value?.id;
|
||||||
|
const newInput = {...(id ? {id} : {}), type};
|
||||||
|
|
||||||
this.$store
|
newInputs.value[props.selectedIndex] = newInput;
|
||||||
.dispatch("plugin/loadInputsType")
|
|
||||||
.then((_) => (this.loading = false));
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
newInputs: [{type: "STRING"}],
|
|
||||||
selectedInput: undefined,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
inject:{
|
|
||||||
panel: {from: PANEL_INJECTION_KEY},
|
|
||||||
breadcrumbs: {from: BREADCRUMB_INJECTION_KEY}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
selectInput(input) {
|
|
||||||
this.selectedInput = input;
|
|
||||||
this.loadSchema(input.type);
|
|
||||||
},
|
|
||||||
getCls(type) {
|
|
||||||
return this.inputsType.find((e) => e.type === type).cls;
|
|
||||||
},
|
|
||||||
getType(cls) {
|
|
||||||
return this.inputsType.find((e) => e.cls === cls).type;
|
|
||||||
},
|
|
||||||
loadSchema(type) {
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
this.$store
|
emit("update:inputs", [...newInputs.value]);
|
||||||
.dispatch("plugin/loadInputSchema", {type: type})
|
loadSchema(type);
|
||||||
.then((_) => (this.loading = false));
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
if (
|
|
||||||
this.newInputs.map((e) => e?.id).length !==
|
|
||||||
new Set(this.newInputs.map((e) => e?.id)).size
|
|
||||||
) {
|
|
||||||
this.$store.dispatch("core/showMessage", {
|
|
||||||
variant: "error",
|
|
||||||
title: this.$t("error"),
|
|
||||||
message: this.$t("duplicate input id"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.panel = undefined;
|
|
||||||
this.breadcrumbs.pop();
|
|
||||||
const value = this.newInputs.filter(input => input?.id);
|
|
||||||
this.$emit("update:modelValue", value.length ? value : undefined);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateSelected(value) {
|
|
||||||
if (this.selectedIndex >= 0) {
|
|
||||||
this.newInputs[this.selectedIndex] = value;
|
|
||||||
this.$emit("update:modelValue", [...this.newInputs]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteInput(index) {
|
|
||||||
this.newInputs.splice(index, 1);
|
|
||||||
},
|
|
||||||
addInput() {
|
|
||||||
this.newInputs.push({type: "STRING"});
|
|
||||||
},
|
|
||||||
onChangeType(type) {
|
|
||||||
// Resetting the selected input if the type changes, but keeping the ID if it exists
|
|
||||||
const id = this.selectedInput?.id || undefined;
|
|
||||||
|
|
||||||
this.selectedInput = {...(id ? {id} : {}), type};
|
|
||||||
this.newInputs[this.selectedIndex] = {...(id ? {id} : {}), type};
|
|
||||||
|
|
||||||
this.$emit("update:modelValue", [...this.newInputs]);
|
|
||||||
|
|
||||||
this.loadSchema(type);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
<template>
|
|
||||||
<TaskWrapper>
|
|
||||||
<template #tasks>
|
|
||||||
<TaskObjectField
|
|
||||||
:field-key="label"
|
|
||||||
v-model="value"
|
|
||||||
:schema
|
|
||||||
:definitions
|
|
||||||
:task="{[label]: value}"
|
|
||||||
@update:model-value="(val) => emit('update:modelValue', val)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</TaskWrapper>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import TaskWrapper from "./tasks/TaskWrapper.vue";
|
|
||||||
import TaskObjectField from "./tasks/TaskObjectField.vue";
|
|
||||||
|
|
||||||
const value = defineModel({
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update:modelValue", value: any): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
label: {type: String, required: true},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// FIXME: Properly fetch and parse the schema and definitions
|
|
||||||
const schema = {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
$ref: "#/definitions/kestra_frontend.core.models.tasks.retrys.Constant-2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$ref: "#/definitions/kestra_frontend.core.models.tasks.retrys.Exponential-2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$ref: "#/definitions/kestra_frontend.core.models.tasks.retrys.Random-2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const definitions = {
|
|
||||||
"kestra_frontend.core.models.tasks.retrys.Random-2": {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
behavior: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["RETRY_FAILED_TASK", "CREATE_NEW_EXECUTION"],
|
|
||||||
default: "RETRY_FAILED_TASK",
|
|
||||||
markdownDescription: "Default value is : `RETRY_FAILED_TASK`",
|
|
||||||
},
|
|
||||||
maxAttempt: {
|
|
||||||
type: "integer",
|
|
||||||
minimum: 1,
|
|
||||||
},
|
|
||||||
maxDuration: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
maxInterval: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
minInterval: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "constant",
|
|
||||||
const: "random",
|
|
||||||
},
|
|
||||||
warningOnRetry: {
|
|
||||||
type: "boolean",
|
|
||||||
default: false,
|
|
||||||
markdownDescription: "Default value is : `false`",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["type", "maxInterval", "minInterval"],
|
|
||||||
},
|
|
||||||
"kestra_frontend.core.models.tasks.retrys.Exponential-2": {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
behavior: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["RETRY_FAILED_TASK", "CREATE_NEW_EXECUTION"],
|
|
||||||
default: "RETRY_FAILED_TASK",
|
|
||||||
markdownDescription: "Default value is : `RETRY_FAILED_TASK`",
|
|
||||||
},
|
|
||||||
delayFactor: {
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
interval: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
maxAttempt: {
|
|
||||||
type: "integer",
|
|
||||||
minimum: 1,
|
|
||||||
},
|
|
||||||
maxDuration: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
maxInterval: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "constant",
|
|
||||||
const: "exponential",
|
|
||||||
},
|
|
||||||
warningOnRetry: {
|
|
||||||
type: "boolean",
|
|
||||||
default: false,
|
|
||||||
markdownDescription: "Default value is : `false`",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["type", "interval", "maxInterval"],
|
|
||||||
},
|
|
||||||
"kestra_frontend.core.models.tasks.retrys.Constant-2": {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
behavior: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["RETRY_FAILED_TASK", "CREATE_NEW_EXECUTION"],
|
|
||||||
default: "RETRY_FAILED_TASK",
|
|
||||||
markdownDescription: "Default value is : `RETRY_FAILED_TASK`",
|
|
||||||
},
|
|
||||||
interval: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
maxAttempt: {
|
|
||||||
type: "integer",
|
|
||||||
minimum: 1,
|
|
||||||
},
|
|
||||||
maxDuration: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "constant",
|
|
||||||
const: "constant",
|
|
||||||
},
|
|
||||||
warningOnRetry: {
|
|
||||||
type: "boolean",
|
|
||||||
default: false,
|
|
||||||
markdownDescription: "Default value is : `false`",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["type", "interval"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "../code/styles/code.scss";
|
|
||||||
</style>
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<template>
|
|
||||||
<TaskWrapper>
|
|
||||||
<template #tasks>
|
|
||||||
<TaskObjectField
|
|
||||||
v-model="value[0]"
|
|
||||||
:field-key="label"
|
|
||||||
:schema
|
|
||||||
:definitions
|
|
||||||
:task="{[label]: value}"
|
|
||||||
@update:model-value="(val) => emit('update:modelValue', val? [val] : undefined)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</TaskWrapper>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import TaskWrapper from "./tasks/TaskWrapper.vue";
|
|
||||||
import TaskObjectField from "./tasks/TaskObjectField.vue";
|
|
||||||
|
|
||||||
const value = defineModel<any[]>({
|
|
||||||
type: Array,
|
|
||||||
default: () => ([]),
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update:modelValue", value: any): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
label: {type: String, required: true},
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: Properly fetch and parse the schema and definitions
|
|
||||||
const schema = {
|
|
||||||
anyOf: [
|
|
||||||
{
|
|
||||||
$ref: "#/definitions/io.kestra.core.models.flows.sla.types.ExecutionAssertionSLA-1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$ref: "#/definitions/io.kestra.core.models.flows.sla.types.MaxDurationSLA-1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const definitions = {
|
|
||||||
"io.kestra.core.models.flows.sla.types.ExecutionAssertionSLA-1": {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: "string",
|
|
||||||
minLength: 1,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "constant",
|
|
||||||
const: "EXECUTION_ASSERTION",
|
|
||||||
},
|
|
||||||
assert: {
|
|
||||||
type: "string",
|
|
||||||
minLength: 1,
|
|
||||||
},
|
|
||||||
behavior: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["FAIL", "CANCEL", "NONE"],
|
|
||||||
},
|
|
||||||
labels: {
|
|
||||||
type: "object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["type", "id", "assert", "behavior"],
|
|
||||||
},
|
|
||||||
"io.kestra.core.models.flows.sla.types.MaxDurationSLA-1": {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: "string",
|
|
||||||
minLength: 1,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "constant",
|
|
||||||
const: "MAX_DURATION",
|
|
||||||
},
|
|
||||||
behavior: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["FAIL", "CANCEL", "NONE"],
|
|
||||||
},
|
|
||||||
duration: {
|
|
||||||
type: "string",
|
|
||||||
format: "duration",
|
|
||||||
},
|
|
||||||
labels: {
|
|
||||||
type: "object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["type", "id", "behavior", "duration"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "../code/styles/code.scss";
|
|
||||||
</style>
|
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
import {useStorage} from "@vueuse/core";
|
import {useStorage} from "@vueuse/core";
|
||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useRoute} from "vue-router";
|
|
||||||
|
|
||||||
import MultiPanelTabs, {Panel, Tab} from "../MultiPanelTabs.vue";
|
import MultiPanelTabs, {Panel, Tab} from "../MultiPanelTabs.vue";
|
||||||
import EditorButtonsWrapper from "../inputs/EditorButtonsWrapper.vue";
|
import EditorButtonsWrapper from "../inputs/EditorButtonsWrapper.vue";
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const route = useRoute();
|
|
||||||
const flow = computed(() => store.state.flow.flow)
|
const flow = computed(() => store.state.flow.flow)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -145,11 +143,19 @@
|
|||||||
const {setupInitialCodeTab} = useInitialCodeTabs()
|
const {setupInitialCodeTab} = useInitialCodeTabs()
|
||||||
|
|
||||||
const isTourRunning = computed(() => store.state.core.guidedProperties?.tourStarted)
|
const isTourRunning = computed(() => store.state.core.guidedProperties?.tourStarted)
|
||||||
const DEAFULT_TABS = route.name === "flows/create" && isTourRunning.value ? ["code", "topology"] : DEFAULT_ACTIVE_TABS
|
const DEFAULT_TOUR_TABS = [
|
||||||
|
{tabs: ["code"], activeTab: "code", size: 1},
|
||||||
|
{tabs: ["topology"], activeTab: "topology", size: 1}
|
||||||
|
];
|
||||||
|
|
||||||
|
function cleanupNoCodeTabKey(key: string): string {
|
||||||
|
// remove the number for "nocode-1234-" prefix from the key
|
||||||
|
return /^nocode-\d{4}/.test(key) ? key.slice(0, 6) + key.slice(11) : key
|
||||||
|
}
|
||||||
|
|
||||||
const panels: Ref<Panel[]> = useStorage<any>(
|
const panels: Ref<Panel[]> = useStorage<any>(
|
||||||
`panel-${flow.value.namespace}-${flow.value.id}`,
|
`panel-${flow.value.namespace}-${flow.value.id}`,
|
||||||
DEAFULT_TABS
|
DEFAULT_ACTIVE_TABS
|
||||||
.map((t):Panel => getPanelFromValue(t).panel),
|
.map((t):Panel => getPanelFromValue(t).panel),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -157,13 +163,13 @@
|
|||||||
write(v: Panel[]){
|
write(v: Panel[]){
|
||||||
return JSON.stringify(v.map(p => ({
|
return JSON.stringify(v.map(p => ({
|
||||||
tabs: p.tabs.map(t => t.value),
|
tabs: p.tabs.map(t => t.value),
|
||||||
activeTab: p.activeTab?.value,
|
activeTab: cleanupNoCodeTabKey(p.activeTab?.value),
|
||||||
size: p.size,
|
size: p.size,
|
||||||
})))
|
})))
|
||||||
},
|
},
|
||||||
read(v?: string) {
|
read(v?: string) {
|
||||||
if(v){
|
if(v){
|
||||||
const panels: {tabs: string[], activeTab: string, size: number}[] = JSON.parse(v)
|
const panels: {tabs: string[], activeTab: string, size: number}[] = isTourRunning.value ? DEFAULT_TOUR_TABS : JSON.parse(v)
|
||||||
return panels
|
return panels
|
||||||
.filter((p) => p.tabs.length)
|
.filter((p) => p.tabs.length)
|
||||||
.map((p):Panel => {
|
.map((p):Panel => {
|
||||||
@@ -174,7 +180,7 @@
|
|||||||
)
|
)
|
||||||
// filter out any tab that may have disappeared
|
// filter out any tab that may have disappeared
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
const activeTab = tabs.find(t => t.value === p.activeTab) ?? tabs[0]
|
const activeTab = tabs.find(t => cleanupNoCodeTabKey(t.value) === p.activeTab) ?? tabs[0]
|
||||||
return {
|
return {
|
||||||
activeTab,
|
activeTab,
|
||||||
tabs,
|
tabs,
|
||||||
@@ -224,11 +230,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-wrapper{
|
.editor-wrapper{
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-panels{
|
:deep(.editor-panels){
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,18 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<TaskObject
|
<div @click="store.dispatch('plugin/updateDocumentation', {task: selectedTaskType});">
|
||||||
v-loading="isLoading"
|
<TaskObject
|
||||||
v-if="selectedTaskType && schema"
|
v-loading="isLoading"
|
||||||
name="root"
|
v-if="selectedTaskType && schema"
|
||||||
:model-value="taskObject"
|
name="root"
|
||||||
@update:model-value="onTaskInput"
|
:model-value="taskObject"
|
||||||
:schema="schemaProp"
|
@update:model-value="onTaskInput"
|
||||||
:properties="properties"
|
:schema="schemaProp"
|
||||||
:definitions="schema.definitions"
|
:properties="properties"
|
||||||
/>
|
:definitions="schema.definitions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
import PluginSelect from "../../components/plugins/PluginSelect.vue";
|
import PluginSelect from "../../components/plugins/PluginSelect.vue";
|
||||||
import {NoCodeElement, Schemas} from "../code/utils/types";
|
import {NoCodeElement, Schemas} from "../code/utils/types";
|
||||||
import {BLOCKTYPE_INJECT_KEY, PARENT_PATH_INJECTION_KEY} from "../code/injectionKeys";
|
import {BLOCKTYPE_INJECT_KEY, PARENT_PATH_INJECTION_KEY} from "../code/injectionKeys";
|
||||||
|
import {removeNullAndUndefined} from "../code/utils/cleanUp";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "TaskEditor",
|
name: "TaskEditor",
|
||||||
@@ -115,8 +118,6 @@
|
|||||||
taskObject.value = parsed;
|
taskObject.value = parsed;
|
||||||
}
|
}
|
||||||
selectedTaskType.value = taskObject.value?.type;
|
selectedTaskType.value = taskObject.value?.type;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// when tab is clicked, load the documentation
|
// when tab is clicked, load the documentation
|
||||||
@@ -165,7 +166,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
modelValue.value = YAML_UTILS.stringify(toRaw(val));
|
modelValue.value = YAML_UTILS.stringify(removeNullAndUndefined(toRaw(val)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTaskTypeSelect() {
|
function onTaskTypeSelect() {
|
||||||
@@ -180,7 +181,6 @@
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.type-div {
|
.type-div {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
:properties="Object.fromEntries(filteredProperties)"
|
:properties="Object.fromEntries(filteredProperties)"
|
||||||
:definitions="definitions"
|
:definitions="definitions"
|
||||||
@update:model-value="onAnyOfInput"
|
@update:model-value="onAnyOfInput"
|
||||||
|
merge
|
||||||
/>
|
/>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,6 +42,52 @@
|
|||||||
import {TaskIcon} from "@kestra-io/ui-libs";
|
import {TaskIcon} from "@kestra-io/ui-libs";
|
||||||
import getTaskComponent from "./getTaskComponent";
|
import getTaskComponent from "./getTaskComponent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* merge allOf schemas if they exist
|
||||||
|
* @param schema
|
||||||
|
*/
|
||||||
|
function consolidateAllOfSchemas(schema, definitions) {
|
||||||
|
if(schema?.allOf?.length) {
|
||||||
|
return {
|
||||||
|
...schema,
|
||||||
|
type: "object",
|
||||||
|
...schema.allOf.reduce((acc, item) => {
|
||||||
|
if(item.$ref) {
|
||||||
|
const refSchema = definitions[item.$ref.split("/").pop()];
|
||||||
|
if(refSchema) {
|
||||||
|
return {
|
||||||
|
required: [
|
||||||
|
...acc.required,
|
||||||
|
...(refSchema.required ?? [])
|
||||||
|
],
|
||||||
|
properties: {
|
||||||
|
...acc.properties,
|
||||||
|
...refSchema.properties,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
required: [
|
||||||
|
...acc.required,
|
||||||
|
...(item.required ?? [])
|
||||||
|
],
|
||||||
|
properties: {
|
||||||
|
...acc.properties,
|
||||||
|
...item.properties,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TaskIcon,
|
TaskIcon,
|
||||||
@@ -51,22 +98,28 @@
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
schemas: [],
|
|
||||||
selectedSchema: undefined,
|
selectedSchema: undefined,
|
||||||
finishedMounting: false,
|
finishedMounting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.schemas = this.schema?.anyOf ?? [];
|
|
||||||
|
|
||||||
const schema = this.schemaOptions.find((item) =>
|
const schema = this.schemaOptions.find((item) =>
|
||||||
typeof item.value === this.modelValue?.type ||
|
item.value === this.modelValue?.type ||
|
||||||
(this.modelValue === "string" && item.value === "string") ||
|
(typeof this.modelValue === "string" && item.value === "string") ||
|
||||||
(this.modelValue === "number" && item.value === "integer") ||
|
(typeof this.modelValue === "number" && item.value === "integer") ||
|
||||||
(Array.isArray(this.modelValue) && item.value === "array"),
|
(Array.isArray(this.modelValue) && item.value === "array"),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.selectedSchema = schema?.value || this.schemaOptions[0]?.value;
|
this.selectedSchema = schema?.value;
|
||||||
|
|
||||||
|
// only default selector to required values
|
||||||
|
if(!this.selectedSchema && this.schemas.length > 0 && this.required) {
|
||||||
|
this.selectedSchema = this.schemas[0].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
this.onSelectType(schema.value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -124,18 +177,37 @@
|
|||||||
this.onInput(value);
|
this.onInput(value);
|
||||||
},
|
},
|
||||||
resetSelectType() {
|
resetSelectType() {
|
||||||
this.selectedSchema = this.schemaOptions[0]?.value;
|
this.selectedSchema = undefined;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.onInput(undefined);
|
this.onInput(undefined);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
expose: [
|
expose: [
|
||||||
"resetSelectType",
|
"resetSelectType",
|
||||||
],
|
],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState("plugin", ["icons"]),
|
...mapState("plugin", ["icons"]),
|
||||||
|
schemas() {
|
||||||
|
if(!this.schema?.anyOf || !Array.isArray(this.schema.anyOf)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.schema.anyOf.map((schema) => {
|
||||||
|
|
||||||
|
if(schema.allOf && Array.isArray(schema.allOf)) {
|
||||||
|
if(schema.allOf.length === 2 && schema.allOf[0].$ref && !schema.allOf[1].$ref) {
|
||||||
|
return {
|
||||||
|
...schema.allOf[1],
|
||||||
|
$ref: schema.allOf[0].$ref,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
});
|
||||||
|
},
|
||||||
constantType() {
|
constantType() {
|
||||||
return this.currentSchema?.properties?.type?.const;
|
return this.currentSchema?.properties?.type?.const;
|
||||||
},
|
},
|
||||||
@@ -145,10 +217,8 @@
|
|||||||
}) : [];
|
}) : [];
|
||||||
},
|
},
|
||||||
currentSchema() {
|
currentSchema() {
|
||||||
return (
|
const rawSchema = this.definitions[this.selectedSchema] ?? this.schemaByType[this.selectedSchema]
|
||||||
this.definitions[this.selectedSchema] ??
|
return consolidateAllOfSchemas(rawSchema, this.definitions);
|
||||||
this.schemaByType[this.selectedSchema]
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
schemaByType() {
|
schemaByType() {
|
||||||
return this.schemas.reduce((acc, schema) => {
|
return this.schemas.reduce((acc, schema) => {
|
||||||
@@ -160,19 +230,23 @@
|
|||||||
return this.selectedSchema ? getTaskComponent(this.currentSchema) : undefined;
|
return this.selectedSchema ? getTaskComponent(this.currentSchema) : undefined;
|
||||||
},
|
},
|
||||||
isSelectingPlugins() {
|
isSelectingPlugins() {
|
||||||
return this.schemaOptions.some((schema) => schema.label.startsWith("io.kestra"));
|
return this.schemaOptions.some((schema) => schema.label.startsWith("io.kestra")) || this.schemas.length > 3;
|
||||||
},
|
},
|
||||||
schemaOptions() {
|
schemaOptions() {
|
||||||
|
if (!this.schemas?.length || !this.definitions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// find the part of the prefix to schema references that is common to all schemas
|
// find the part of the prefix to schema references that is common to all schemas
|
||||||
const schemaRefsArray = this.schemas
|
const schemaRefsArray = this.schemas
|
||||||
.map((schema) => schema.$ref?.split("/").pop() ?? schema.type)
|
?.map((schema) => schema.$ref?.split("/").pop() ?? schema.type)
|
||||||
.filter((schemaRef) => schemaRef)
|
.filter((schemaRef) => schemaRef)
|
||||||
.map((schemaRef) => this.definitions[schemaRef]?.type?.const ?? schemaRef)
|
.map((schemaRef) => this.definitions[schemaRef]?.type?.const ?? schemaRef)
|
||||||
.map((schemaRef) => schemaRef.split("."))
|
.map((schemaRef) => schemaRef.split("."))
|
||||||
|
|
||||||
let mismatch = false
|
let mismatch = false
|
||||||
const commonPart = schemaRefsArray[0]
|
const commonPart = schemaRefsArray[0]
|
||||||
.filter((schemaRef, index) => {
|
?.filter((schemaRef, index) => {
|
||||||
if(!mismatch && schemaRefsArray.every((item) => item[index] === schemaRef)){
|
if(!mismatch && schemaRefsArray.every((item) => item[index] === schemaRef)){
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -188,6 +262,14 @@
|
|||||||
? schema.$ref.split("/").pop()
|
? schema.$ref.split("/").pop()
|
||||||
: schema.type;
|
: schema.type;
|
||||||
|
|
||||||
|
if (!schemaRef) {
|
||||||
|
return {
|
||||||
|
label: "Unknown Schema",
|
||||||
|
value: "",
|
||||||
|
id: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const cleanSchemaRef = schemaRef.replace(/-\d+$/, "");
|
const cleanSchemaRef = schemaRef.replace(/-\d+$/, "");
|
||||||
|
|
||||||
const lastPartOfValue = cleanSchemaRef.slice(
|
const lastPartOfValue = cleanSchemaRef.slice(
|
||||||
@@ -199,6 +281,8 @@
|
|||||||
value: schemaRef,
|
value: schemaRef,
|
||||||
id: cleanSchemaRef,
|
id: cleanSchemaRef,
|
||||||
};
|
};
|
||||||
|
}).filter((schema) => {
|
||||||
|
return schema.value
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,26 +16,21 @@
|
|||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="items.length > 1 ? 20 : 22" class="pe-2">
|
<el-col :span="items.length > 1 ? 20 : 22" class="pe-2">
|
||||||
<el-select
|
<TaskWrapper :merge="!needWrapper">
|
||||||
v-if="$attrs?.schema?.items?.enum"
|
<template #tasks>
|
||||||
:model-value="element"
|
<component
|
||||||
@update:model-value="(v) => handleInput(v, index)"
|
:key="'array-' + index"
|
||||||
:placeholder="$t('value')"
|
:is="componentType"
|
||||||
>
|
:model-value="element"
|
||||||
<el-option
|
:task="modelValue"
|
||||||
v-for="item in $attrs.schema.items.enum.filter((i) => !items.includes(i))"
|
:root="`${root}[${index}]`"
|
||||||
:key="item"
|
:properties="{}"
|
||||||
:label="item"
|
:schema="props.schema.items"
|
||||||
:value="item"
|
:definitions="props.definitions"
|
||||||
/>
|
@update:model-value="handleInput($event, index)"
|
||||||
</el-select>
|
/>
|
||||||
<InputText
|
</template>
|
||||||
v-else
|
</TaskWrapper>
|
||||||
:model-value="element"
|
|
||||||
@update:model-value="(v) => handleInput(v, index)"
|
|
||||||
:placeholder="$t('value')"
|
|
||||||
class="w-100"
|
|
||||||
/>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2" class="d-flex align-items-center justify-content-center delete">
|
<el-col :span="2" class="d-flex align-items-center justify-content-center delete">
|
||||||
<DeleteOutline @click="removeItem(index)" />
|
<DeleteOutline @click="removeItem(index)" />
|
||||||
@@ -45,52 +40,90 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref} from "vue";
|
import {computed} from "vue";
|
||||||
|
|
||||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../code/utils/icons";
|
import {DeleteOutline, ChevronUp, ChevronDown} from "../../code/utils/icons";
|
||||||
|
|
||||||
import InputText from "../../code/components/inputs/InputText.vue";
|
|
||||||
import Add from "../../code/components/Add.vue";
|
import Add from "../../code/components/Add.vue";
|
||||||
|
import getTaskComponent from "./getTaskComponent";
|
||||||
|
import TaskWrapper from "./TaskWrapper.vue";
|
||||||
|
|
||||||
defineOptions({inheritAttrs: false});
|
defineOptions({inheritAttrs: false});
|
||||||
|
|
||||||
const emits = defineEmits(["update:modelValue"]);
|
const emits = defineEmits(["update:modelValue"]);
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
schema: any;
|
||||||
|
definitions: any;
|
||||||
modelValue?: (string | number | boolean | undefined)[] | string | number | boolean;
|
modelValue?: (string | number | boolean | undefined)[] | string | number | boolean;
|
||||||
|
required?: boolean;
|
||||||
|
root?: string;
|
||||||
}>(), {
|
}>(), {
|
||||||
modelValue: undefined
|
modelValue: undefined,
|
||||||
|
schema: () => ({}),
|
||||||
|
definitions: () => ({}),
|
||||||
|
required: false,
|
||||||
|
root: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = ref(
|
const componentType = computed(() => {
|
||||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
return getTaskComponent(props.schema.items, "", props.definitions);
|
||||||
|
});
|
||||||
|
|
||||||
|
const needWrapper = computed(() => {
|
||||||
|
return ![
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"boolean",
|
||||||
|
"expression",
|
||||||
|
].includes(componentType.value.ksTaskName)
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = computed(() =>
|
||||||
|
props.modelValue === undefined && !props.required
|
||||||
|
// we want to avoid displaying an item when
|
||||||
|
// modelValue is undefined
|
||||||
|
// if field is required though it invites users to fill it in
|
||||||
|
? []
|
||||||
|
: !Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInput = (value: string, index: number) => {
|
const handleInput = (value: string, index: number) => {
|
||||||
items.value[index] = value;
|
emits("update:modelValue", items.value.toSpliced(index, 1, value));
|
||||||
emits("update:modelValue", items.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const newEmptyValue = computed(() => {
|
||||||
|
if (props.schema.items?.type === "string") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return props.schema.items?.default ?? undefined;
|
||||||
|
})
|
||||||
|
|
||||||
const addItem = () => {
|
const addItem = () => {
|
||||||
items.value.push(undefined);
|
emits("update:modelValue", [...items.value, newEmptyValue.value]);
|
||||||
emits("update:modelValue", items.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeItem = (index: number) => {
|
const removeItem = (index: number) => {
|
||||||
items.value.splice(index, 1);
|
if (items.value.length <= 1) {
|
||||||
emits("update:modelValue", items.value);
|
emits("update:modelValue", undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emits("update:modelValue", items.value.toSpliced(index, 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveItem = (index: number, direction: "up" | "down") => {
|
const moveItem = (index: number, direction: "up" | "down") => {
|
||||||
|
const tempValue = items.value
|
||||||
if (direction === "up" && index > 0) {
|
if (direction === "up" && index > 0) {
|
||||||
[items.value[index - 1], items.value[index]] = [
|
[tempValue[index - 1], tempValue[index]] = [
|
||||||
items.value[index],
|
tempValue[index],
|
||||||
items.value[index - 1],
|
tempValue[index - 1],
|
||||||
];
|
];
|
||||||
} else if (direction === "down" && index < items.value.length - 1) {
|
} else if (direction === "down" && index < tempValue.length - 1) {
|
||||||
[items.value[index + 1], items.value[index]] = [
|
[tempValue[index + 1], tempValue[index]] = [
|
||||||
items.value[index],
|
tempValue[index],
|
||||||
items.value[index + 1],
|
tempValue[index + 1],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
emits("update:modelValue", items.value);
|
emits("update:modelValue", tempValue);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
:schema
|
:schema
|
||||||
:definitions
|
:definitions
|
||||||
:properties="computedProperties"
|
:properties="computedProperties"
|
||||||
|
:root="root"
|
||||||
|
:task="task"
|
||||||
|
:required="required"
|
||||||
merge
|
merge
|
||||||
@update:model-value="onInput"
|
@update:model-value="onInput"
|
||||||
/>
|
/>
|
||||||
@@ -16,21 +19,25 @@
|
|||||||
<script>
|
<script>
|
||||||
import Task from "./Task";
|
import Task from "./Task";
|
||||||
|
|
||||||
import {
|
|
||||||
BREADCRUMB_INJECTION_KEY,
|
|
||||||
PANEL_INJECTION_KEY,
|
|
||||||
} from "../../code/injectionKeys";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
inheritAttrs: false,
|
||||||
mixins: [Task],
|
mixins: [Task],
|
||||||
inject: {
|
|
||||||
panel: {from: PANEL_INJECTION_KEY},
|
|
||||||
breadcrumbs: {from: BREADCRUMB_INJECTION_KEY},
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
computedProperties() {
|
computedProperties() {
|
||||||
const type = this.schema.$ref.split("/").pop();
|
if(!this.schema?.allOf && !this.schema?.$ref) {
|
||||||
return this.definitions[type]?.properties;
|
return this.schema?.properties || {};
|
||||||
|
}
|
||||||
|
const schemas = this.schema.allOf ?? [this.schema];
|
||||||
|
return schemas.reduce((acc, item) => {
|
||||||
|
if (item.$ref) {
|
||||||
|
const type = item.$ref.split("/").pop();
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
...this.definitions[type]?.properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {...acc, ...item.properties};
|
||||||
|
}, {});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-row v-for="(item, index) in currentValue" :key="index" :gutter="10" class="w-100">
|
<el-alert
|
||||||
|
v-if="duplicatedKeys?.length"
|
||||||
|
:title="t('duplicate-pair', {label: t('key'), key: duplicatedKeys[0]})"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
<el-row v-for="(item, index) in currentValue" :key="index" :gutter="10" class="w-100" :data-testid="`task-dict-item-${item[0]}-${index}`">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<InputText
|
<InputText
|
||||||
:model-value="item[0]"
|
:model-value="item[0]"
|
||||||
@update:model-value="onKey(index, $event)"
|
@update:model-value="onKey(index, $event)"
|
||||||
@change="onKeyChange(index, $event)"
|
|
||||||
margin="m-0"
|
margin="m-0"
|
||||||
|
placeholder="Key"
|
||||||
|
:have-error="duplicatedKeys.includes(item[0])"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
<component
|
<component
|
||||||
|
:is="schema.additionalProperties ? getTaskComponent(schema.additionalProperties) : TaskExpression"
|
||||||
:is="schema.additionalProperties ? getTaskComponent(schema.additionalProperties, key, properties) : TaskExpression"
|
|
||||||
:model-value="item[1]"
|
:model-value="item[1]"
|
||||||
@update:model-value="onValueChange(index, $event)"
|
@update:model-value="onValueChange(index, $event)"
|
||||||
:root="getKey(item[0])"
|
:root="getKey(item[0])"
|
||||||
:schema="schema.additionalProperties"
|
:schema="schema.additionalProperties"
|
||||||
:required="isRequired(item[0])"
|
:required="isRequired(item[0])"
|
||||||
:definitions="definitions"
|
:definitions="definitions"
|
||||||
|
:disabled
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2" class="col align-self-center delete">
|
<el-col :span="2" class="col align-self-center delete">
|
||||||
@@ -27,107 +36,120 @@
|
|||||||
<Add v-if="!disabledAdding" @add="addItem()" />
|
<Add v-if="!disabledAdding" @add="addItem()" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
import {DeleteOutline} from "../../code/utils/icons";
|
import {DeleteOutline} from "../../code/utils/icons";
|
||||||
|
|
||||||
import InputText from "../../code/components/inputs/InputText.vue";
|
import InputText from "../../code/components/inputs/InputText.vue";
|
||||||
import TaskExpression from "./TaskExpression.vue";
|
import TaskExpression from "./TaskExpression.vue";
|
||||||
import Add from "../../code/components/Add.vue";
|
import Add from "../../code/components/Add.vue";
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import {toRaw} from "vue";
|
|
||||||
import Task from "./Task";
|
|
||||||
import getTaskComponent from "./getTaskComponent";
|
import getTaskComponent from "./getTaskComponent";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
function emptyValueObjectProvider() {
|
const {t} = useI18n();
|
||||||
return {"": undefined};
|
|
||||||
|
defineOptions({
|
||||||
|
name: "TaskDict",
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
definitions: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentValue = ref<[string, any][]>([])
|
||||||
|
|
||||||
|
// this flag will avoid updating the modelValue when the
|
||||||
|
// change was initiated in the component itself
|
||||||
|
const localEdit = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
if(localEdit.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localEdit.value = false;
|
||||||
|
if(newValue === undefined || newValue === null) {
|
||||||
|
currentValue.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentValue.value = Object.entries(newValue ?? {});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicatedKeys = computed(() => {
|
||||||
|
return currentValue.value.map(pair => pair[0])
|
||||||
|
.filter((key, index, self) =>
|
||||||
|
self.indexOf(key) !== index
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emitUpdate = debounce(function () {
|
||||||
|
if(duplicatedKeys.value?.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localEdit.value = true;
|
||||||
|
emit("update:modelValue", Object.fromEntries(currentValue.value.filter(pair => pair[0] !== "" && pair[1] !== undefined)));
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
function getKey(key: string) {
|
||||||
|
return props.root ? `${props.root}.${key}` : key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyValueEntriesProvider() {
|
function isRequired(key: string) {
|
||||||
return ["", undefined];
|
return props.schema?.required?.includes(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
function onKey(key: number, val: string) {
|
||||||
mixins: [Task],
|
currentValue.value[key][0] = val;
|
||||||
emits: ["update:modelValue"],
|
emitUpdate()
|
||||||
props: {
|
}
|
||||||
class: {
|
|
||||||
type: String,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentValue: undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.currentValue = Object.entries(toRaw(this.values));
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
disabledAdding() {
|
|
||||||
return !this.currentValue.at(-1)[0] || !this.currentValue.at(-1)[1];
|
|
||||||
},
|
|
||||||
values() {
|
|
||||||
if (this.modelValue === undefined) {
|
|
||||||
return emptyValueObjectProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.modelValue;
|
function onValueChange(key: number, val: any) {
|
||||||
},
|
currentValue.value[key][1] = val;
|
||||||
},
|
emitUpdate()
|
||||||
watch: {
|
}
|
||||||
modelValue() {
|
|
||||||
this.currentValue = Object.entries(toRaw(this.values));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
emitLocal(index, value) {
|
|
||||||
const local = this.currentValue.reduce(function (acc, cur, i) {
|
|
||||||
acc[i === index ? value : cur[0]] = cur[1];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
this.$emit("update:modelValue", local);
|
function removeItem(index: number) {
|
||||||
},
|
currentValue.value.splice(index, 1);
|
||||||
onValueChange(key, value) {
|
emitUpdate()
|
||||||
const local = this.currentValue || [];
|
}
|
||||||
local[key][1] = value;
|
|
||||||
this.currentValue = local;
|
|
||||||
|
|
||||||
this.emitLocal();
|
function addItem() {
|
||||||
},
|
currentValue.value.push(["", undefined]);
|
||||||
onKey(key, value) {
|
emitUpdate()
|
||||||
const local = this.currentValue || [];
|
}
|
||||||
local[key][0] = value;
|
|
||||||
this.currentValue = local;
|
|
||||||
},
|
|
||||||
onKeyChange(index, value) {
|
|
||||||
this.emitLocal(index, value);
|
|
||||||
},
|
|
||||||
addItem() {
|
|
||||||
const local = this.currentValue || [];
|
|
||||||
local.push(["", undefined]);
|
|
||||||
|
|
||||||
this.currentValue = local;
|
const disabledAdding = computed(() => {
|
||||||
|
return props.disabled || currentValue.value.at(-1)?.[0] === "" && currentValue.value.at(-1)?.[1] === undefined;
|
||||||
this.emitLocal();
|
});
|
||||||
},
|
|
||||||
removeItem(x) {
|
|
||||||
let local = this.currentValue || [];
|
|
||||||
if (local.length === 1) {
|
|
||||||
local = [emptyValueEntriesProvider()];
|
|
||||||
} else {
|
|
||||||
local.splice(x, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentValue = local;
|
|
||||||
|
|
||||||
this.emitLocal();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -10,36 +10,50 @@
|
|||||||
:large-suggestions="false"
|
:large-suggestions="false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script lang="ts" setup>
|
||||||
import Task from "./Task";
|
import {collapseEmptyValues} from "./Task";
|
||||||
import Editor from "../../../components/inputs/Editor.vue";
|
import Editor from "../../../components/inputs/Editor.vue";
|
||||||
import {YamlUtils as YAML_UTILS} from "@kestra-io/ui-libs";
|
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
mixins: [Task],
|
modelValue: {
|
||||||
components: {Editor},
|
type: [String, Object],
|
||||||
data() {
|
default: undefined
|
||||||
return {
|
|
||||||
localEditorValue: undefined
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
created() {
|
root: {
|
||||||
this.localEditorValue = this.editorValue;
|
type: String,
|
||||||
},
|
default: undefined
|
||||||
methods: {
|
|
||||||
editorInput(value) {
|
|
||||||
this.localEditorValue = value;
|
|
||||||
this.onInput(this.parseValue(value));
|
|
||||||
},
|
|
||||||
parseValue(value) {
|
|
||||||
if(value.match(/^\s*{{/)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return YAML_UTILS.parse(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
function editorInput(value: string) {
|
||||||
|
localEditorValue.value = value;
|
||||||
|
onInput(parseValue(value));
|
||||||
|
}
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
function onInput(value: any) {
|
||||||
|
emit("update:modelValue", collapseEmptyValues(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorValue = computed(() => {
|
||||||
|
if (typeof props.modelValue === "string") {
|
||||||
|
return props.modelValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return YAML_UTILS.stringify(props.modelValue);
|
||||||
|
})
|
||||||
|
|
||||||
|
const localEditorValue = ref(editorValue.value)
|
||||||
|
|
||||||
|
function parseValue(value: string) {
|
||||||
|
if(value.match(/^\s*{{/)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return YAML_UTILS.parse(value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
7
ui/src/components/flows/tasks/TaskKvPairs.vue
Normal file
7
ui/src/components/flows/tasks/TaskKvPairs.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<InputPair />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import InputPair from "../../code/components/inputs/InputPair.vue";
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<namespace-select
|
<NamespaceSelect
|
||||||
data-type="flow"
|
data-type="flow"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
allow-create
|
allow-create
|
||||||
@@ -58,6 +58,8 @@
|
|||||||
import TaskDict from "./TaskDict.vue";
|
import TaskDict from "./TaskDict.vue";
|
||||||
import TaskWrapper from "./TaskWrapper.vue";
|
import TaskWrapper from "./TaskWrapper.vue";
|
||||||
import TaskObjectField from "./TaskObjectField.vue";
|
import TaskObjectField from "./TaskObjectField.vue";
|
||||||
|
|
||||||
|
defineEmits(["update:modelValue"]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -114,7 +116,6 @@
|
|||||||
merge: {type: Boolean, default: false},
|
merge: {type: Boolean, default: false},
|
||||||
metadataInputs: {type: Boolean, default: false}
|
metadataInputs: {type: Boolean, default: false}
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeNames: [],
|
activeNames: [],
|
||||||
@@ -150,11 +151,12 @@
|
|||||||
"onUpdate:modelValue": (value) => {
|
"onUpdate:modelValue": (value) => {
|
||||||
this.onObjectInput(key, value);
|
this.onObjectInput(key, value);
|
||||||
},
|
},
|
||||||
|
root: this.root,
|
||||||
fieldKey: key,
|
fieldKey: key,
|
||||||
task: this.modelValue,
|
task: this.modelValue,
|
||||||
schema: schema,
|
schema: schema,
|
||||||
definitions: this.definitions,
|
definitions: this.definitions,
|
||||||
required: this.schema?.required,
|
required: this.requiredProperties.map(([p]) => p),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{{ props.fieldKey }}
|
{{ props.fieldKey }}
|
||||||
</span>
|
</span>
|
||||||
<ClearButton
|
<ClearButton
|
||||||
v-if="isAnyOf && !required"
|
v-if="isAnyOf && !required && modelValue && Object.keys(modelValue).length > 0"
|
||||||
@click="$emit('update:modelValue', undefined); taskComponent?.resetSelectType?.();"
|
@click="$emit('update:modelValue', undefined); taskComponent?.resetSelectType?.();"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
ref="taskComponent"
|
ref="taskComponent"
|
||||||
:is="type"
|
:is="type"
|
||||||
v-bind="{...componentProps}"
|
v-bind="{...componentProps}"
|
||||||
|
:disabled
|
||||||
class="mt-1 mb-2 wrapper"
|
class="mt-1 mb-2 wrapper"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -65,10 +66,12 @@
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
schema: any;
|
schema: any;
|
||||||
definitions: any;
|
definitions: any;
|
||||||
|
root?: string;
|
||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
task: any;
|
task: any;
|
||||||
modelValue?: Record<string, any> | string | number | boolean | Array<any>,
|
modelValue?: Record<string, any> | string | number | boolean | Array<any>,
|
||||||
required?: string[];
|
required?: string[];
|
||||||
|
disabled?: boolean;
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -78,7 +81,7 @@
|
|||||||
const taskComponent = templateRef<{resetSelectType?: () => void}>("taskComponent");
|
const taskComponent = templateRef<{resetSelectType?: () => void}>("taskComponent");
|
||||||
|
|
||||||
const required = computed(() => {
|
const required = computed(() => {
|
||||||
return props.required?.includes(props.fieldKey);
|
return props.required?.includes(props.fieldKey) && props.schema.$required
|
||||||
})
|
})
|
||||||
|
|
||||||
const componentProps = computed(() => {
|
const componentProps = computed(() => {
|
||||||
@@ -88,7 +91,7 @@
|
|||||||
emit("update:modelValue", value);
|
emit("update:modelValue", value);
|
||||||
},
|
},
|
||||||
task: props.task,
|
task: props.task,
|
||||||
root: props.fieldKey,
|
root: props.root ? `${props.root}.${props.fieldKey}` : props.fieldKey,
|
||||||
schema: props.schema,
|
schema: props.schema,
|
||||||
required: required.value,
|
required: required.value,
|
||||||
definitions: props.definitions
|
definitions: props.definitions
|
||||||
|
|||||||
@@ -1,15 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-if="schema.format === 'duration'">
|
<div class="wrapper">
|
||||||
|
<el-checkbox-button
|
||||||
|
v-if="['duration', 'date-time'].includes(schema.format)"
|
||||||
|
v-model="pebble"
|
||||||
|
:label="$t('no_code.toggle_pebble')"
|
||||||
|
:title="$t('no_code.toggle_pebble')"
|
||||||
|
class="ks-pebble"
|
||||||
|
>
|
||||||
|
<IconCodeBracesBox />
|
||||||
|
</el-checkbox-button>
|
||||||
<el-time-picker
|
<el-time-picker
|
||||||
|
v-if="!pebble && schema.format === 'duration'"
|
||||||
:model-value="durationValue"
|
:model-value="durationValue"
|
||||||
type="time"
|
type="time"
|
||||||
:default-value="defaultDuration"
|
:default-value="defaultDuration"
|
||||||
:placeholder="`Choose a${/^[aeiou]/i.test(root || '') ? 'n' : ''} ${root || 'duration'}`"
|
:placeholder="`Choose a${/^[aeiou]/i.test(root || '') ? 'n' : ''} ${root || 'duration'}`"
|
||||||
@update:model-value="onInputDuration"
|
@update:model-value="onInputDuration"
|
||||||
/>
|
/>
|
||||||
</template>
|
<el-date-picker
|
||||||
<template v-else>
|
v-else-if="!pebble && schema.format === 'date-time'"
|
||||||
|
:model-value="modelValue"
|
||||||
|
type="date"
|
||||||
|
:placeholder="`Choose a${/^[aeiou]/i.test(root || '') ? 'n' : ''} ${root || 'date'}`"
|
||||||
|
@update:model-value="onInput($event.toISOString())"
|
||||||
|
/>
|
||||||
|
<InputText
|
||||||
|
v-else-if="disabled"
|
||||||
|
:model-value="modelValue"
|
||||||
|
disabled
|
||||||
|
class="w-100 disabled-field"
|
||||||
|
/>
|
||||||
<editor
|
<editor
|
||||||
|
v-else
|
||||||
:model-value="editorValue"
|
:model-value="editorValue"
|
||||||
:navbar="false"
|
:navbar="false"
|
||||||
:full-height="false"
|
:full-height="false"
|
||||||
@@ -17,20 +39,44 @@
|
|||||||
schema-type="flow"
|
schema-type="flow"
|
||||||
lang="plaintext"
|
lang="plaintext"
|
||||||
input
|
input
|
||||||
:placeholder="`Your ${root || 'value'} here...`"
|
|
||||||
@update:model-value="onInput"
|
@update:model-value="onInput"
|
||||||
:large-suggestions="false"
|
:large-suggestions="false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Editor from "../../../components/inputs/Editor.vue";
|
||||||
|
import InputText from "../../code/components/inputs/InputText.vue";
|
||||||
|
import IconCodeBracesBox from "vue-material-design-icons/CodeBracesBox.vue";
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
import Task from "./Task";
|
import Task from "./Task";
|
||||||
import Editor from "../../../components/inputs/Editor.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
inheritAttrs: false,
|
||||||
mixins: [Task],
|
mixins: [Task],
|
||||||
components: {Editor},
|
components: {Editor},
|
||||||
|
props:{
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pebble: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
|
mounted(){
|
||||||
|
if(!["duration", "date-time"].includes(this.schema.format) || !this.modelValue){
|
||||||
|
this.pebble = false;
|
||||||
|
} else if( this.schema.format === "duration" && this.values) {
|
||||||
|
this.pebble = !this.$moment.duration(this.modelValue).isValid();
|
||||||
|
} else if (this.schema.format === "date-time" && this.values) {
|
||||||
|
this.pebble = isNaN(Date.parse(this.modelValue));
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isValid() {
|
isValid() {
|
||||||
if (this.required && !this.modelValue) {
|
if (this.required && !this.modelValue) {
|
||||||
@@ -94,4 +140,38 @@
|
|||||||
:deep(.placeholder) {
|
:deep(.placeholder) {
|
||||||
top: -7px !important;
|
top: -7px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 1px solid var(--ks-border-primary);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:deep(.disabled-field) {
|
||||||
|
margin: 0!important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper),
|
||||||
|
:deep(.editor-container) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox-button__inner) {
|
||||||
|
padding: 4px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-pebble:deep(span:hover){
|
||||||
|
color: var(--ks-content-link-hover) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-pebble * {
|
||||||
|
font-size: 24px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,71 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-input :model-value="JSON.stringify(values)">
|
<div class="w-100">
|
||||||
<template #append>
|
<Element
|
||||||
<el-button :icon="TextSearch" @click="isOpen = true" />
|
:section="root"
|
||||||
</template>
|
block-type="tasks"
|
||||||
</el-input>
|
:parent-path-complete="parentPathComplete"
|
||||||
|
:element="{
|
||||||
<drawer
|
id: model?.id ?? 'Set a task',
|
||||||
v-if="isOpen"
|
type: model?.type,
|
||||||
v-model="isOpen"
|
}"
|
||||||
>
|
@remove-element="removeElement()"
|
||||||
<template #header>
|
/>
|
||||||
<code>{{ root }}</code>
|
</div>
|
||||||
</template>
|
|
||||||
<el-form label-position="top">
|
|
||||||
<task-editor
|
|
||||||
ref="editor"
|
|
||||||
:model-value="taskYaml"
|
|
||||||
:section="section"
|
|
||||||
@update:model-value="onInput"
|
|
||||||
/>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button :icon="ContentSave" @click="isOpen = false" type="primary">
|
|
||||||
{{ $t('save') }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</drawer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import {SECTIONS} from "@kestra-io/ui-libs";
|
import {computed, inject} from "vue";
|
||||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
import {
|
||||||
|
PARENT_PATH_INJECTION_KEY,
|
||||||
|
REF_PATH_INJECTION_KEY,
|
||||||
|
CREATING_TASK_INJECTION_KEY
|
||||||
|
} from "../../code/injectionKeys";
|
||||||
|
import Element from "../../code/components/collapse/Element.vue";
|
||||||
|
|
||||||
import TextSearch from "vue-material-design-icons/TextSearch.vue";
|
const model = defineModel({
|
||||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
type: Object,
|
||||||
import TaskEditor from "../TaskEditor.vue"
|
default: () => ({})
|
||||||
import Drawer from "../../Drawer.vue"
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
root: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
|
||||||
|
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
||||||
|
const creatingTask = inject(CREATING_TASK_INJECTION_KEY, false);
|
||||||
|
|
||||||
|
const parentPathComplete = computed(() => {
|
||||||
|
return `${[
|
||||||
|
[
|
||||||
|
parentPath,
|
||||||
|
creatingTask && refPath !== undefined
|
||||||
|
? `[${refPath + 1}]`
|
||||||
|
: refPath !== undefined
|
||||||
|
? `[${refPath}]`
|
||||||
|
: undefined,
|
||||||
|
].filter(Boolean).join(""),
|
||||||
|
props.root,
|
||||||
|
].filter(p => p.length).join(".")}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function removeElement() {
|
||||||
|
model.value = undefined;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
|
||||||
import Task from "./Task"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
inheritAttrs: false,
|
|
||||||
mixins: [Task],
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
props: {
|
|
||||||
section: {
|
|
||||||
type: String,
|
|
||||||
default: SECTIONS.TASKS
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isOpen: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
taskYaml() {
|
|
||||||
return YAML_UTILS.stringify(this.modelValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onInput(value) {
|
|
||||||
this.$emit("update:modelValue", YAML_UTILS.parse(value));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<Collapse
|
<Collapse
|
||||||
title="tasks"
|
title="tasks"
|
||||||
:elements="items"
|
:elements="items"
|
||||||
section="tasks"
|
:section
|
||||||
block-type="tasks"
|
block-type="tasks"
|
||||||
@remove="(yaml) => store.commit('flow/setFlowYaml', yaml)"
|
@remove="(yaml) => store.commit('flow/setFlowYaml', yaml)"
|
||||||
@reorder="(yaml) => store.commit('flow/setFlowYaml', yaml)"
|
@reorder="(yaml) => store.commit('flow/setFlowYaml', yaml)"
|
||||||
@@ -16,21 +16,32 @@
|
|||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
import Collapse from "../../code/components/collapse/Collapse.vue";
|
import Collapse from "../../code/components/collapse/Collapse.vue";
|
||||||
|
|
||||||
defineOptions({inheritAttrs: false});
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
});
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
interface Task {id:string, type:string}
|
interface Task {
|
||||||
|
id:string,
|
||||||
|
type:string
|
||||||
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
modelValue?: Task[]
|
modelValue?: Task[],
|
||||||
|
root?: string;
|
||||||
}>(), {
|
}>(), {
|
||||||
modelValue: () => []
|
modelValue: () => [],
|
||||||
|
root: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = computed(() =>
|
const items = computed(() =>
|
||||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const section = computed(() => {
|
||||||
|
return props.root ?? "tasks";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -13,13 +13,14 @@
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.schema-wrapper {
|
.schema-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
padding-bottom: 1rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 1rem 0;
|
|
||||||
background: var(--ks-background-box);
|
|
||||||
}
|
}
|
||||||
.bordered {
|
.bordered {
|
||||||
|
background: var(--ks-background-box);
|
||||||
border: 1px solid var(--ks-border-secondary);
|
border: 1px solid var(--ks-border-secondary);
|
||||||
box-shadow: 0 0 0 1px var(--ks-border-primary) inset;
|
box-shadow: 0 0 0 1px var(--ks-border-primary) inset;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {pascalCase} from "change-case";
|
import {pascalCase} from "change-case";
|
||||||
import InputPair from "../../code/components/inputs/InputPair.vue";
|
|
||||||
|
|
||||||
const TasksComponents = import.meta.glob<{default: any}>("./Task*.vue", {eager: true});
|
const TasksComponents = import.meta.glob<{default: any}>("./Task*.vue", {eager: true});
|
||||||
|
|
||||||
@@ -24,7 +23,18 @@ function getType(property: any, key?: string, schema?: any): string {
|
|||||||
return "complex";
|
return "complex";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if( Object.prototype.hasOwnProperty.call(property, "allOf")) {
|
||||||
|
if (property.allOf.length === 2
|
||||||
|
&& property.allOf[0].$ref && !property.allOf[1].properties) {
|
||||||
|
return "complex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(property, "anyOf")) {
|
if (Object.prototype.hasOwnProperty.call(property, "anyOf")) {
|
||||||
|
if( key === "labels" && property.anyOf.length === 2
|
||||||
|
&& property.anyOf[0].type === "array" && property.anyOf[1].type === "object") {
|
||||||
|
return "KV-pairs";
|
||||||
|
}
|
||||||
return "any-of";
|
return "any-of";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +47,7 @@ function getType(property: any, key?: string, schema?: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (key === "namespace") {
|
if (key === "namespace") {
|
||||||
return "subflow-namespace";
|
return "namespace";
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = Object.keys(schema?.properties ?? {});
|
const properties = Object.keys(schema?.properties ?? {});
|
||||||
@@ -55,7 +65,8 @@ function getType(property: any, key?: string, schema?: any): string {
|
|||||||
return "tasks";
|
return "tasks";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property.items?.$ref?.includes("conditions.Condition")) {
|
if (property.items?.$ref?.includes("conditions.Condition")
|
||||||
|
|| property.items.anyOf?.every((item: any) => item.$ref?.includes("io.kestra.plugin.core.condition"))) {
|
||||||
return "conditions";
|
return "conditions";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +78,7 @@ function getType(property: any, key?: string, schema?: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if( property.type === "object" && !property.properties) {
|
if( property.type === "object" && !property.properties) {
|
||||||
return "input-pair";
|
return "KV-pairs";
|
||||||
}
|
}
|
||||||
|
|
||||||
return property.type || "expression";
|
return property.type || "expression";
|
||||||
@@ -75,13 +86,10 @@ function getType(property: any, key?: string, schema?: any): string {
|
|||||||
|
|
||||||
export default function getTaskComponent(property: any, key?: string, schema?: any) {
|
export default function getTaskComponent(property: any, key?: string, schema?: any) {
|
||||||
const typeString = getType(property, key, schema);
|
const typeString = getType(property, key, schema);
|
||||||
if( typeString === "input-pair") {
|
|
||||||
return InputPair;
|
|
||||||
}
|
|
||||||
const type = pascalCase(typeString);
|
const type = pascalCase(typeString);
|
||||||
const component = TasksComponents[`./Task${type}.vue`]?.default;
|
const component = TasksComponents[`./Task${type}.vue`]?.default;
|
||||||
if (component) {
|
if (component) {
|
||||||
component.ksTaskName = typeString;
|
component.ksTaskName = typeString;
|
||||||
}
|
}
|
||||||
return component
|
return component ?? {}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import {defineAsyncComponent, h, markRaw, Ref, Suspense} from "vue"
|
import {h, markRaw, Ref, Suspense} from "vue"
|
||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import MouseRightClickIcon from "vue-material-design-icons/MouseRightClick.vue";
|
import MouseRightClickIcon from "vue-material-design-icons/MouseRightClick.vue";
|
||||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
import type {Panel, Tab} from "../MultiPanelTabs.vue";
|
import type {Panel, Tab} from "../MultiPanelTabs.vue";
|
||||||
import {BlockType} from "../code/utils/types";
|
import {BlockType} from "../code/utils/types";
|
||||||
|
import NoCodeWrapper from "../code/NoCodeWrapper.vue"
|
||||||
|
|
||||||
import type {NoCodeProps} from "../code/NoCodeWrapper.vue";
|
import type {NoCodeProps} from "../code/NoCodeWrapper.vue";
|
||||||
|
|
||||||
const NoCodeWrapper = markRaw(defineAsyncComponent(() => import("../code/NoCodeWrapper.vue")))
|
|
||||||
|
|
||||||
|
|
||||||
const NOCODE_PREFIX = "nocode"
|
const NOCODE_PREFIX = "nocode"
|
||||||
@@ -20,7 +20,7 @@ interface Opener {
|
|||||||
|
|
||||||
interface Handlers {
|
interface Handlers {
|
||||||
onCreateTask: (opener: Opener, blockType: BlockType | "pluginDefaults", parentPath: string, refPath?: number, position?: "before" | "after") => boolean,
|
onCreateTask: (opener: Opener, blockType: BlockType | "pluginDefaults", parentPath: string, refPath?: number, position?: "before" | "after") => boolean,
|
||||||
onEditTask: (opener: Opener, blockType: BlockType | "pluginDefaults", parentPath: string, refPath: number) => boolean
|
onEditTask: (opener: Opener, blockType: BlockType | "pluginDefaults", parentPath: string, refPath?: number) => boolean
|
||||||
onCloseTask: (opener: Opener) => boolean
|
onCloseTask: (opener: Opener) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +67,15 @@ export function getTabFromNoCodeTab(tab: NoCodeTabWithAction, t: (key: string) =
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if (tab.action === "edit") {
|
} else if (tab.action === "edit") {
|
||||||
|
const path = tab.refPath !== undefined
|
||||||
|
? `${tab.parentPath}[${tab.refPath}]`
|
||||||
|
: tab.parentPath ?? ""
|
||||||
|
|
||||||
const currentBlock: any = tab.parentPath ? YAML_UTILS.parse(YAML_UTILS.extractBlockWithPath({
|
const currentBlock: any = tab.parentPath ? YAML_UTILS.parse(YAML_UTILS.extractBlockWithPath({
|
||||||
source: flow,
|
source: flow,
|
||||||
path: `${tab.parentPath}[${tab.refPath}]`,
|
path,
|
||||||
})) : {}
|
})) : {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: getEditTabKey(tab, keepAliveCacheBuster++),
|
value: getEditTabKey(tab, keepAliveCacheBuster++),
|
||||||
button: {
|
button: {
|
||||||
@@ -102,6 +107,7 @@ export function getTabFromNoCodeTab(tab: NoCodeTabWithAction, t: (key: string) =
|
|||||||
[h(NoCodeWrapper, {
|
[h(NoCodeWrapper, {
|
||||||
...restOfTab,
|
...restOfTab,
|
||||||
creatingTask: tab.action === "create",
|
creatingTask: tab.action === "create",
|
||||||
|
editingTask: tab.action === "edit",
|
||||||
onCloseTask: onCloseTask?.bind({}, props),
|
onCloseTask: onCloseTask?.bind({}, props),
|
||||||
onCreateTask: onCreateTask?.bind({}, props) as any,
|
onCreateTask: onCreateTask?.bind({}, props) as any,
|
||||||
onEditTask: onEditTask?.bind({}, props) as any,
|
onEditTask: onEditTask?.bind({}, props) as any,
|
||||||
@@ -178,7 +184,7 @@ export function useNoCodePanels(panels: Ref<Panel[]>, handlers: Handlers) {
|
|||||||
openerPanel.activeTab = tab
|
openerPanel.activeTab = tab
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditTaskTab(opener: { panelIndex: number, tabIndex: number }, blockType: BlockType | "pluginDefaults", parentPath: string, refPath: number, dirty: boolean = false) {
|
function openEditTaskTab(opener: { panelIndex: number, tabIndex: number }, blockType: BlockType | "pluginDefaults", parentPath: string, refPath?: number, dirty: boolean = false) {
|
||||||
const tab = getTabFromNoCodeTab({
|
const tab = getTabFromNoCodeTab({
|
||||||
action: "edit",
|
action: "edit",
|
||||||
blockType,
|
blockType,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {defineAsyncComponent, shallowRef} from "vue";
|
import {shallowRef} from "vue";
|
||||||
import UnfoldLessHorizontal from "vue-material-design-icons/UnfoldLessHorizontal.vue";
|
import UnfoldLessHorizontal from "vue-material-design-icons/UnfoldLessHorizontal.vue";
|
||||||
import UnfoldMoreHorizontal from "vue-material-design-icons/UnfoldMoreHorizontal.vue";
|
import UnfoldMoreHorizontal from "vue-material-design-icons/UnfoldMoreHorizontal.vue";
|
||||||
import Help from "vue-material-design-icons/Help.vue";
|
import Help from "vue-material-design-icons/Help.vue";
|
||||||
@@ -83,8 +83,7 @@
|
|||||||
import BookMultipleOutline from "vue-material-design-icons/BookMultipleOutline.vue";
|
import BookMultipleOutline from "vue-material-design-icons/BookMultipleOutline.vue";
|
||||||
import Close from "vue-material-design-icons/Close.vue";
|
import Close from "vue-material-design-icons/Close.vue";
|
||||||
import {TabFocus} from "monaco-editor/esm/vs/editor/browser/config/tabFocus.js";
|
import {TabFocus} from "monaco-editor/esm/vs/editor/browser/config/tabFocus.js";
|
||||||
|
import MonacoEditor from "./MonacoEditor.vue";
|
||||||
const MonacoEditor = defineAsyncComponent(() => import("./MonacoEditor.vue"));
|
|
||||||
|
|
||||||
import Utils from "../../utils/utils";
|
import Utils from "../../utils/utils";
|
||||||
|
|
||||||
@@ -142,6 +141,7 @@
|
|||||||
plugin: undefined,
|
plugin: undefined,
|
||||||
taskType: undefined,
|
taskType: undefined,
|
||||||
themeComputed: Utils.getTheme(),
|
themeComputed: Utils.getTheme(),
|
||||||
|
preventCursorChange: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -154,6 +154,13 @@
|
|||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
|
modelValue(value) {
|
||||||
|
if (this.editor?.getValue() !== value) {
|
||||||
|
this.preventCursorChange = true;
|
||||||
|
} else {
|
||||||
|
this.preventCursorChange = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({mappedTheme: state => state.misc.theme}),
|
...mapState({mappedTheme: state => state.misc.theme}),
|
||||||
@@ -427,9 +434,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.editor.onDidChangeCursorPosition?.(() => {
|
this.editor.onDidChangeCursorPosition?.(() => {
|
||||||
|
clearTimeout(this.lastTimeout);
|
||||||
|
if(this.preventCursorChange) {
|
||||||
|
this.preventCursorChange = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
let position = this.editor.getPosition();
|
let position = this.editor.getPosition();
|
||||||
let model = this.editor.getModel();
|
let model = this.editor.getModel();
|
||||||
clearTimeout(this.lastTimeout);
|
|
||||||
this.lastTimeout = setTimeout(() => {
|
this.lastTimeout = setTimeout(() => {
|
||||||
this.$emit("cursor", {
|
this.$emit("cursor", {
|
||||||
position: position,
|
position: position,
|
||||||
@@ -555,7 +566,7 @@
|
|||||||
padding-top: 7px;
|
padding-top: 7px;
|
||||||
|
|
||||||
&.custom-dark-vs-theme {
|
&.custom-dark-vs-theme {
|
||||||
background-color: var(--ks-background-input);
|
background-color: var(--ks-background-input);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-light {
|
&.theme-light {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
:teleported="false"
|
:teleported="false"
|
||||||
:default-value="nowMoment.toDate()"
|
:default-value="nowMoment.toDate()"
|
||||||
@change="datePickerCallback"
|
@change="datePickerCallback"
|
||||||
@keydown.esc.prevent="editorResolved.focus()"
|
@keydown.esc.prevent="editorResolved?.focus()"
|
||||||
@keydown.enter.prevent="datePickerCallback"
|
@keydown.enter.prevent="datePickerCallback"
|
||||||
:clearable="false"
|
:clearable="false"
|
||||||
class="z-3"
|
class="z-3"
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
import {Moment} from "moment";
|
import {Moment} from "moment";
|
||||||
import PlaceholderContentWidget from "../../composables/monaco/PlaceholderContentWidget.ts";
|
import PlaceholderContentWidget from "../../composables/monaco/PlaceholderContentWidget.ts";
|
||||||
import ICodeEditor = editor.ICodeEditor;
|
import ICodeEditor = editor.ICodeEditor;
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
import {hashCode} from "../../utils/global.ts";
|
import {hashCode} from "../../utils/global.ts";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -168,7 +169,7 @@
|
|||||||
base: kestraBaseTheme.base
|
base: kestraBaseTheme.base
|
||||||
}
|
}
|
||||||
: theme as Partial<editor.IStandaloneThemeData> & { base: editor.BuiltinTheme };
|
: theme as Partial<editor.IStandaloneThemeData> & { base: editor.BuiltinTheme };
|
||||||
|
|
||||||
const themeId = hashCode(JSON.stringify(theme)).toString();
|
const themeId = hashCode(JSON.stringify(theme)).toString();
|
||||||
monaco.editor.defineTheme(themeId, {
|
monaco.editor.defineTheme(themeId, {
|
||||||
inherit: true,
|
inherit: true,
|
||||||
@@ -176,7 +177,7 @@
|
|||||||
colors: {},
|
colors: {},
|
||||||
...base
|
...base
|
||||||
});
|
});
|
||||||
|
|
||||||
return themeId;
|
return themeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +245,7 @@
|
|||||||
watch(() => props.theme, (newTheme) => {
|
watch(() => props.theme, (newTheme) => {
|
||||||
if (typeof newTheme === "object") {
|
if (typeof newTheme === "object") {
|
||||||
const themeId = defineCustomTheme(newTheme);
|
const themeId = defineCustomTheme(newTheme);
|
||||||
|
|
||||||
if (editorResolved.value) {
|
if (editorResolved.value) {
|
||||||
monaco.editor.setTheme(themeId);
|
monaco.editor.setTheme(themeId);
|
||||||
}
|
}
|
||||||
@@ -303,7 +304,7 @@
|
|||||||
node.querySelector(`.${KESTRA_ICON_WRAPPER_CLASS}`)?.remove();
|
node.querySelector(`.${KESTRA_ICON_WRAPPER_CLASS}`)?.remove();
|
||||||
|
|
||||||
if (completionValue.includes(".") && !completionValue.includes("{")) {
|
if (completionValue.includes(".") && !completionValue.includes("{")) {
|
||||||
if (store.state.plugin.icons[completionValue] !== undefined) {
|
if (store.state.plugin?.icons?.[completionValue] !== undefined) {
|
||||||
replaceRowIcon(vsCodeIcon, h(TaskIcon, {
|
replaceRowIcon(vsCodeIcon, h(TaskIcon, {
|
||||||
cls: completionValue,
|
cls: completionValue,
|
||||||
"only-icon": true,
|
"only-icon": true,
|
||||||
@@ -464,6 +465,12 @@
|
|||||||
(window as any).clearEditor = () => {
|
(window as any).clearEditor = () => {
|
||||||
localEditor?.getModel()?.setValue("")
|
localEditor?.getModel()?.setValue("")
|
||||||
};
|
};
|
||||||
|
(window as any).acceptSuggestion = () => {
|
||||||
|
localEditor?.trigger("acceptSelectedSuggestion", "acceptSelectedSuggestion", {});
|
||||||
|
};
|
||||||
|
(window as any).nextSuggestion = () => {
|
||||||
|
localEditor?.trigger("selectNextSuggestion", "selectNextSuggestion", {});
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(function () {
|
onBeforeUnmount(function () {
|
||||||
@@ -657,12 +664,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
localEditor.onDidChangeCursorPosition(() => {
|
localEditor.onDidChangeCursorPosition(debounce(() => {
|
||||||
if (suggestController.model.state !== 0) {
|
if (suggestController.model.state !== 0) {
|
||||||
suggestController.cancelSuggestWidget();
|
suggestController.cancelSuggestWidget();
|
||||||
localEditor!.trigger("refreshSuggestionsOnCursorMove", "editor.action.triggerSuggest", {});
|
localEditor!.trigger("refreshSuggestionsOnCursorMove", "editor.action.triggerSuggest", {});
|
||||||
}
|
}
|
||||||
})
|
}, 300))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.input) {
|
if (!props.input) {
|
||||||
@@ -797,4 +804,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<component :is="autoRefresh ? 'auto-renew' : 'auto-renew-off'" class="auto-refresh-icon" />
|
<component :is="autoRefresh ? 'auto-renew' : 'auto-renew-off'" class="auto-refresh-icon" />
|
||||||
</kicon>
|
</kicon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="triggerRefresh" data-test-id="trigger-refresh-button">
|
<el-button @click="triggerRefresh" data-test-id="trigger-refresh-button" data-testid="trigger-refresh-button">
|
||||||
<kicon :tooltip="$t('trigger refresh')" placement="bottom">
|
<kicon :tooltip="$t('trigger refresh')" placement="bottom">
|
||||||
<refresh />
|
<refresh />
|
||||||
</kicon>
|
</kicon>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, Ref, onMounted} from "vue";
|
import {ref, computed, Ref, watch, onMounted} from "vue";
|
||||||
|
|
||||||
import {useTabs} from "override/components/namespaces/useTabs";
|
import {useTabs} from "override/components/namespaces/useTabs";
|
||||||
import {useHelpers} from "./utils/useHelpers";
|
import {useHelpers} from "./utils/useHelpers";
|
||||||
@@ -25,11 +25,17 @@
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const context = ref({title: details.title});
|
const context = ref({title: details.value.title});
|
||||||
useRouteContext(context);
|
useRouteContext(context);
|
||||||
|
|
||||||
const namespace = computed(() => route.params?.id) as Ref<string>;
|
const namespace = computed(() => route.params?.id) as Ref<string>;
|
||||||
|
|
||||||
|
watch(namespace, (newID) => {
|
||||||
|
if (newID) {
|
||||||
|
store.dispatch("namespace/load", newID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (namespace.value) {
|
if (namespace.value) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Component} from "vue";
|
import {Component, computed, Ref} from "vue";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
@@ -45,30 +45,30 @@ export function useHelpers() {
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
|
|
||||||
const namespace = route.params?.id as string;
|
const namespace = computed(() => route.params?.id) as Ref<string>;
|
||||||
|
|
||||||
const parts = namespace?.split(".") ?? [];
|
const parts = computed(() => namespace.value?.split(".") ?? []);
|
||||||
const details: Details = {
|
const details: Ref<Details> = computed(() => ({
|
||||||
title: parts.at(-1) || t("namespaces"),
|
title: parts.value.at(-1) || t("namespaces"),
|
||||||
breadcrumb: [
|
breadcrumb: [
|
||||||
{label: t("namespaces"), link: {name: "namespaces/list"}},
|
{label: t("namespaces"), link: {name: "namespaces/list"}},
|
||||||
...parts.map((_: string, index: number) => ({
|
...parts.value.map((_: string, index: number) => ({
|
||||||
label: parts[index],
|
label: parts.value[index],
|
||||||
link: {
|
link: {
|
||||||
name: "namespaces/update",
|
name: "namespaces/update",
|
||||||
params: {
|
params: {
|
||||||
id: parts.slice(0, index + 1).join("."),
|
id: parts.value.slice(0, index + 1).join("."),
|
||||||
tab: "overview",
|
tab: "overview",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
disabled: index === parts.length - 1,
|
disabled: index === parts.value.length - 1,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
};
|
}));
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
// If it's a system namespace, include the blueprints tab
|
// If it's a system namespace, include the blueprints tab
|
||||||
...(namespace === "system"
|
...(namespace.value === "system"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: "blueprints",
|
name: "blueprints",
|
||||||
@@ -88,26 +88,34 @@ export function useHelpers() {
|
|||||||
name: "flows",
|
name: "flows",
|
||||||
title: t("flows"),
|
title: t("flows"),
|
||||||
component: Flows,
|
component: Flows,
|
||||||
props: {namespace, topbar: false},
|
props: {namespace: namespace.value, topbar: false},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "executions",
|
name: "executions",
|
||||||
title: t("executions"),
|
title: t("executions"),
|
||||||
component: Executions,
|
component: Executions,
|
||||||
props: {namespace, topbar: false, visibleCharts: true},
|
props: {
|
||||||
|
namespace: namespace.value,
|
||||||
|
topbar: false,
|
||||||
|
visibleCharts: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dependencies",
|
name: "dependencies",
|
||||||
title: t("dependencies"),
|
title: t("dependencies"),
|
||||||
component: Dependencies,
|
component: Dependencies,
|
||||||
props: {namespace, type: "dependencies"},
|
props: {namespace: namespace.value, type: "dependencies"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
maximized: true,
|
maximized: true,
|
||||||
name: "files",
|
name: "files",
|
||||||
title: t("files"),
|
title: t("files"),
|
||||||
component: EditorView,
|
component: EditorView,
|
||||||
props: {namespace, isNamespace: true, isReadOnly: false},
|
props: {
|
||||||
|
namespace: namespace.value,
|
||||||
|
isNamespace: true,
|
||||||
|
isReadOnly: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="plugin-doc">
|
<div class="plugin-doc">
|
||||||
<template v-if="editorPlugin">
|
<template v-if="fetchPluginDocumentation && editorPlugin">
|
||||||
<div class="d-flex gap-3 mb-3 align-items-center">
|
<div class="d-flex gap-3 mb-3 align-items-center">
|
||||||
<task-icon
|
<task-icon
|
||||||
class="plugin-icon"
|
class="plugin-icon"
|
||||||
@@ -42,6 +42,10 @@
|
|||||||
absolute: {
|
absolute: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
fetchPluginDocumentation: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -60,11 +60,18 @@ export class FilterKeyCompletions {
|
|||||||
private readonly _comparators: Comparators[];
|
private readonly _comparators: Comparators[];
|
||||||
private readonly _valuesFetcher: (store: Store<Record<string, any>>, hardcodedValues: ReturnType<typeof useValues>["VALUES"]) => Promise<ValueCompletions>;
|
private readonly _valuesFetcher: (store: Store<Record<string, any>>, hardcodedValues: ReturnType<typeof useValues>["VALUES"]) => Promise<ValueCompletions>;
|
||||||
private readonly _allowMultipleValues: boolean;
|
private readonly _allowMultipleValues: boolean;
|
||||||
|
private readonly _forbiddenConcurrentKeys: string[];
|
||||||
|
|
||||||
constructor(comparators: Comparators[], valuesFetcher: (store: Store<Record<string, any>>, hardcodedValues: ReturnType<typeof useValues>["VALUES"]) => Promise<ValueCompletions> = async () => [], allowMultipleValues?: boolean) {
|
constructor(
|
||||||
|
comparators: Comparators[],
|
||||||
|
valuesFetcher: (store: Store<Record<string, any>>, hardcodedValues: ReturnType<typeof useValues>["VALUES"]) => Promise<ValueCompletions> = async () => [],
|
||||||
|
allowMultipleValues?: boolean,
|
||||||
|
forbiddenConcurrentKeys: string[] = []
|
||||||
|
) {
|
||||||
this._comparators = comparators;
|
this._comparators = comparators;
|
||||||
this._valuesFetcher = valuesFetcher;
|
this._valuesFetcher = valuesFetcher;
|
||||||
this._allowMultipleValues = allowMultipleValues ?? false;
|
this._allowMultipleValues = allowMultipleValues ?? false;
|
||||||
|
this._forbiddenConcurrentKeys = forbiddenConcurrentKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
get comparators(): Comparators[] {
|
get comparators(): Comparators[] {
|
||||||
@@ -78,4 +85,8 @@ export class FilterKeyCompletions {
|
|||||||
get allowMultipleValues(): boolean {
|
get allowMultipleValues(): boolean {
|
||||||
return this._allowMultipleValues;
|
return this._allowMultipleValues;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
get forbiddenConcurrentKeys(): string[] {
|
||||||
|
return this._forbiddenConcurrentKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ export abstract class FilterLanguage {
|
|||||||
return this._filterKeyCompletions.map(([{regex}]) => regex);
|
return this._filterKeyCompletions.map(([{regex}]) => regex);
|
||||||
}
|
}
|
||||||
|
|
||||||
async keyCompletion(): Promise<Completion[]> {
|
async keyCompletion(usedKeys: string[] = []): Promise<Completion[]> {
|
||||||
return this._filterKeyCompletions
|
return this._filterKeyCompletions
|
||||||
.map(([{key}, {comparators}]) => {
|
.filter(([_, {forbiddenConcurrentKeys}]) => {
|
||||||
|
return !usedKeys.some(usedKey => forbiddenConcurrentKeys.includes(usedKey));
|
||||||
|
}).map(([{key}, {comparators}]) => {
|
||||||
return new Completion(
|
return new Completion(
|
||||||
key.replaceAll(/\$(\{[^}]*})/g, "$1"),
|
key.replaceAll(/\$(\{[^}]*})/g, "$1"),
|
||||||
key.replaceAll(/\$?\{([^}]*)}/g, "") + (key.includes("{") ? "" : comparators[0])
|
key.replaceAll(/\$?\{([^}]*)}/g, "") + (key.includes("{") ? "" : comparators[0])
|
||||||
@@ -96,4 +98,4 @@ export abstract class FilterLanguage {
|
|||||||
multipleValuesAllowed(key: string): boolean {
|
multipleValuesAllowed(key: string): boolean {
|
||||||
return this.completionForKey(key)?.allowMultipleValues ?? false;
|
return this.completionForKey(key)?.allowMultipleValues ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
const keyLabelToRegex = (keyLabel: string) => {
|
const keyLabelToRegex = (keyLabel: string) => {
|
||||||
return new RegExp(keyLabel
|
return new RegExp(keyLabel
|
||||||
.replaceAll(".", "\\.")
|
.replaceAll(".", "\\.")
|
||||||
.replaceAll(/\{[^}]*}/g, "(?:\"[^,\"]*\"|[^\\s,\"]*?(?=" + COMPARATORS_REGEX + "|\\s|$))"));
|
.replaceAll(/\{[^}]*}/g, "(?:\"[^\"]*\"|[^\\s,\"]*?(?=" + COMPARATORS_REGEX + "|\\s|$))"));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this._filterLanguage && monaco.languages.getLanguages().find(l => l.id === this.language) === undefined) {
|
if (this._filterLanguage && monaco.languages.getLanguages().find(l => l.id === this.language) === undefined) {
|
||||||
@@ -102,7 +102,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
includeLF: true,
|
includeLF: true,
|
||||||
tokenizer: {
|
tokenizer: {
|
||||||
root: [
|
root: [
|
||||||
[/[\w."]+/, {
|
[/[\w.]*(?:"[^"]*")?[\w.]*/, {
|
||||||
cases: {
|
cases: {
|
||||||
...keysTokenizerCases,
|
...keysTokenizerCases,
|
||||||
"@default": {token: "@rematch", next: "@rawText"}
|
"@default": {token: "@rematch", next: "@rawText"}
|
||||||
@@ -123,7 +123,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
],
|
],
|
||||||
value: [
|
value: [
|
||||||
[/"[^"]+(?![^"]*")/, "invalid"],
|
[/"[^"]+(?![^"]*")/, "invalid"],
|
||||||
[new RegExp("\"[^\\n,\"]*\""), {
|
[new RegExp("\"[^\\n\"]*\""), {
|
||||||
token: "variable.value",
|
token: "variable.value",
|
||||||
next: "@separator"
|
next: "@separator"
|
||||||
}],
|
}],
|
||||||
@@ -186,7 +186,6 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const KEY_COMPLETIONS: Promise<Completion[]> = filterLanguage.keyCompletion();
|
|
||||||
const filterLanguageConfiguratorInstance = this;
|
const filterLanguageConfiguratorInstance = this;
|
||||||
return [
|
return [
|
||||||
monaco.languages.registerCompletionItemProvider({
|
monaco.languages.registerCompletionItemProvider({
|
||||||
@@ -259,6 +258,9 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
null,
|
null,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const usedKeys = [...modelValue.matchAll(new RegExp(`\\s?(\\S+?)${COMPARATORS_REGEX}`, "g"))]
|
||||||
|
.map(([_, key]) => FilterLanguage.withNestedKeyPlaceholder(key));
|
||||||
if (offset === 0
|
if (offset === 0
|
||||||
|| (SEPARATOR_CHARS.includes(previousChar) && !inQuotedString)
|
|| (SEPARATOR_CHARS.includes(previousChar) && !inQuotedString)
|
||||||
|| (!lastWordIsComparator && comparatorsAfterCurrentWord?.matches?.[1] !== undefined)) {
|
|| (!lastWordIsComparator && comparatorsAfterCurrentWord?.matches?.[1] !== undefined)) {
|
||||||
@@ -268,7 +270,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
...wordAtPosition,
|
...wordAtPosition,
|
||||||
endColumn: wordAtPosition.endColumn + (comparatorsAfterCurrentWord?.matches?.[1]?.length ?? 0)
|
endColumn: wordAtPosition.endColumn + (comparatorsAfterCurrentWord?.matches?.[1]?.length ?? 0)
|
||||||
},
|
},
|
||||||
await KEY_COMPLETIONS
|
await filterLanguage.keyCompletion(usedKeys)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +311,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (currentFilterMatch === null) {
|
if (currentFilterMatch === null) {
|
||||||
return TO_SUGGESTIONS(position, wordAtPosition, await KEY_COMPLETIONS);
|
return TO_SUGGESTIONS(position, wordAtPosition, await filterLanguage.keyCompletion(usedKeys));
|
||||||
} else {
|
} else {
|
||||||
const [, key, comparator, commaSeparatedValues] = currentFilterMatch?.matches ?? [];
|
const [, key, comparator, commaSeparatedValues] = currentFilterMatch?.matches ?? [];
|
||||||
|
|
||||||
@@ -344,4 +346,4 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
|||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,21 @@ const dashboardFilterKeys: Record<string, FilterKeyCompletions> = {
|
|||||||
),
|
),
|
||||||
timeRange: new FilterKeyCompletions(
|
timeRange: new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS],
|
[Comparators.EQUALS],
|
||||||
async (_, hardcodedValues) => hardcodedValues.RELATIVE_DATE
|
async (_, hardcodedValues) => hardcodedValues.RELATIVE_DATE,
|
||||||
|
false,
|
||||||
|
["timeRange", "startDate", "endDate"]
|
||||||
),
|
),
|
||||||
startDate: new FilterKeyCompletions(
|
startDate: new FilterKeyCompletions(
|
||||||
[Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
async () => PICK_DATE_VALUE
|
async () => PICK_DATE_VALUE,
|
||||||
|
false,
|
||||||
|
["timeRange"]
|
||||||
),
|
),
|
||||||
endDate: new FilterKeyCompletions(
|
endDate: new FilterKeyCompletions(
|
||||||
[Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
async () => PICK_DATE_VALUE
|
async () => PICK_DATE_VALUE,
|
||||||
|
false,
|
||||||
|
["timeRange"]
|
||||||
),
|
),
|
||||||
"labels.{key}": new FilterKeyCompletions(
|
"labels.{key}": new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
@@ -50,4 +56,4 @@ class DashboardFilterLanguage extends FilterLanguage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardFilterLanguage.INSTANCE as FilterLanguage;
|
export default DashboardFilterLanguage.INSTANCE as FilterLanguage;
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ const executionFilterKeys: Record<string, FilterKeyCompletions> = {
|
|||||||
),
|
),
|
||||||
scope: new FilterKeyCompletions(
|
scope: new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
async (_, hardcodedValues) => hardcodedValues.SCOPES
|
async (_, hardcodedValues) => hardcodedValues.SCOPES,
|
||||||
|
undefined,
|
||||||
|
["scope"]
|
||||||
),
|
),
|
||||||
childFilter: new FilterKeyCompletions(
|
childFilter: new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
@@ -43,15 +45,21 @@ const executionFilterKeys: Record<string, FilterKeyCompletions> = {
|
|||||||
),
|
),
|
||||||
timeRange: new FilterKeyCompletions(
|
timeRange: new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS],
|
[Comparators.EQUALS],
|
||||||
async (_, hardcodedValues) => hardcodedValues.RELATIVE_DATE
|
async (_, hardcodedValues) => hardcodedValues.RELATIVE_DATE,
|
||||||
|
false,
|
||||||
|
["timeRange", "startDate", "endDate"]
|
||||||
),
|
),
|
||||||
startDate: new FilterKeyCompletions(
|
startDate: new FilterKeyCompletions(
|
||||||
[Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
async () => PICK_DATE_VALUE
|
async () => PICK_DATE_VALUE,
|
||||||
|
false,
|
||||||
|
["timeRange"]
|
||||||
),
|
),
|
||||||
endDate: new FilterKeyCompletions(
|
endDate: new FilterKeyCompletions(
|
||||||
[Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
async () => PICK_DATE_VALUE
|
async () => PICK_DATE_VALUE,
|
||||||
|
false,
|
||||||
|
["timeRange"]
|
||||||
),
|
),
|
||||||
"labels.{key}": new FilterKeyCompletions(
|
"labels.{key}": new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
@@ -73,4 +81,4 @@ class ExecutionFilterLanguage extends FilterLanguage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExecutionFilterLanguage.INSTANCE as FilterLanguage;
|
export default ExecutionFilterLanguage.INSTANCE as FilterLanguage;
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import {FilterLanguage} from "../filterLanguage.ts";
|
|||||||
const flowDashboardFilterKeys: Record<string, FilterKeyCompletions> = {
|
const flowDashboardFilterKeys: Record<string, FilterKeyCompletions> = {
|
||||||
timeRange: new FilterKeyCompletions(
|
timeRange: new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS],
|
[Comparators.EQUALS],
|
||||||
async (_, hardcodedValues) => hardcodedValues.RELATIVE_DATE
|
async (_, hardcodedValues) => hardcodedValues.RELATIVE_DATE,
|
||||||
|
false,
|
||||||
|
["timeRange", "startDate", "endDate"]
|
||||||
),
|
),
|
||||||
startDate: new FilterKeyCompletions(
|
startDate: new FilterKeyCompletions(
|
||||||
[Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
async () => PICK_DATE_VALUE
|
async () => PICK_DATE_VALUE,
|
||||||
|
false,
|
||||||
|
["timeRange"]
|
||||||
),
|
),
|
||||||
endDate: new FilterKeyCompletions(
|
endDate: new FilterKeyCompletions(
|
||||||
[Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.LESS_THAN_OR_EQUAL_TO, Comparators.LESS_THAN, Comparators.GREATER_THAN_OR_EQUAL_TO, Comparators.GREATER_THAN, Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
async () => PICK_DATE_VALUE
|
async () => PICK_DATE_VALUE,
|
||||||
|
false,
|
||||||
|
["timeRange"]
|
||||||
),
|
),
|
||||||
"labels.{key}": new FilterKeyCompletions(
|
"labels.{key}": new FilterKeyCompletions(
|
||||||
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
[Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
@@ -29,4 +35,4 @@ class FlowDashboardFilterLanguage extends FilterLanguage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FlowDashboardFilterLanguage.INSTANCE as FilterLanguage;
|
export default FlowDashboardFilterLanguage.INSTANCE as FilterLanguage;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user