mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
48 Commits
global-sta
...
v0.21.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aca5a9ff4c | ||
|
|
a6ce86d702 | ||
|
|
4392c89ec7 | ||
|
|
d74a31ba7f | ||
|
|
cb3195900f | ||
|
|
cf4b91f44d | ||
|
|
33ecf8d5f5 | ||
|
|
39a2293a45 | ||
|
|
88c93995df | ||
|
|
6afe5ff41f | ||
|
|
a3a8863f46 | ||
|
|
fcfee5116b | ||
|
|
3f2d91014b | ||
|
|
41149a83b3 | ||
|
|
1ed882e8f3 | ||
|
|
0f6e0de29c | ||
|
|
238bc532c3 | ||
|
|
6919848ab3 | ||
|
|
86aec88de4 | ||
|
|
f609d57a0c | ||
|
|
f3852a3c24 | ||
|
|
804ff6a81c | ||
|
|
7869f90edd | ||
|
|
2b72306b3d | ||
|
|
f0d5d4b93f | ||
|
|
4e4ab80b2f | ||
|
|
c33d08afda | ||
|
|
a246ac38f5 | ||
|
|
7bdaa81dee | ||
|
|
6a1d831849 | ||
|
|
95d2d1dfa3 | ||
|
|
d12dd179c2 | ||
|
|
ceda5eb8ee | ||
|
|
1301aaac76 | ||
|
|
5f7468a9a4 | ||
|
|
aa24c888a3 | ||
|
|
c792d9b6ea | ||
|
|
a921b95404 | ||
|
|
e46df069a9 | ||
|
|
c08f4f24ca | ||
|
|
67b3937824 | ||
|
|
17e1623342 | ||
|
|
d12fbf05b0 | ||
|
|
efa2d44e76 | ||
|
|
acdb46cea0 | ||
|
|
c1807516f5 | ||
|
|
ab796dff93 | ||
|
|
2d98f909de |
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -9,6 +9,8 @@ jobs:
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
name: Check & Publish
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
24
.github/workflows/docker.yml
vendored
24
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Create Docker images on tag
|
||||
name: Create Docker images on Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
release-tag:
|
||||
description: 'Kestra Release Tag'
|
||||
required: false
|
||||
type: string
|
||||
plugin-version:
|
||||
description: 'Plugin version'
|
||||
required: false
|
||||
@@ -38,7 +42,6 @@ jobs:
|
||||
name: Publish Docker
|
||||
needs: [ plugins ]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
strategy:
|
||||
matrix:
|
||||
image:
|
||||
@@ -57,10 +60,19 @@ jobs:
|
||||
- name: Set image name
|
||||
id: vars
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/*/}
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ inputs.release-tag }}" == "" ]]; then
|
||||
TAG=${GITHUB_REF#refs/*/}
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
TAG="${{ inputs.release-tag }}"
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [[ "${{ env.PLUGIN_VERSION }}" == *"-SNAPSHOT" ]]; then
|
||||
echo "plugins=--repositories=https://s01.oss.sonatype.org/content/repositories/snapshots ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# Download release
|
||||
- name: Download release
|
||||
uses: robinraju/release-downloader@v1.11
|
||||
|
||||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -35,6 +35,8 @@ env:
|
||||
DOCKER_APT_PACKAGES: python3 python3-venv python-is-python3 python3-pip nodejs npm curl zip unzip
|
||||
DOCKER_PYTHON_LIBRARIES: kestra
|
||||
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build Artifacts
|
||||
@@ -45,13 +47,14 @@ jobs:
|
||||
docker-artifact-name: ${{ steps.vars.outputs.artifact }}
|
||||
plugins: ${{ steps.plugins-list.outputs.plugins }}
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout current ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout GitHub Actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
path: actions
|
||||
|
||||
25
.github/workflows/release-plugins.yml
vendored
25
.github/workflows/release-plugins.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releaseVersion:
|
||||
description: 'The release version (e.g., 0.21.0)'
|
||||
description: 'The release version (e.g., 0.21.0-RC1)'
|
||||
required: true
|
||||
type: string
|
||||
nextVersion:
|
||||
@@ -18,13 +18,29 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Release plugins
|
||||
runs-on: kestra-private-standard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
path: actions
|
||||
ref: main
|
||||
|
||||
# Setup build
|
||||
- uses: ./actions/.github/actions/setup-build
|
||||
id: build
|
||||
with:
|
||||
java-enabled: true
|
||||
node-enabled: true
|
||||
python-enabled: true
|
||||
caches-enabled: true
|
||||
|
||||
# Get Plugins List
|
||||
- name: Get Plugins List
|
||||
uses: ./.github/actions/plugins-list
|
||||
@@ -33,6 +49,11 @@ jobs:
|
||||
with:
|
||||
plugin-version: 'LATEST'
|
||||
|
||||
- name: 'Configure Git'
|
||||
run: |
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
# Execute
|
||||
- name: Run Gradle Release
|
||||
if: ${{ github.event.inputs.dryRun == 'false' }}
|
||||
|
||||
29
.github/workflows/tag-plugins.yml
vendored
29
.github/workflows/tag-plugins.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Update and Tag Kestra Plugins
|
||||
name: Set Version and Tag Plugins
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,13 +14,29 @@ on:
|
||||
jobs:
|
||||
tag:
|
||||
name: Release plugins
|
||||
runs-on: kestra-private-standard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
path: actions
|
||||
ref: main
|
||||
|
||||
# Setup build
|
||||
- uses: ./actions/.github/actions/setup-build
|
||||
id: build
|
||||
with:
|
||||
java-enabled: true
|
||||
node-enabled: true
|
||||
python-enabled: true
|
||||
caches-enabled: true
|
||||
|
||||
# Get Plugins List
|
||||
- name: Get Plugins List
|
||||
uses: ./.github/actions/plugins-list
|
||||
@@ -29,8 +45,13 @@ jobs:
|
||||
with:
|
||||
plugin-version: 'LATEST'
|
||||
|
||||
- name: 'Configure Git'
|
||||
run: |
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
# Execute
|
||||
- name: Tag Plugins
|
||||
- name: Set Version and Tag Plugins
|
||||
if: ${{ github.event.inputs.dryRun == 'false' }}
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
@@ -41,7 +62,7 @@ jobs:
|
||||
--yes \
|
||||
${{ steps.plugins-list.outputs.repositories }}
|
||||
|
||||
- name: Run Gradle Release (DRY_RUN)
|
||||
- name: Set Version and Tag Plugins (DRY_RUN)
|
||||
if: ${{ github.event.inputs.dryRun == 'true' }}
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.services.FlowListenersInterface;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.micronaut.context.annotation.Value;
|
||||
import io.micronaut.scheduling.io.watch.FileWatchConfiguration;
|
||||
@@ -36,6 +37,9 @@ public class FileChangedEventListener {
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepositoryInterface;
|
||||
|
||||
@Inject
|
||||
private PluginDefaultService pluginDefaultService;
|
||||
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@@ -64,7 +68,7 @@ public class FileChangedEventListener {
|
||||
|
||||
public void startListeningFromConfig() throws IOException, InterruptedException {
|
||||
if (fileWatchConfiguration != null && fileWatchConfiguration.isEnabled()) {
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface);
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface, pluginDefaultService);
|
||||
List<Path> paths = fileWatchConfiguration.getPaths();
|
||||
this.setup(paths);
|
||||
|
||||
@@ -107,7 +111,6 @@ public class FileChangedEventListener {
|
||||
} else {
|
||||
log.info("File watching is disabled.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void startListening(List<Path> paths) throws IOException, InterruptedException {
|
||||
@@ -118,60 +121,64 @@ public class FileChangedEventListener {
|
||||
WatchKey key;
|
||||
while ((key = watchService.take()) != null) {
|
||||
for (WatchEvent<?> watchEvent : key.pollEvents()) {
|
||||
WatchEvent.Kind<?> kind = watchEvent.kind();
|
||||
Path entry = (Path) watchEvent.context();
|
||||
try {
|
||||
WatchEvent.Kind<?> kind = watchEvent.kind();
|
||||
Path entry = (Path) watchEvent.context();
|
||||
|
||||
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
|
||||
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
|
||||
|
||||
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
if (Files.isDirectory(filePath)) {
|
||||
loadFlowsFromFolder(filePath);
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
if (Files.isDirectory(filePath)) {
|
||||
loadFlowsFromFolder(filePath);
|
||||
} else {
|
||||
|
||||
try {
|
||||
String content = Files.readString(filePath, Charset.defaultCharset());
|
||||
try {
|
||||
String content = Files.readString(filePath, Charset.defaultCharset());
|
||||
|
||||
Optional<Flow> flow = parseFlow(content, entry);
|
||||
if (flow.isPresent()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
// Check if we already have a file with the given path
|
||||
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
|
||||
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
|
||||
// Check if Flow from file has id/namespace updated
|
||||
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
|
||||
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
|
||||
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
|
||||
Optional<Flow> flow = parseFlow(content, entry);
|
||||
if (flow.isPresent()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
// Check if we already have a file with the given path
|
||||
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
|
||||
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
|
||||
// Check if Flow from file has id/namespace updated
|
||||
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
|
||||
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
|
||||
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
|
||||
}
|
||||
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
|
||||
} catch (NoSuchFileException e) {
|
||||
log.error("File not found: {}", entry, e);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading file: {}", entry, e);
|
||||
}
|
||||
|
||||
} catch (NoSuchFileException e) {
|
||||
log.error("File not found: {}", entry, e);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading file: {}", entry, e);
|
||||
}
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
flows.stream()
|
||||
.filter(flow -> flow.getPath().equals(filePath.toString()))
|
||||
.findFirst()
|
||||
.ifPresent(flowWithPath -> {
|
||||
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
|
||||
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
flows.stream()
|
||||
.filter(flow -> flow.getPath().equals(filePath.toString()))
|
||||
.findFirst()
|
||||
.ifPresent(flowWithPath -> {
|
||||
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
|
||||
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error while watching flows", e);
|
||||
}
|
||||
}
|
||||
key.reset();
|
||||
@@ -230,7 +237,8 @@ public class FileChangedEventListener {
|
||||
private Optional<Flow> parseFlow(String content, Path entry) {
|
||||
try {
|
||||
Flow flow = yamlParser.parse(content, Flow.class);
|
||||
modelValidator.validate(flow);
|
||||
FlowWithSource withPluginDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
modelValidator.validate(withPluginDefault);
|
||||
return Optional.of(flow);
|
||||
} catch (ConstraintViolationException e) {
|
||||
log.warn("Error while parsing flow: {}", entry, e);
|
||||
|
||||
@@ -3,32 +3,36 @@ package io.kestra.cli.services;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Requires(property = "micronaut.io.watch.enabled", value = "true")
|
||||
@Slf4j
|
||||
public class LocalFlowFileWatcher implements FlowFilesManager {
|
||||
private FlowRepositoryInterface flowRepositoryInterface;
|
||||
private final FlowRepositoryInterface flowRepository;
|
||||
private final PluginDefaultService pluginDefaultService;
|
||||
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepositoryInterface) {
|
||||
this.flowRepositoryInterface = flowRepositoryInterface;
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepository, PluginDefaultService pluginDefaultService) {
|
||||
this.flowRepository = flowRepository;
|
||||
this.pluginDefaultService = pluginDefaultService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowWithSource createOrUpdateFlow(Flow flow, String content) {
|
||||
return flowRepositoryInterface.findById(null, flow.getNamespace(), flow.getId())
|
||||
.map(previous -> flowRepositoryInterface.update(flow, previous, content, flow))
|
||||
.orElseGet(() -> flowRepositoryInterface.create(flow, content, flow));
|
||||
FlowWithSource withDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
return flowRepository.findById(null, flow.getNamespace(), flow.getId())
|
||||
.map(previous -> flowRepository.update(flow, previous, content, withDefault))
|
||||
.orElseGet(() -> flowRepository.create(flow, content, withDefault));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFlow(FlowWithSource toDelete) {
|
||||
flowRepositoryInterface.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepositoryInterface::delete);
|
||||
log.error("Flow {} has been deleted", toDelete.getId());
|
||||
flowRepository.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepository::delete);
|
||||
log.info("Flow {} has been deleted", toDelete.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFlow(String tenantId, String namespace, String id) {
|
||||
flowRepositoryInterface.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepositoryInterface::delete);
|
||||
log.error("Flow {} has been deleted", id);
|
||||
flowRepository.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepository::delete);
|
||||
log.info("Flow {} has been deleted", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package io.kestra.cli.services;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.utils.Await;
|
||||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
@MicronautTest(environments = {"test", "file-watch"}, transactional = false)
|
||||
class FileChangedEventListenerTest {
|
||||
public static final String FILE_WATCH = "build/file-watch";
|
||||
@Inject
|
||||
private FileChangedEventListener fileWatcher;
|
||||
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepository;
|
||||
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws IOException {
|
||||
if (!Files.exists(Path.of(FILE_WATCH))) {
|
||||
Files.createDirectories(Path.of(FILE_WATCH));
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() throws IOException {
|
||||
if (Files.exists(Path.of(FILE_WATCH))) {
|
||||
FileUtils.deleteDirectory(Path.of(FILE_WATCH).toFile());
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Exception {
|
||||
if (started.compareAndSet(false, true)) {
|
||||
executorService.execute(throwRunnable(() -> fileWatcher.startListeningFromConfig()));
|
||||
}
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but always pass locally
|
||||
void test() throws IOException, TimeoutException {
|
||||
// remove the flow if it already exists
|
||||
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "myflow").ifPresent(flow -> flowRepository.delete(flow));
|
||||
|
||||
// create a basic flow
|
||||
String flow = """
|
||||
id: myflow
|
||||
namespace: io.kestra.tests.watch
|
||||
|
||||
tasks:
|
||||
- id: hello
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello World! 🚀
|
||||
""";
|
||||
Files.write(Path.of(FILE_WATCH + "/myflow.yaml"), flow.getBytes());
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isPresent(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
Flow myflow = flowRepository.findById(null, "io.kestra.tests.watch", "myflow").orElseThrow();
|
||||
assertThat(myflow.getTasks(), hasSize(1));
|
||||
assertThat(myflow.getTasks().getFirst().getId(), is("hello"));
|
||||
assertThat(myflow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
|
||||
|
||||
// delete the flow
|
||||
Files.delete(Path.of(FILE_WATCH + "/myflow.yaml"));
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isEmpty(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but always pass locally
|
||||
void testWithPluginDefault() throws IOException, TimeoutException {
|
||||
// remove the flow if it already exists
|
||||
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "pluginDefault").ifPresent(flow -> flowRepository.delete(flow));
|
||||
|
||||
// create a flow with plugin default
|
||||
String pluginDefault = """
|
||||
id: pluginDefault
|
||||
namespace: io.kestra.tests.watch
|
||||
|
||||
tasks:
|
||||
- id: helloWithDefault
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
|
||||
pluginDefaults:
|
||||
- type: io.kestra.plugin.core.log.Log
|
||||
values:
|
||||
message: Hello World!
|
||||
""";
|
||||
Files.write(Path.of(FILE_WATCH + "/plugin-default.yaml"), pluginDefault.getBytes());
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isPresent(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
Flow pluginDefaultFlow = flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").orElseThrow();
|
||||
assertThat(pluginDefaultFlow.getTasks(), hasSize(1));
|
||||
assertThat(pluginDefaultFlow.getTasks().getFirst().getId(), is("helloWithDefault"));
|
||||
assertThat(pluginDefaultFlow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
|
||||
|
||||
// delete both files
|
||||
Files.delete(Path.of(FILE_WATCH + "/plugin-default.yaml"));
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isEmpty(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
}
|
||||
}
|
||||
12
cli/src/test/resources/application-file-watch.yml
Normal file
12
cli/src/test/resources/application-file-watch.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
micronaut:
|
||||
io:
|
||||
watch:
|
||||
enabled: true
|
||||
paths:
|
||||
- build/file-watch
|
||||
|
||||
kestra:
|
||||
repository:
|
||||
type: memory
|
||||
queue:
|
||||
type: memory
|
||||
@@ -311,10 +311,12 @@ public class JsonSchemaGenerator {
|
||||
if (member.getDeclaredType().isInstanceOf(Property.class)) {
|
||||
memberAttributes.put("$dynamic", true);
|
||||
// if we are in the String definition of a Property but the target type is not String: we configure the pattern
|
||||
Class<?> targetType = member.getDeclaredType().getTypeParameters().getFirst().getErasedType();
|
||||
if (!String.class.isAssignableFrom(targetType) && String.class.isAssignableFrom(member.getType().getErasedType())) {
|
||||
memberAttributes.put("pattern", ".*{{.*}}.*");
|
||||
}
|
||||
// TODO this was a good idea but their is too much cases where it didn't work like in List or Map so if we want it we need to make it more clever
|
||||
// I keep it for now commented but at some point we may want to re-do and improve it or remove these commented lines
|
||||
// Class<?> targetType = member.getDeclaredType().getTypeParameters().getFirst().getErasedType();
|
||||
// if (!String.class.isAssignableFrom(targetType) && String.class.isAssignableFrom(member.getType().getErasedType())) {
|
||||
// memberAttributes.put("pattern", ".*{{.*}}.*");
|
||||
// }
|
||||
} else if (member.getDeclaredType().isInstanceOf(Data.class)) {
|
||||
memberAttributes.put("$dynamic", false);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ public class Plugin {
|
||||
private String subGroup;
|
||||
|
||||
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup) {
|
||||
return Plugin.of(registeredPlugin, subgroup, true);
|
||||
}
|
||||
|
||||
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup, boolean includeDeprecated) {
|
||||
Plugin plugin = new Plugin();
|
||||
plugin.name = registeredPlugin.name();
|
||||
PluginSubGroup subGroupInfos = null;
|
||||
@@ -80,17 +84,17 @@ public class Plugin {
|
||||
|
||||
plugin.subGroup = subgroup;
|
||||
|
||||
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.apps = filterAndGetClassName(registeredPlugin.getApps()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.apps = filterAndGetClassName(registeredPlugin.getApps(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
|
||||
return plugin;
|
||||
}
|
||||
@@ -100,12 +104,14 @@ public class Plugin {
|
||||
* Those classes are only filtered from the documentation to ensure backward compatibility.
|
||||
*
|
||||
* @param list The list of classes?
|
||||
* @param includeDeprecated whether to include deprecated plugins or not
|
||||
* @return a filtered streams.
|
||||
*/
|
||||
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list) {
|
||||
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list, boolean includeDeprecated) {
|
||||
return list
|
||||
.stream()
|
||||
.filter(not(io.kestra.core.models.Plugin::isInternal))
|
||||
.filter(p -> includeDeprecated || !io.kestra.core.models.Plugin.isDeprecated(p))
|
||||
.map(Class::getName)
|
||||
.filter(c -> !c.startsWith("org.kestra."))
|
||||
.toList();
|
||||
|
||||
@@ -2,23 +2,26 @@ package io.kestra.core.models.tasks.runners;
|
||||
|
||||
import io.kestra.core.models.tasks.Output;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
public class TaskRunnerResult<T extends TaskRunnerDetailResult> implements Output {
|
||||
private int exitCode;
|
||||
|
||||
private AbstractLogConsumer logConsumer;
|
||||
|
||||
@Nullable
|
||||
private T details;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public TaskRunnerResult(int exitCode, AbstractLogConsumer logConsumer) {
|
||||
this.exitCode = exitCode;
|
||||
this.logConsumer = logConsumer;
|
||||
this.details = (T) TaskRunnerDetailResult.builder().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.kestra.core.repositories.ExecutionRepositoryInterface;
|
||||
import io.kestra.core.services.ExecutionService;
|
||||
import io.kestra.core.storages.Storage;
|
||||
import io.kestra.core.trace.TracerFactory;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.utils.MapUtils;
|
||||
import io.kestra.core.trace.propagation.ExecutionTextMapSetter;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
@@ -153,7 +154,7 @@ public final class ExecutableUtils {
|
||||
throw new IllegalStateException("Cannot execute an invalid flow: " + fwe.getException());
|
||||
}
|
||||
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(currentExecution.getLabels()) : new ArrayList<>(systemLabels(currentExecution));
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
|
||||
if (labels != null) {
|
||||
labels.forEach(throwConsumer(label -> newLabels.add(new Label(runContext.render(label.key()), runContext.render(label.value())))));
|
||||
}
|
||||
@@ -201,6 +202,16 @@ public final class ExecutableUtils {
|
||||
}));
|
||||
}
|
||||
|
||||
private static List<Label> filterLabels(List<Label> labels, Flow flow) {
|
||||
if (ListUtils.isEmpty(flow.getLabels())) {
|
||||
return labels;
|
||||
}
|
||||
|
||||
return labels.stream()
|
||||
.filter(label -> flow.getLabels().stream().noneMatch(flowLabel -> flowLabel.key().equals(label.key())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<Label> systemLabels(Execution execution) {
|
||||
return Streams.of(execution.getLabels())
|
||||
.filter(label -> label.key().startsWith(Label.SYSTEM_PREFIX))
|
||||
|
||||
@@ -172,13 +172,15 @@ public class FlowService {
|
||||
subFlows.forEach(subflow -> {
|
||||
Optional<Flow> optional = findById(flow.getTenantId(), subflow.getNamespace(), subflow.getFlowId());
|
||||
|
||||
violations.add(ManualConstraintViolation.of(
|
||||
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
|
||||
flow,
|
||||
Flow.class,
|
||||
"flow.tasks",
|
||||
flow.getNamespace()
|
||||
));
|
||||
if (optional.isEmpty()) {
|
||||
violations.add(ManualConstraintViolation.of(
|
||||
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
|
||||
flow,
|
||||
Flow.class,
|
||||
"flow.tasks",
|
||||
flow.getNamespace()
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
|
||||
@@ -149,7 +149,7 @@ class ClassPluginDocumentationTest {
|
||||
assertThat(oneOf.getFirst().get("type"), is("integer"));
|
||||
assertThat(oneOf.getFirst().get("$dynamic"), is(true));
|
||||
assertThat(oneOf.get(1).get("type"), is("string"));
|
||||
assertThat(oneOf.get(1).get("pattern"), is(".*{{.*}}.*"));
|
||||
// assertThat(oneOf.get(1).get("pattern"), is(".*{{.*}}.*"));
|
||||
|
||||
Map<String, Object> withDefault = (Map<String, Object>) properties.get("withDefault");
|
||||
assertThat(withDefault.get("type"), is("string"));
|
||||
|
||||
@@ -22,6 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KestraTest
|
||||
@@ -323,7 +324,7 @@ class FlowServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkValidSubflowsNotFound() {
|
||||
void checkSubflowNotFound() {
|
||||
Flow flow = create("mainFlow", "task", 1).toBuilder()
|
||||
.tasks(List.of(
|
||||
io.kestra.plugin.core.flow.Subflow.builder()
|
||||
@@ -342,4 +343,23 @@ class FlowServiceTest {
|
||||
assertThat(exception.getConstraintViolations().size(), is(1));
|
||||
assertThat(exception.getConstraintViolations().iterator().next().getMessage(), is("The subflow 'nonExistentSubflow' not found in namespace 'io.kestra.unittest'."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkValidSubflow() {
|
||||
Flow subflow = create("existingSubflow", "task", 1);
|
||||
flowRepository.create(subflow, subflow.generateSource(), subflow);
|
||||
|
||||
Flow flow = create("mainFlow", "task", 1).toBuilder()
|
||||
.tasks(List.of(
|
||||
io.kestra.plugin.core.flow.Subflow.builder()
|
||||
.id("subflowTask")
|
||||
.type(io.kestra.plugin.core.flow.Subflow.class.getName())
|
||||
.namespace("io.kestra.unittest")
|
||||
.flowId("existingSubflow")
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
|
||||
assertDoesNotThrow(() -> flowService.checkValidSubflows(flow));
|
||||
}
|
||||
}
|
||||
@@ -99,21 +99,24 @@ public class FlowCaseTest {
|
||||
assertThat(triggered.get().getState().getCurrent(), is(triggerState));
|
||||
|
||||
if (testInherited) {
|
||||
assertThat(triggered.get().getLabels().size(), is(5));
|
||||
assertThat(triggered.get().getLabels().size(), is(6));
|
||||
assertThat(triggered.get().getLabels(), hasItems(
|
||||
new Label(Label.CORRELATION_ID, execution.getId()),
|
||||
new Label("mainFlowExecutionLabel", "execFoo"),
|
||||
new Label("mainFlowLabel", "flowFoo"),
|
||||
new Label("launchTaskLabel", "launchFoo"),
|
||||
new Label("switchFlowLabel", "switchFoo")
|
||||
new Label("switchFlowLabel", "switchFoo"),
|
||||
new Label("overriding", "child")
|
||||
));
|
||||
} else {
|
||||
assertThat(triggered.get().getLabels().size(), is(3));
|
||||
assertThat(triggered.get().getLabels().size(), is(4));
|
||||
assertThat(triggered.get().getLabels(), hasItems(
|
||||
new Label(Label.CORRELATION_ID, execution.getId()),
|
||||
new Label("launchTaskLabel", "launchFoo"),
|
||||
new Label("switchFlowLabel", "switchFoo")
|
||||
new Label("switchFlowLabel", "switchFoo"),
|
||||
new Label("overriding", "child")
|
||||
));
|
||||
assertThat(triggered.get().getLabels(), not(hasItems(new Label("inherited", "label"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ public class ForEachItemCaseTest {
|
||||
}
|
||||
|
||||
public void restartForEachItem() throws Exception {
|
||||
CountDownLatch countDownLatch = new CountDownLatch(26);
|
||||
CountDownLatch countDownLatch = new CountDownLatch(6);
|
||||
Flux<Execution> receiveSubflows = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution subflowExecution = either.getLeft();
|
||||
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isFailed()) {
|
||||
@@ -285,7 +285,7 @@ public class ForEachItemCaseTest {
|
||||
});
|
||||
|
||||
URI file = storageUpload();
|
||||
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 4);
|
||||
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 20);
|
||||
Execution execution = runnerUtils.runOne(null, TEST_NAMESPACE, "restart-for-each-item", null,
|
||||
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
|
||||
Duration.ofSeconds(30));
|
||||
@@ -296,7 +296,7 @@ public class ForEachItemCaseTest {
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.MINUTES));
|
||||
receiveSubflows.blockLast();
|
||||
|
||||
CountDownLatch successLatch = new CountDownLatch(26);
|
||||
CountDownLatch successLatch = new CountDownLatch(6);
|
||||
receiveSubflows = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution subflowExecution = either.getLeft();
|
||||
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isSuccess()) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -43,7 +43,7 @@ class TimeoutTest {
|
||||
@Inject
|
||||
private RunnerUtils runnerUtils;
|
||||
|
||||
@Test
|
||||
@RetryingTest(5) // Flaky on CI but never locally even with 100 repetitions
|
||||
void timeout() throws TimeoutException, QueueException {
|
||||
List<LogEntry> logs = new CopyOnWriteArrayList<>();
|
||||
Flux<LogEntry> receive = TestsUtils.receive(workerTaskLogQueue, either -> logs.add(either.getLeft()));
|
||||
|
||||
@@ -10,6 +10,7 @@ inputs:
|
||||
|
||||
labels:
|
||||
switchFlowLabel: switchFoo
|
||||
overriding: child
|
||||
|
||||
tasks:
|
||||
- id: parent-seq
|
||||
|
||||
@@ -7,6 +7,7 @@ inputs:
|
||||
|
||||
labels:
|
||||
mainFlowLabel: flowFoo
|
||||
overriding: parent
|
||||
|
||||
tasks:
|
||||
- id: launch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version=0.21.0-rc0-SNAPSHOT
|
||||
version=0.21.0
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.priority=low
|
||||
org.gradle.priority=low
|
||||
|
||||
@@ -4,8 +4,6 @@ import io.kestra.core.models.annotations.Plugin.Id;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -55,6 +53,18 @@ public interface Plugin {
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method to check whether a given plugin is deprecated.
|
||||
*
|
||||
* @param plugin The plugin type.
|
||||
* @return {@code true} if the plugin is deprecated.
|
||||
*/
|
||||
static boolean isDeprecated(final Class<?> plugin) {
|
||||
Objects.requireNonNull(plugin, "Cannot check if a plugin is deprecated from null");
|
||||
Deprecated annotation = plugin.getAnnotation(Deprecated.class);
|
||||
return annotation != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method to get the id of a plugin.
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ javaPlatform {
|
||||
dependencies {
|
||||
// versions for libraries with multiple module but no BOM
|
||||
def slf4jVersion = "2.0.16"
|
||||
def protobufVersion = "4.29.3"
|
||||
def protobufVersion = "3.25.5" // Orc still uses 3.25.5 see https://github.com/apache/orc/blob/main/java/pom.xml
|
||||
def bouncycastleVersion = "1.80"
|
||||
def aetherVersion = "1.1.0"
|
||||
def jollydayVersion = "0.32.0"
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT
|
||||
# To release a specific plugin:
|
||||
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT plugin-kubernetes
|
||||
|
||||
# To release specific plugins from file:
|
||||
# ./release-plugins.sh --release-version=0.20.0 --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
@@ -43,6 +44,7 @@ usage() {
|
||||
echo "Options:"
|
||||
echo " --release-version <version> Specify the release version (required)."
|
||||
echo " --next-version <version> Specify the next 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."
|
||||
@@ -81,6 +83,14 @@ while [[ "$#" -gt 0 ]]; do
|
||||
NEXT_VERSION="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--plugin-file)
|
||||
PLUGIN_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--plugin-file=*)
|
||||
PLUGIN_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
|
||||
@@ -143,7 +143,7 @@ public abstract class AbstractExecScript extends Task implements RunnableTask<Sc
|
||||
* protected DockerOptions docker = DockerOptions.builder().build();
|
||||
* }</pre>
|
||||
*/
|
||||
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) {
|
||||
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) throws IllegalVariableEvaluationException {
|
||||
// FIXME to keep backward compatibility, we call the old method from the new one by default
|
||||
return injectDefaults(original);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0
|
||||
# To release a specific plugin:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0 plugin-kubernetes
|
||||
|
||||
# To release specific plugins from file:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0 --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
@@ -40,6 +41,7 @@ usage() {
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " --release-version <version> Specify the release 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."
|
||||
@@ -70,6 +72,14 @@ while [[ "$#" -gt 0 ]]; do
|
||||
RELEASE_VERSION="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--plugin-file)
|
||||
PLUGIN_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--plugin-file=*)
|
||||
PLUGIN_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
@@ -163,7 +173,7 @@ do
|
||||
git checkout "$RELEASE_BRANCH";
|
||||
|
||||
# Update version
|
||||
sed -i.bak "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
||||
sed -i "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
||||
git add ./gradle.properties
|
||||
git commit -m"chore(version): update to version 'v$RELEASE_VERSION'."
|
||||
git push
|
||||
|
||||
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@js-joda/core": "^5.6.3",
|
||||
"@kestra-io/ui-libs": "^0.0.119",
|
||||
"@kestra-io/ui-libs": "^0.0.125",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
"@vue-flow/core": "^1.42.1",
|
||||
@@ -2499,9 +2499,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@kestra-io/ui-libs": {
|
||||
"version": "0.0.119",
|
||||
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.119.tgz",
|
||||
"integrity": "sha512-KfIY0YG5OmsJW9kL1yBmgDEbs1UFru5SFSrHM5ea7IFUkipzWviVdfWb7u8lnGyN3L05BVYO3hcRi6zYYcvheQ==",
|
||||
"version": "0.0.125",
|
||||
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.125.tgz",
|
||||
"integrity": "sha512-5VobEs66ORkBJO3H/YR5DT+jV4MR1wNvh8Uz10bmWot/TNkS7gMNzbqyQj8jC9ATE7PitEc0s2JDuQqJ7ywuWg==",
|
||||
"dependencies": {
|
||||
"@nuxtjs/mdc": "^0.12.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@js-joda/core": "^5.6.3",
|
||||
"@kestra-io/ui-libs": "^0.0.119",
|
||||
"@kestra-io/ui-libs": "^0.0.125",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
"@vue-flow/core": "^1.42.1",
|
||||
|
||||
@@ -6,7 +6,6 @@ Welcome to the Custom Dashboard! This feature allows you to create and manage pe
|
||||
|
||||
Below is an example of a dashboard definition that displays executions over time, a table that uses metrics to display the sum of sales per namespace, and a table that shows the log count by level per namespace:
|
||||
|
||||
::collapse{title="Expand for a example dashboard definition"}
|
||||
```yaml
|
||||
title: Getting Started
|
||||
description: First custom dashboard
|
||||
@@ -84,7 +83,6 @@ charts:
|
||||
- dev_graph
|
||||
- prod_graph
|
||||
```
|
||||
::
|
||||
|
||||
To see all available properties to configure a custom dashboard as code, see examples provided in the [Enterprise Edition Examples](https://github.com/kestra-io/enterprise-edition-examples) repository.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:metadata
|
||||
@update-metadata="(k, v) => emits('updateMetadata', {[k]: v})"
|
||||
@update-task="(yaml) => emits('updateTask', yaml)"
|
||||
@reorder="(yaml) => emits('reorder', yaml)"
|
||||
@update-documentation="(task) => emits('updateDocumentation', task)"
|
||||
/>
|
||||
</div>
|
||||
@@ -30,6 +31,7 @@
|
||||
"updateTask",
|
||||
"updateMetadata",
|
||||
"updateDocumentation",
|
||||
"reorder",
|
||||
]);
|
||||
const props = defineProps({
|
||||
flow: {type: String, required: true},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class="item"
|
||||
@click="
|
||||
(store.commit('code/removeBreadcrumb', {position: index}),
|
||||
store.commit('code/unsetPanel'))
|
||||
store.commit('code/unsetPanel', false))
|
||||
"
|
||||
>
|
||||
<router-link :to="breadcrumb.to">
|
||||
|
||||
@@ -11,15 +11,22 @@
|
||||
<Creation :section="item.title" />
|
||||
</template>
|
||||
|
||||
<template v-if="creation">
|
||||
<Element
|
||||
v-for="(element, elementIndex) in item.elements"
|
||||
:key="elementIndex"
|
||||
:section="item.title"
|
||||
:element
|
||||
@remove-element="removeElement(item.title, elementIndex)"
|
||||
/>
|
||||
</template>
|
||||
<Element
|
||||
v-for="(element, elementIndex) in item.elements"
|
||||
:key="elementIndex"
|
||||
:section="item.title"
|
||||
:element
|
||||
@remove-element="removeElement(item.title, elementIndex)"
|
||||
@move-element="
|
||||
(direction: 'up' | 'down') =>
|
||||
moveElement(
|
||||
item.elements,
|
||||
element.id,
|
||||
elementIndex,
|
||||
direction,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
@@ -32,7 +39,7 @@
|
||||
import Creation from "./buttons/Creation.vue";
|
||||
import Element from "./Element.vue";
|
||||
|
||||
const emits = defineEmits(["remove"]);
|
||||
const emits = defineEmits(["remove", "reorder"]);
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
@@ -67,6 +74,27 @@
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
import {YamlUtils as YAML_FROM_UI_LIBS} from "@kestra-io/ui-libs";
|
||||
const moveElement = (
|
||||
items: Record<string, any>[] | undefined,
|
||||
elementID: string,
|
||||
index: number,
|
||||
direction: "up" | "down",
|
||||
) => {
|
||||
if (!items || !props.flow) return;
|
||||
if (
|
||||
(direction === "up" && index === 0) ||
|
||||
(direction === "down" && index === items.length - 1)
|
||||
)
|
||||
return;
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
emits(
|
||||
"reorder",
|
||||
YAML_FROM_UI_LIBS.swapTasks(props.flow, elementID, items[newIndex].id),
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -14,17 +14,21 @@
|
||||
size="small"
|
||||
class="border-0"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<ChevronUp @click.prevent.stop="emits('moveElement', 'up')" />
|
||||
<ChevronDown @click.prevent.stop="emits('moveElement', 'down')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
|
||||
import {DeleteOutline} from "../../utils/icons";
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../utils/icons";
|
||||
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
|
||||
const emits = defineEmits(["removeElement"]);
|
||||
const emits = defineEmits(["removeElement", "moveElement"]);
|
||||
|
||||
const props = defineProps({
|
||||
section: {type: String, required: true},
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
<span v-if="required" class="me-1 text-danger">*</span>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
<div class="mt-1 mb-2 wrapper" :class="props.class">
|
||||
<el-input v-model="input" @input="handleInput" :placeholder :disabled />
|
||||
<el-input
|
||||
v-model="input"
|
||||
@input="handleInput"
|
||||
:placeholder
|
||||
:disabled
|
||||
type="textarea"
|
||||
:autosize="{minRows: 1}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from "vue";
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({
|
||||
modelValue: {type: [String, Number, Boolean], default: undefined},
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
creation
|
||||
:flow
|
||||
@remove="(yaml) => emits('updateTask', yaml)"
|
||||
@reorder="(yaml) => emits('reorder', yaml)"
|
||||
/>
|
||||
|
||||
<hr class="my-4">
|
||||
@@ -96,6 +97,7 @@
|
||||
"updateTask",
|
||||
"updateMetadata",
|
||||
"updateDocumentation",
|
||||
"reorder",
|
||||
]);
|
||||
|
||||
const saveEvent = (e: KeyboardEvent) => {
|
||||
@@ -235,6 +237,7 @@
|
||||
"error_handlers",
|
||||
YamlUtils.parse(props.flow).errors ?? [],
|
||||
),
|
||||
getSectionTitle("finally", YamlUtils.parse(props.flow).finally ?? []),
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
v-else
|
||||
:is="lastBreadcumb.component.type"
|
||||
v-bind="lastBreadcumb.component.props"
|
||||
v-on="lastBreadcumb.component.listeners"
|
||||
:model-value="lastBreadcumb.component.props.modelValue"
|
||||
@update:model-value="validateTask"
|
||||
/>
|
||||
@@ -126,39 +125,51 @@
|
||||
YamlUtils.parse(yaml.value).id,
|
||||
);
|
||||
|
||||
if (route.query.section === SECTIONS.TRIGGERS.toLowerCase()) {
|
||||
const existingTask = YamlUtils.checkTaskAlreadyExist(
|
||||
source,
|
||||
CURRENT.value,
|
||||
);
|
||||
if (existingTask) {
|
||||
store.dispatch("core/showMessage", {
|
||||
variant: "error",
|
||||
title: "Trigger Id already exist",
|
||||
message: `Trigger Id ${existingTask} already exist in the flow.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const currentSection = route.query.section;
|
||||
const isCreation =
|
||||
props.creation &&
|
||||
(!route.query.identifier || route.query.identifier === "new");
|
||||
|
||||
emits("updateTask", YamlUtils.insertTrigger(source, CURRENT.value));
|
||||
CURRENT.value = null;
|
||||
} else {
|
||||
const action = props.creation
|
||||
? YamlUtils.insertTask(
|
||||
let result;
|
||||
|
||||
if (isCreation) {
|
||||
if (currentSection === "tasks") {
|
||||
const existing = YamlUtils.checkTaskAlreadyExist(
|
||||
source,
|
||||
YamlUtils.getLastTask(source),
|
||||
task,
|
||||
"after",
|
||||
)
|
||||
: YamlUtils.replaceTaskInDocument(
|
||||
source,
|
||||
route.query.identifier,
|
||||
task,
|
||||
CURRENT.value,
|
||||
);
|
||||
|
||||
emits("updateTask", action);
|
||||
if (existing) {
|
||||
store.dispatch("core/showMessage", {
|
||||
variant: "error",
|
||||
title: "Task with same ID already exist",
|
||||
message: `Task in ${route.query.section} block with ID: ${existing} already exist in the flow.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
result = YamlUtils.insertTask(
|
||||
source,
|
||||
route.query.target ?? YamlUtils.getLastTask(source),
|
||||
task,
|
||||
route.query.position ?? "after",
|
||||
);
|
||||
} else if (currentSection === "triggers") {
|
||||
result = YamlUtils.insertTrigger(source, CURRENT.value);
|
||||
} else if (currentSection === "error handlers") {
|
||||
result = YamlUtils.insertError(source, CURRENT.value);
|
||||
} else if (currentSection === "finally") {
|
||||
result = YamlUtils.insertFinally(source, CURRENT.value);
|
||||
}
|
||||
} else {
|
||||
result = YamlUtils.replaceTaskInDocument(
|
||||
source,
|
||||
route.query.identifier,
|
||||
task,
|
||||
);
|
||||
}
|
||||
|
||||
emits("updateTask", result);
|
||||
store.commit("code/removeBreadcrumb", {last: true});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -53,7 +53,8 @@ $code-font-sm: var(--el-font-size-small);
|
||||
font-size: $code-font-sm;
|
||||
}
|
||||
|
||||
.delete {
|
||||
.delete,
|
||||
.reorder {
|
||||
cursor: pointer;
|
||||
padding-left: 0;
|
||||
color: $code-gray-700;
|
||||
@@ -64,7 +65,8 @@ $code-font-sm: var(--el-font-size-small);
|
||||
:deep(*) {
|
||||
--el-disabled-text-color: #{$code-gray-700};
|
||||
|
||||
.el-input__inner {
|
||||
.el-input__inner,
|
||||
.el-textarea__inner {
|
||||
color: $code-gray-700;
|
||||
font-size: $code-font-sm;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Plus from "vue-material-design-icons/Plus.vue";
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
import DeleteOutline from "vue-material-design-icons/DeleteOutline.vue";
|
||||
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
||||
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
||||
|
||||
export {Plus, ContentSave, DeleteOutline};
|
||||
export {Plus, ContentSave, DeleteOutline, ChevronUp, ChevronDown};
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
import {useRoute} from "vue-router";
|
||||
import {Utils} from "@kestra-io/ui-libs";
|
||||
import KestraUtils from "../../../../../utils/utils.js"
|
||||
|
||||
const store = useStore();
|
||||
|
||||
@@ -129,7 +130,7 @@
|
||||
const parsedData = computed(() => {
|
||||
const parseValue = (value) => {
|
||||
const date = moment(value, moment.ISO_8601, true);
|
||||
return date.isValid() ? date.format("YYYY-MM-DD") : value;
|
||||
return date.isValid() ? date.format(KestraUtils.getDateFormat(route.query.startDate, route.query.endDate)) : value;
|
||||
};
|
||||
|
||||
const rawData = generated.value.results;
|
||||
|
||||
@@ -68,10 +68,10 @@
|
||||
z-index: -2;
|
||||
background-image: linear-gradient(138.8deg, #CCE8FE 0%, #CDA0FF 27.03%, #8489F5 41.02%, #CDF1FF 68.68%, #B591E9 94%, #CCE8FE 100%);
|
||||
background-size: 200% 200%;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
animation: move-border 3s linear infinite;
|
||||
}
|
||||
|
||||
@@ -79,11 +79,11 @@
|
||||
|
||||
.enterprise-tag::after{
|
||||
z-index: -1;
|
||||
background: $base-gray-200;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
bottom: -1px;
|
||||
right: -1px;
|
||||
background: $base-gray-100;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
bottom: 1px;
|
||||
right: 1px;
|
||||
html.dark & {
|
||||
background: $base-gray-400;
|
||||
}
|
||||
@@ -92,14 +92,12 @@
|
||||
.enterprise-tag{
|
||||
position: relative;
|
||||
background: $base-gray-200;
|
||||
border: 1px solid transparent;
|
||||
padding: 0 1rem;
|
||||
padding: .125rem 1rem;
|
||||
border-radius: 1rem;
|
||||
display: inline-block;
|
||||
z-index: 2;
|
||||
html.dark &{
|
||||
background: #FBFBFB26;
|
||||
border-color: #FFFFFF;
|
||||
}
|
||||
.flare{
|
||||
display: none;
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
<el-select
|
||||
ref="select"
|
||||
:model-value="current"
|
||||
:model-value="currentFilters"
|
||||
value-key="label"
|
||||
:placeholder="props.placeholder ?? t('filters.label')"
|
||||
default-first-option
|
||||
allow-create
|
||||
filterable
|
||||
:filter-method="(f) => prefixFilter = f.toLowerCase()"
|
||||
clearable
|
||||
multiple
|
||||
placement="bottom"
|
||||
@@ -20,7 +21,7 @@
|
||||
@keyup="(e) => handleInputChange(e.key)"
|
||||
@keyup.enter="() => handleEnterKey(select?.hoverOption?.value)"
|
||||
@remove-tag="(item) => removeItem(item)"
|
||||
@visible-change="(visible) => dropdownClosedCallback(visible)"
|
||||
@visible-change="(visible) => dropdownToggleCallback(visible)"
|
||||
@clear="handleClear"
|
||||
:class="{
|
||||
refresh: buttons.refresh.shown,
|
||||
@@ -65,7 +66,7 @@
|
||||
:value="comparator"
|
||||
:label="comparator.label"
|
||||
:class="{
|
||||
selected: current.some(
|
||||
selected: currentFilters.some(
|
||||
(c) => c.comparator === comparator,
|
||||
),
|
||||
}"
|
||||
@@ -75,14 +76,11 @@
|
||||
</template>
|
||||
<template v-else-if="dropdowns.third.shown">
|
||||
<el-option
|
||||
v-for="(filter, index) in valueOptions"
|
||||
v-for="(filter, index) in prefixFilteredValueOptions"
|
||||
:key="filter.value"
|
||||
:value="filter"
|
||||
:disabled="isOptionDisabled(filter)"
|
||||
:class="{
|
||||
selected: current.some((c) =>
|
||||
c.value.includes(filter.value),
|
||||
),
|
||||
selected: currentFilters.at(-1)?.value?.includes(filter.value),
|
||||
disabled: isOptionDisabled(filter),
|
||||
'level-3': true
|
||||
}"
|
||||
@@ -117,7 +115,7 @@
|
||||
class="rounded-0"
|
||||
/>
|
||||
</KestraIcon>
|
||||
<Save :disabled="!current.length" :prefix="ITEMS_PREFIX" :current />
|
||||
<Save :disabled="!currentFilters.length" :prefix="ITEMS_PREFIX" :current="currentFilters" />
|
||||
</el-button-group>
|
||||
|
||||
<el-button-group
|
||||
@@ -145,10 +143,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, watch, nextTick, shallowRef} from "vue";
|
||||
import {computed, nextTick, onMounted, ref, shallowRef, watch} from "vue";
|
||||
import {ElSelect} from "element-plus";
|
||||
|
||||
import {Shown, Buttons, CurrentItem} from "./utils/types";
|
||||
import {Buttons, CurrentItem, Shown} from "./utils/types";
|
||||
|
||||
import Refresh from "../layout/RefreshButton.vue";
|
||||
import Items from "./segments/Items.vue";
|
||||
@@ -163,12 +161,18 @@
|
||||
import {Magnify} from "./utils/icons";
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useStore} from "vuex";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {useFilters} from "./composables/useFilters";
|
||||
import action from "../../models/action.js";
|
||||
import permission from "../../models/permission.js";
|
||||
import {useValues} from "./composables/useValues";
|
||||
import {decodeParams, encodeParams} from "./utils/helpers";
|
||||
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
import {useStore} from "vuex";
|
||||
const store = useStore();
|
||||
|
||||
import {useRouter, useRoute} from "vue-router";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
@@ -198,12 +202,21 @@
|
||||
|
||||
const ITEMS_PREFIX = props.prefix ?? String(route.name);
|
||||
|
||||
import {useFilters} from "./composables/useFilters";
|
||||
const {COMPARATORS, OPTIONS} = useFilters(ITEMS_PREFIX);
|
||||
|
||||
const prefixFilteredValueOptions = computed(() => {
|
||||
if (prefixFilter.value === "") {
|
||||
return valueOptions.value;
|
||||
}
|
||||
return valueOptions.value.filter(o => o.label.toLowerCase().startsWith(prefixFilter.value));
|
||||
})
|
||||
|
||||
const select = ref<InstanceType<typeof ElSelect> | null>(null);
|
||||
const updateHoveringIndex = (index) => {
|
||||
select.value!.states.hoveringIndex = index >= 0 ? index : 0;
|
||||
select.value!.states.hoveringIndex = undefined;
|
||||
nextTick(() => {
|
||||
select.value!.states.hoveringIndex = Math.max(index, 0);
|
||||
})
|
||||
};
|
||||
const emptyLabel = ref(t("filters.empty"));
|
||||
const INITIAL_DROPDOWNS = {
|
||||
@@ -239,6 +252,8 @@
|
||||
} else if (dropdowns.value.third.shown) {
|
||||
valueCallback(option);
|
||||
}
|
||||
|
||||
prefixFilter.value = "";
|
||||
};
|
||||
|
||||
const getInputValue = () => select.value?.states.inputValue;
|
||||
@@ -250,13 +265,13 @@
|
||||
|
||||
if (key === "Enter") return;
|
||||
|
||||
if (current.value.at(-1)?.label === "user") {
|
||||
if (currentFilters.value.at(-1)?.label === "user") {
|
||||
emits("input", getInputValue());
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
current.value = [];
|
||||
currentFilters.value = [];
|
||||
triggerSearch();
|
||||
};
|
||||
|
||||
@@ -279,7 +294,7 @@
|
||||
};
|
||||
|
||||
// Check if parent filter already exists
|
||||
const existingFilterIndex = current.value.findIndex(
|
||||
const existingFilterIndex = currentFilters.value.findIndex(
|
||||
(item) => item.label === option.value.label,
|
||||
);
|
||||
if (existingFilterIndex !== -1) {
|
||||
@@ -296,8 +311,8 @@
|
||||
} else {
|
||||
// If it doesn't exist, push new filter
|
||||
dropdowns.value.first = {shown: false, value: option};
|
||||
dropdowns.value.second = {shown: true, index: current.value.length};
|
||||
current.value.push(option.value);
|
||||
dropdowns.value.second = {shown: true, index: currentFilters.value.length};
|
||||
currentFilters.value.push(option.value);
|
||||
activeParentFilter.value = option.value.label;
|
||||
lastClickedParent.value = option.value.label;
|
||||
parentValue.value = option.value.label;
|
||||
@@ -309,9 +324,9 @@
|
||||
}
|
||||
};
|
||||
const comparatorCallback = (value) => {
|
||||
current.value[dropdowns.value.second.index].comparator = value;
|
||||
currentFilters.value[dropdowns.value.second.index].comparator = value;
|
||||
emptyLabel.value = ["labels", "details"].includes(
|
||||
current.value[dropdowns.value.second.index].label,
|
||||
currentFilters.value[dropdowns.value.second.index].label,
|
||||
)
|
||||
? t("filters.format")
|
||||
: t("filters.empty");
|
||||
@@ -319,34 +334,28 @@
|
||||
dropdowns.value = {
|
||||
first: {shown: false, value: {}},
|
||||
second: {shown: false, index: -1},
|
||||
third: {shown: true, index: current.value.length - 1},
|
||||
third: {shown: true, index: currentFilters.value.length - 1},
|
||||
};
|
||||
|
||||
// Set hover index to the selected comparator for highlighting
|
||||
const index = valueOptions.value.findIndex((o) => o.value === value.value);
|
||||
updateHoveringIndex(index);
|
||||
updateHoveringIndex(0);
|
||||
};
|
||||
|
||||
const dropdownClosedCallback = (visible) => {
|
||||
const dropdownToggleCallback = (visible) => {
|
||||
if (!visible) {
|
||||
dropdowns.value = {...INITIAL_DROPDOWNS};
|
||||
activeParentFilter.value = null;
|
||||
lastClickedParent.value = null;
|
||||
showSubFilterDropdown.value = false;
|
||||
// If last filter item selection was not completed, remove it from array
|
||||
if (current.value?.at(-1)?.value?.length === 0) current.value.pop();
|
||||
if (currentFilters.value?.at(-1)?.value?.length === 0) currentFilters.value.pop();
|
||||
} else {
|
||||
// Highlight all selected items by setting hoveringIndex to match the first selected item
|
||||
const index = valueOptions.value.findIndex((o) => {
|
||||
return current.value.some((c) => c.value.includes(o.value));
|
||||
});
|
||||
updateHoveringIndex(index);
|
||||
updateHoveringIndex(0);
|
||||
}
|
||||
};
|
||||
const isOptionDisabled = () => {
|
||||
if (!activeParentFilter.value) return false;
|
||||
|
||||
const parentIndex = current.value.findIndex(
|
||||
const parentIndex = currentFilters.value.findIndex(
|
||||
(item) => item.label === activeParentFilter.value,
|
||||
);
|
||||
if (parentIndex === -1) return false;
|
||||
@@ -355,38 +364,30 @@
|
||||
// Don't do anything if the option is disabled
|
||||
if (isOptionDisabled(filter)) return;
|
||||
if (!isDate) {
|
||||
const parentIndex = current.value.findIndex(
|
||||
const parentIndex = currentFilters.value.findIndex(
|
||||
(item) => item.label === parentValue.value,
|
||||
);
|
||||
if (parentIndex !== -1) {
|
||||
if (
|
||||
lastClickedParent.value === "Namespace" ||
|
||||
lastClickedParent.value === "namespace" ||
|
||||
lastClickedParent.value === "Log level"
|
||||
) {
|
||||
const values = current.value[parentIndex].value;
|
||||
if (["namespace", "log level"].includes(lastClickedParent.value.toLowerCase())) {
|
||||
const values = currentFilters.value[parentIndex].value;
|
||||
const index = values.indexOf(filter.value);
|
||||
|
||||
if (index === -1) {
|
||||
current.value[parentIndex].value = [filter.value]; // Add only the filter.value
|
||||
currentFilters.value[parentIndex].value = [filter.value]; // Add only the filter.value
|
||||
} else {
|
||||
current.value[parentIndex].value = values.filter(
|
||||
currentFilters.value[parentIndex].value = values.filter(
|
||||
(value, i) => i !== index,
|
||||
); // remove the clicked item
|
||||
}
|
||||
} else {
|
||||
const values = current.value[parentIndex].value;
|
||||
const values = currentFilters.value[parentIndex].value;
|
||||
const index = values.indexOf(filter.value);
|
||||
if (index === -1) values.push(filter.value);
|
||||
else values.splice(index, 1);
|
||||
}
|
||||
const hoverIndex = valueOptions.value.findIndex(
|
||||
(o) => o.value === filter.value,
|
||||
);
|
||||
updateHoveringIndex(hoverIndex);
|
||||
}
|
||||
} else {
|
||||
const match = current.value.find((v) => v.label === "absolute_date");
|
||||
const match = currentFilters.value.find((v) => v.label === "absolute_date");
|
||||
if (match) {
|
||||
match.value = [
|
||||
{
|
||||
@@ -397,16 +398,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!current.value[dropdowns.value.third.index].comparator?.multiple) {
|
||||
if (!currentFilters.value[dropdowns.value.third.index].comparator?.multiple) {
|
||||
// If selection is not multiple, close the dropdown
|
||||
closeDropdown();
|
||||
}
|
||||
triggerSearch();
|
||||
};
|
||||
|
||||
import action from "../../models/action.js";
|
||||
import permission from "../../models/permission.js";
|
||||
|
||||
const user = computed(() => store.state.auth.user);
|
||||
|
||||
const namespaceOptions = ref([]);
|
||||
@@ -438,11 +436,10 @@
|
||||
// Load all namespaces only if that filter is included
|
||||
if (props.include.includes("namespace")) loadNamespaces();
|
||||
|
||||
import {useValues} from "./composables/useValues";
|
||||
const {VALUES} = useValues(ITEMS_PREFIX);
|
||||
|
||||
const isDatePickerShown = computed(() => {
|
||||
return current?.value?.some(
|
||||
return currentFilters?.value?.some(
|
||||
(c) => c.label === "absolute_date" && c.comparator,
|
||||
);
|
||||
});
|
||||
@@ -541,40 +538,45 @@
|
||||
break;
|
||||
}
|
||||
};
|
||||
const current = ref<CurrentItem[]>([]);
|
||||
const currentFilters = ref<CurrentItem[]>([]);
|
||||
|
||||
const prefixFilter = ref("");
|
||||
|
||||
const includedOptions = computed(() => {
|
||||
const dates = ["relative_date", "absolute_date"];
|
||||
|
||||
const found = current.value?.find((v) => dates.includes(v?.label));
|
||||
const found = currentFilters.value?.find((v) => dates.includes(v?.label));
|
||||
const exclude = found ? dates.find((date) => date !== found.label) : null;
|
||||
|
||||
return OPTIONS.filter((o) => {
|
||||
const label = o.value?.label;
|
||||
return props.include.includes(label) && label !== exclude;
|
||||
return props.include.includes(label) && label !== exclude && label.startsWith(prefixFilter.value);
|
||||
});
|
||||
});
|
||||
|
||||
const changeCallback = (v) => {
|
||||
if (!Array.isArray(v) || !v.length) return;
|
||||
const changeCallback = (wholeSearchContent) => {
|
||||
if (!Array.isArray(wholeSearchContent) || !wholeSearchContent.length) return;
|
||||
|
||||
if (typeof v.at(-1) === "string") {
|
||||
if (["labels", "details"].includes(v.at(-2)?.label)) {
|
||||
// Adding labels to proper filter
|
||||
v.at(-2).value?.push(v.at(-1));
|
||||
closeDropdown();
|
||||
triggerSearch();
|
||||
if (typeof wholeSearchContent.at(-1) === "string") {
|
||||
if (
|
||||
["labels", "details"].includes(wholeSearchContent.at(-2)?.label) ||
|
||||
wholeSearchContent.at(-2)?.value?.length === 0
|
||||
) {
|
||||
// Adding value to preceding empty filter
|
||||
// TODO Provide a way for user to escape infinite labels & details loop (you can never fallback to a new filter, any further text will be added as a value to the filter)
|
||||
wholeSearchContent.at(-2)?.value?.push(wholeSearchContent.at(-1));
|
||||
} else {
|
||||
// Adding text search string
|
||||
const label = t("filters.options.text");
|
||||
const index = current.value.findIndex((i) => i.label === label);
|
||||
const index = currentFilters.value.findIndex((i) => i.label === label);
|
||||
|
||||
if (index !== -1) current.value[index].value = [v.at(-1)];
|
||||
else current.value.push({label, value: [v.at(-1)]});
|
||||
|
||||
triggerSearch();
|
||||
closeDropdown();
|
||||
if (index !== -1) currentFilters.value[index].value = [wholeSearchContent.at(-1)];
|
||||
else currentFilters.value.push({label, value: [wholeSearchContent.at(-1)]});
|
||||
}
|
||||
|
||||
triggerSearch();
|
||||
closeDropdown();
|
||||
|
||||
triggerEnter.value = false;
|
||||
}
|
||||
|
||||
@@ -583,7 +585,7 @@
|
||||
};
|
||||
|
||||
const removeItem = (value) => {
|
||||
current.value = current.value.filter(
|
||||
currentFilters.value = currentFilters.value.filter(
|
||||
(item) => JSON.stringify(item) !== JSON.stringify(value),
|
||||
);
|
||||
|
||||
@@ -591,22 +593,20 @@
|
||||
};
|
||||
|
||||
const handleClickedItems = (value) => {
|
||||
if (value) current.value = value;
|
||||
if (value) currentFilters.value = value;
|
||||
select.value?.focus();
|
||||
};
|
||||
|
||||
import {encodeParams, decodeParams} from "./utils/helpers";
|
||||
|
||||
const triggerSearch = () => {
|
||||
if (props.searchCallback) return;
|
||||
else router.push({query: encodeParams(current.value, OPTIONS)});
|
||||
else router.push({query: encodeParams(currentFilters.value, OPTIONS)});
|
||||
};
|
||||
|
||||
// Include parameters from URL directly to filter
|
||||
onMounted(() => {
|
||||
if (props.decode) {
|
||||
const decodedParams = decodeParams(route.query, props.include, OPTIONS);
|
||||
current.value = decodedParams.map((item: any) => {
|
||||
currentFilters.value = decodedParams.map((item: any) => {
|
||||
if (item.label === "absolute_date") {
|
||||
return {
|
||||
...item,
|
||||
@@ -635,7 +635,7 @@
|
||||
|
||||
const addNamespaceFilter = (namespace) => {
|
||||
if (!props.decode || !namespace) return;
|
||||
current.value.push({
|
||||
currentFilters.value.push({
|
||||
label: "namespace",
|
||||
value: [namespace],
|
||||
comparator: COMPARATORS.STARTS_WITH,
|
||||
@@ -649,7 +649,7 @@
|
||||
addNamespaceFilter(params?.namespace);
|
||||
|
||||
if (props.decode && params.id) {
|
||||
current.value.push({
|
||||
currentFilters.value.push({
|
||||
label: "flow",
|
||||
value: [`${params.id}`],
|
||||
comparator: COMPARATORS.IS,
|
||||
@@ -675,12 +675,12 @@
|
||||
);
|
||||
|
||||
const handleFocus = () => {
|
||||
if (current.value.length > 0 && lastClickedParent.value) {
|
||||
const existingFilterIndex = current.value.findIndex(
|
||||
if (currentFilters.value.length > 0 && lastClickedParent.value) {
|
||||
const existingFilterIndex = currentFilters.value.findIndex(
|
||||
(item) => item.label === lastClickedParent.value,
|
||||
);
|
||||
if (existingFilterIndex !== -1) {
|
||||
if (!current.value[existingFilterIndex].comparator) {
|
||||
if (!currentFilters.value[existingFilterIndex].comparator) {
|
||||
dropdowns.value = {
|
||||
first: {shown: false, value: {}},
|
||||
second: {shown: true, index: existingFilterIndex},
|
||||
@@ -741,7 +741,7 @@
|
||||
const label = labelElement?.textContent;
|
||||
|
||||
if (label) {
|
||||
const existingFilterIndex = current.value.findIndex(
|
||||
const existingFilterIndex = currentFilters.value.findIndex(
|
||||
(item) =>
|
||||
item?.label.toLowerCase() ===
|
||||
label
|
||||
@@ -757,7 +757,7 @@
|
||||
.replace(/\blog\b/gi, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "_"); // Set parentValue when a filter is clicked
|
||||
if (!current.value[existingFilterIndex].comparator) {
|
||||
if (!currentFilters.value[existingFilterIndex].comparator) {
|
||||
dropdowns.value = {
|
||||
first: {shown: false, value: {}},
|
||||
second: {
|
||||
@@ -835,6 +835,7 @@ $dashboards: 52px;
|
||||
}
|
||||
|
||||
&.dashboards {
|
||||
min-width: $dashboards;
|
||||
max-width: calc(100% - $included - $dashboards);
|
||||
}
|
||||
}
|
||||
@@ -872,6 +873,7 @@ $dashboards: 52px;
|
||||
.filters-select {
|
||||
& .el-select-dropdown {
|
||||
width: auto !important;
|
||||
max-width: 300px;
|
||||
|
||||
&:has(.el-select-dropdown__empty) {
|
||||
width: auto !important;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<KestraIcon placement="bottom">
|
||||
<el-button :icon="Menu">
|
||||
{{ selectedDashboard ?? $t('default_dashboard') }}
|
||||
<el-button :icon="Menu" class="main-button d-inline">
|
||||
<span class="text-truncate">{{ selectedDashboard ?? $t('default_dashboard') }}</span>
|
||||
</el-button>
|
||||
</KestraIcon>
|
||||
|
||||
@@ -141,4 +141,12 @@
|
||||
.items {
|
||||
max-height: 160px !important; // 5 visible items
|
||||
}
|
||||
|
||||
.main-button {
|
||||
max-width: 300px;
|
||||
|
||||
span {
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -32,6 +32,7 @@
|
||||
import {SECTIONS} from "../../utils/constants.js";
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
computed: {
|
||||
...mapGetters("flow", ["taskError"]),
|
||||
},
|
||||
|
||||
@@ -1,94 +1,72 @@
|
||||
<template>
|
||||
<el-row
|
||||
v-for="(item, index) in values"
|
||||
v-for="(element, index) in items"
|
||||
:key="'array-' + index"
|
||||
:gutter="10"
|
||||
class="w-100"
|
||||
>
|
||||
<el-col :span="22">
|
||||
<component
|
||||
:is="`task-${getType(schema.items)}`"
|
||||
:model-value="item"
|
||||
@update:model-value="onInput(index, $event)"
|
||||
:root="getKey(index)"
|
||||
:schema="schema.items"
|
||||
:definitions="definitions"
|
||||
<el-col :span="2" class="d-flex flex-column mt-1 mb-2 reorder">
|
||||
<ChevronUp @click.prevent.stop="moveItem(index, 'up')" />
|
||||
<ChevronDown @click.prevent.stop="moveItem(index, 'down')" />
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<InputText
|
||||
:model-value="element"
|
||||
@update:model-value="(v) => handleInput(v, index)"
|
||||
:placeholder="$t('value')"
|
||||
class="w-100"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="2" class="col align-self-center delete">
|
||||
<DeleteOutline @click="removeItem(key)" />
|
||||
<DeleteOutline @click="removeItem(index)" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<Add @add="addItem()" v-if="values.at(-1)" />
|
||||
<Add @add="addItem()" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {DeleteOutline} from "../../code/utils/icons";
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../code/utils/icons";
|
||||
|
||||
import InputText from "../../code/components/inputs/InputText.vue";
|
||||
import Add from "../../code/components/Add.vue";
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import {toRaw} from "vue";
|
||||
import Task from "./Task";
|
||||
defineOptions({inheritAttrs: false});
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
emits: ["update:modelValue"],
|
||||
created() {
|
||||
if (!Array.isArray(this.modelValue) && this.modelValue !== undefined) {
|
||||
this.$emit("update:modelValue", []);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
values() {
|
||||
if (this.modelValue === undefined) {
|
||||
return this.schema.default || [undefined];
|
||||
}
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({modelValue: {type: Array, default: undefined}});
|
||||
|
||||
return this.modelValue;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getPropertiesValue(properties) {
|
||||
return this.modelValue && this.modelValue[properties]
|
||||
? this.modelValue[properties]
|
||||
: undefined;
|
||||
},
|
||||
onInput(index, value) {
|
||||
const local = this.modelValue || [];
|
||||
local[index] = value;
|
||||
const items = ref(
|
||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||
);
|
||||
|
||||
this.$emit("update:modelValue", local);
|
||||
},
|
||||
addItem() {
|
||||
let local = this.modelValue || [];
|
||||
local.push(undefined);
|
||||
const handleInput = (value: string, index: number) => {
|
||||
items.value[index] = value;
|
||||
emits("update:modelValue", items.value);
|
||||
};
|
||||
|
||||
// click on + when there is no items
|
||||
if (this.modelValue === undefined) {
|
||||
local.push(undefined);
|
||||
}
|
||||
|
||||
this.$emit("update:modelValue", local);
|
||||
},
|
||||
removeItem(x) {
|
||||
let local = this.modelValue || [];
|
||||
local.splice(x, 1);
|
||||
|
||||
if (local.length === 1) {
|
||||
let raw = toRaw(local[0]);
|
||||
|
||||
if (raw === null || raw === undefined) {
|
||||
local = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit("update:modelValue", local);
|
||||
},
|
||||
},
|
||||
const addItem = () => {
|
||||
items.value.push(undefined);
|
||||
emits("update:modelValue", items.value);
|
||||
};
|
||||
const removeItem = (index: number) => {
|
||||
items.value.splice(index, 1);
|
||||
emits("update:modelValue", items.value);
|
||||
};
|
||||
const moveItem = (index: number, direction: "up" | "down") => {
|
||||
if (direction === "up" && index > 0) {
|
||||
[items.value[index - 1], items.value[index]] = [
|
||||
items.value[index],
|
||||
items.value[index - 1],
|
||||
];
|
||||
} else if (direction === "down" && index < items.value.length - 1) {
|
||||
[items.value[index + 1], items.value[index]] = [
|
||||
items.value[index],
|
||||
items.value[index + 1],
|
||||
];
|
||||
}
|
||||
emits("update:modelValue", items.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
emits: ["update:modelValue"],
|
||||
methods: {
|
||||
properties(requiredFields) {
|
||||
if (this.schema) {
|
||||
if (this.schema?.properties) {
|
||||
const properties = Object.entries(
|
||||
this.schema.properties,
|
||||
).reduce((acc, [key, value]) => {
|
||||
|
||||
@@ -8,20 +8,24 @@
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<InputText
|
||||
<editor
|
||||
:model-value="editorValue"
|
||||
:navbar="false"
|
||||
:full-height="false"
|
||||
schema-type="flow"
|
||||
lang="plaintext"
|
||||
input
|
||||
@update:model-value="onInput"
|
||||
class="w-100"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import InputText from "../../../components/code/components/inputs/InputText.vue";
|
||||
import Editor from "../../../components/inputs/Editor.vue";
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
components: {InputText},
|
||||
components: {Editor},
|
||||
emits: ["update:modelValue"],
|
||||
computed: {
|
||||
isValid() {
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
:flow="flowYaml"
|
||||
@update-metadata="(e) => onUpdateMetadata(e, true)"
|
||||
@update-task="(e) => editorUpdate(e)"
|
||||
@reorder="(yaml) => handleReorder(yaml)"
|
||||
@update-documentation="(task) => updatePluginDocumentation(undefined, task)"
|
||||
/>
|
||||
</div>
|
||||
@@ -799,8 +800,8 @@
|
||||
}
|
||||
|
||||
haveChange.value = true;
|
||||
store.dispatch("core/isUnsaved", true);
|
||||
|
||||
if(editorViewType.value === "YAML") store.dispatch("core/isUnsaved", true);
|
||||
|
||||
if(!props.isCreating){
|
||||
store.commit("editor/changeOpenedTabs", {
|
||||
action: "dirty",
|
||||
@@ -939,15 +940,13 @@
|
||||
};
|
||||
|
||||
const onUpdateMetadata = (event, shouldSave) => {
|
||||
metadata.value = event;
|
||||
|
||||
if(shouldSave) {
|
||||
metadata.value = {...metadata.value, ...event};
|
||||
metadata.value = {...metadata.value, ...(event.concurrency.limit === 0 ? {concurrency: null} : event)};
|
||||
onSaveMetadata();
|
||||
validateFlow(flowYaml.value)
|
||||
|
||||
} else {
|
||||
metadata.value = event;
|
||||
metadata.value = event.concurrency.limit === 0 ? {concurrency: null} : event;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -959,6 +958,12 @@
|
||||
haveChange.value = true;
|
||||
};
|
||||
|
||||
const handleReorder = (yaml) => {
|
||||
flowYaml.value = yaml;
|
||||
haveChange.value = true;
|
||||
save()
|
||||
};
|
||||
|
||||
const editorUpdate = (event) => {
|
||||
const currentIsFlow = isFlow();
|
||||
|
||||
|
||||
@@ -293,10 +293,12 @@
|
||||
);
|
||||
};
|
||||
|
||||
const onCreateNewTask = () => {
|
||||
const onCreateNewTask = (details) => {
|
||||
emit("openNoCode", {
|
||||
section: SECTIONS.TASKS.toLowerCase(),
|
||||
identifier: "new",
|
||||
target: details[0],
|
||||
position: details[1],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
color: var(--ks-content-primary);
|
||||
box-shadow: none;
|
||||
|
||||
&_active, body &_active:hover, &:hover, &.vsm--link_hover, &.vsm--link_open {
|
||||
&_active, body &_active:hover, &.vsm--link_open, &.vsm--link_open:hover {
|
||||
background-color: var(--ks-button-background-primary);
|
||||
color: var(--ks-button-content-primary);
|
||||
font-weight: normal;
|
||||
@@ -269,7 +269,7 @@
|
||||
}
|
||||
|
||||
&:hover, body &_hover {
|
||||
background-color: var(--ks-button-background-primary);
|
||||
background-color: var(--ks-button-background-secondary-hover);
|
||||
}
|
||||
|
||||
.el-tooltip__trigger {
|
||||
|
||||
@@ -70,7 +70,9 @@
|
||||
},
|
||||
methods: {
|
||||
loadToc() {
|
||||
this.$store.dispatch("plugin/listWithSubgroup")
|
||||
this.$store.dispatch("plugin/listWithSubgroup", {
|
||||
includeDeprecated: false
|
||||
})
|
||||
},
|
||||
|
||||
loadPlugin() {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<KestraFilter :placeholder="$t('pluginPage.search', {count: countPlugin})" :search-callback="(input)=> searchInput = input" />
|
||||
</el-row>
|
||||
<section class="px-3 plugins-container">
|
||||
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="index" effect="light">
|
||||
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="plugin.name + '-' + index" effect="light">
|
||||
<template #content>
|
||||
<div class="tasks-tooltips">
|
||||
<p v-if="plugin?.tasks.filter(t => t.toLowerCase().includes(searchInput)).length > 0" class="mb-0">
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
:model-value="modelValue"
|
||||
:placeholder="$t('no_code.creation.select', {section: section.toLowerCase().slice(0, -1)})"
|
||||
filterable
|
||||
clearable
|
||||
@update:model-value="onInput"
|
||||
>
|
||||
<el-option
|
||||
|
||||
@@ -86,7 +86,9 @@
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
if(!this.$route?.params?.tab) this.$router.push({name: "blueprints", params: {tab: "community"}})
|
||||
if(!this.embed && !this.$route?.params?.tab) {
|
||||
this.$router.push({name: "blueprints", params: {tab: "community", kind: this.kind}})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
routeInfo() {
|
||||
@@ -120,6 +122,11 @@
|
||||
this.embeddedTab = newTab.name;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
tab(newVal) {
|
||||
this.embeddedTab = newVal;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -140,6 +140,9 @@
|
||||
default: tagsResponse => Object.fromEntries(tagsResponse.map(tag => [tag.id, tag]))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.commit("doc/setDocId", `blueprints.${this.blueprintType}`);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
q: undefined,
|
||||
|
||||
@@ -61,6 +61,15 @@ export function useLeftMenu() {
|
||||
},
|
||||
exact: false,
|
||||
},
|
||||
{
|
||||
href: {name: "apps/list"},
|
||||
routes: routeStartWith("apps"),
|
||||
title: t("apps"),
|
||||
icon: {
|
||||
element: shallowRef(FormatListGroupPlus),
|
||||
class: "menu-icon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: {name: "templates/list"},
|
||||
routes: routeStartWith("templates"),
|
||||
@@ -145,15 +154,6 @@ export function useLeftMenu() {
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
{
|
||||
href: {name: "apps/list"},
|
||||
routes: routeStartWith("apps"),
|
||||
title: t("apps"),
|
||||
icon: {
|
||||
element: shallowRef(FormatListGroupPlus),
|
||||
class: "menu-icon"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("administration"),
|
||||
routes: routeStartWith("admin"),
|
||||
|
||||
@@ -41,9 +41,9 @@ export default {
|
||||
state.panel = panel;
|
||||
state.breadcrumbs[1] = {...breadcrumb, panel: true};
|
||||
},
|
||||
unsetPanel(state: State) {
|
||||
unsetPanel(state: State, shouldSplice = true) {
|
||||
state.panel = undefined;
|
||||
state.breadcrumbs.splice(1);
|
||||
if (shouldSplice) state.breadcrumbs.splice(1);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,8 +21,10 @@ export default {
|
||||
return response.data;
|
||||
})
|
||||
},
|
||||
listWithSubgroup({commit}) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins/groups/subgroups`, {}).then(response => {
|
||||
listWithSubgroup({commit}, options) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins/groups/subgroups`, {
|
||||
params: options
|
||||
}).then(response => {
|
||||
commit("setPlugins", response.data)
|
||||
commit("setPluginSingleList", response.data.map(plugin => plugin.tasks.concat(plugin.triggers, plugin.conditions, plugin.controllers, plugin.storages, plugin.taskRunners, plugin.charts, plugin.dataFilters, plugin.aliases, plugin.logExporters)).flat())
|
||||
return response.data;
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Haupteigenschaften",
|
||||
"error_handlers": "Fehlerbehandler",
|
||||
"tasks": "Aufgaben",
|
||||
"advanced": "Erweiterte Konfiguration"
|
||||
"advanced": "Erweiterte Konfiguration",
|
||||
"finally": "Schließlich"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Wählen Sie einen {section} aus",
|
||||
"tasks": "Aufgabe hinzufügen",
|
||||
"error handlers": "Fügen Sie einen Fehlerhandler hinzu",
|
||||
"triggers": "Füge einen Trigger hinzu"
|
||||
"triggers": "Füge einen Trigger hinzu",
|
||||
"finally": "Fügen Sie einen Finally-Block hinzu"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Aufgabe speichern",
|
||||
"triggers": "Speicher Trigger",
|
||||
"error handlers": "Fehlerbehandlungsroutine speichern",
|
||||
"input": "Eingabe speichern"
|
||||
"input": "Eingabe speichern",
|
||||
"finally": "Aufgabe speichern"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Kein Code-Editor",
|
||||
|
||||
@@ -821,18 +821,18 @@
|
||||
"edit": {
|
||||
"title": "Upgrade Your Namespace Management",
|
||||
"message": "In Kestra Enterprise Edition, namespaces provide advanced isolation and governance of secrets, variables and plugin defaults at scale. Administrators can configure custom secrets managers, isolated storage backends, dedicated worker groups, and fine-grained permissions on a per-namespace basis. This ensures that secrets, variables and plugin configurations remain secure and easy to maintain across different teams and projects."
|
||||
},
|
||||
},
|
||||
"variables": {
|
||||
"title": "Centrally Govern Your Variables",
|
||||
"message": "In Kestra Enterprise Edition, you can define and manage namespace-level variables to eliminate repetitive configurations across flows. These variables ensure consistency, simplify configuration updates, and can be easily referenced by any task or trigger for cleaner, more maintainable workflows."
|
||||
},
|
||||
"plugin-defaults": {
|
||||
"title": "Standardize Configuration with Plugin Defaults",
|
||||
"message": "In Kestra Enterprise Edition, you can set namespace-specific plugin defaults, reducing the need for duplicated setup in each flow. This central plugin governance enforces consistent configurations, allows secure referencing of secrets or variables, and simplifies maintenance of your workflows."
|
||||
},
|
||||
"secrets": {
|
||||
"title": "Manage Secrets in a Secure Way",
|
||||
"message": "In Kestra Enterprise Edition, you can store and control secrets at the namespace level, minimizing risk and ensuring each team's credentials remain isolated. Thanks to the nested hierarchy, you can also configure credentials in a parent namespace and they will be inherited by all child namespaces. Support for dedicated secrets managers and fine-grained namespace-level permission settings further strengthens security and compliance."
|
||||
"title": "Centrally Govern Your Variables",
|
||||
"message": "In Kestra Enterprise Edition, you can define and manage namespace-level variables to eliminate repetitive configurations across flows. These variables ensure consistency, simplify configuration updates, and can be easily referenced by any task or trigger for cleaner, more maintainable workflows."
|
||||
},
|
||||
"plugin-defaults": {
|
||||
"title": "Standardize Configuration with Plugin Defaults",
|
||||
"message": "In Kestra Enterprise Edition, you can set namespace-specific plugin defaults, reducing the need for duplicated setup in each flow. This central plugin governance enforces consistent configurations, allows secure referencing of secrets or variables, and simplifies maintenance of your workflows."
|
||||
},
|
||||
"secrets": {
|
||||
"title": "Manage Secrets in a Secure Way",
|
||||
"message": "In Kestra Enterprise Edition, you can store and control secrets at the namespace level, minimizing risk and ensuring each team's credentials remain isolated. Thanks to the nested hierarchy, you can also configure credentials in a parent namespace and they will be inherited by all child namespaces. Support for dedicated secrets managers and fine-grained namespace-level permission settings further strengthens security and compliance."
|
||||
},
|
||||
"audit-logs": {
|
||||
"title": "Track All Changes in One Place",
|
||||
@@ -1128,7 +1128,8 @@
|
||||
"advanced": "Advanced configuration",
|
||||
"tasks": "Tasks",
|
||||
"triggers": "Triggers",
|
||||
"error_handlers": "Error Handlers"
|
||||
"error_handlers": "Error Handlers",
|
||||
"finally": "Finally"
|
||||
},
|
||||
"fields": {
|
||||
"main": {
|
||||
@@ -1151,12 +1152,14 @@
|
||||
"select": "Select a {section}",
|
||||
"tasks": "Add a task",
|
||||
"triggers": "Add a trigger",
|
||||
"error handlers": "Add an error handler"
|
||||
"error handlers": "Add an error handler",
|
||||
"finally": "Add a finally block"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Save task",
|
||||
"triggers": "Save trigger",
|
||||
"error handlers": "Save error handler",
|
||||
"finally": "Save task",
|
||||
"input": "Save input"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Propiedades principales",
|
||||
"error_handlers": "Manejadores de Errores",
|
||||
"tasks": "Tareas",
|
||||
"advanced": "Configuración avanzada"
|
||||
"advanced": "Configuración avanzada",
|
||||
"finally": "Finalmente"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Seleccione una {section}",
|
||||
"tasks": "Agregar una task",
|
||||
"error handlers": "Agregar un manejador de errores",
|
||||
"triggers": "Agregar un trigger"
|
||||
"triggers": "Agregar un trigger",
|
||||
"finally": "Agregar un bloque finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Guardar tarea",
|
||||
"triggers": "Guardar trigger",
|
||||
"error handlers": "Guardar controlador de errores",
|
||||
"input": "Guardar input"
|
||||
"input": "Guardar input",
|
||||
"finally": "Guardar tarea"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Editor Sin Código",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Principales propriétés",
|
||||
"error_handlers": "Gestionnaires d'erreurs",
|
||||
"tasks": "Tâches",
|
||||
"advanced": "Configuration avancée"
|
||||
"advanced": "Configuration avancée",
|
||||
"finally": "Enfin"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Sélectionnez une {section}",
|
||||
"tasks": "Ajouter une tâche",
|
||||
"error handlers": "Ajouter un gestionnaire d'erreurs",
|
||||
"triggers": "Ajouter un trigger"
|
||||
"triggers": "Ajouter un trigger",
|
||||
"finally": "Ajouter un bloc finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Enregistrer la tâche",
|
||||
"triggers": "Enregistrer le trigger",
|
||||
"error handlers": "Enregistrer le gestionnaire d'erreurs",
|
||||
"input": "Enregistrer l'input"
|
||||
"input": "Enregistrer l'input",
|
||||
"finally": "Enregistrer la task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Éditeur sans code",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "मुख्य गुणधर्म",
|
||||
"error_handlers": "त्रुटि हैंडलर",
|
||||
"tasks": "कार्य",
|
||||
"advanced": "उन्नत कॉन्फ़िगरेशन"
|
||||
"advanced": "उन्नत कॉन्फ़िगरेशन",
|
||||
"finally": "अंत में"
|
||||
},
|
||||
"creation": {
|
||||
"select": "किसी {section} का चयन करें",
|
||||
"tasks": "कार्य जोड़ें",
|
||||
"error handlers": "त्रुटि हैंडलर जोड़ें",
|
||||
"triggers": "ट्रिगर जोड़ें"
|
||||
"triggers": "ट्रिगर जोड़ें",
|
||||
"finally": "अंत में एक finally ब्लॉक जोड़ें"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "कार्य सहेजें",
|
||||
"triggers": "ट्रिगर सहेजें",
|
||||
"error handlers": "त्रुटि हैंडलर सहेजें",
|
||||
"input": "इनपुट सहेजें"
|
||||
"input": "इनपुट सहेजें",
|
||||
"finally": "कार्य सहेजें"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "कोड संपादक नहीं",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Proprietà principali",
|
||||
"error_handlers": "Gestori degli Errori",
|
||||
"tasks": "Attività",
|
||||
"advanced": "Configurazione avanzata"
|
||||
"advanced": "Configurazione avanzata",
|
||||
"finally": "Infine"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Seleziona una {section}",
|
||||
"tasks": "Aggiungi un task",
|
||||
"error handlers": "Aggiungi un gestore degli errori",
|
||||
"triggers": "Aggiungi un trigger"
|
||||
"triggers": "Aggiungi un trigger",
|
||||
"finally": "Aggiungi un blocco finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Salva task",
|
||||
"triggers": "Salva trigger",
|
||||
"error handlers": "Salva gestore errori",
|
||||
"input": "Salva input"
|
||||
"input": "Salva input",
|
||||
"finally": "Salva task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Editor Senza Codice",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "メインプロパティ",
|
||||
"error_handlers": "エラーハンドラー",
|
||||
"tasks": "タスク",
|
||||
"advanced": "高度な設定"
|
||||
"advanced": "高度な設定",
|
||||
"finally": "最後に"
|
||||
},
|
||||
"creation": {
|
||||
"select": "{section}を選択してください",
|
||||
"tasks": "タスクを追加",
|
||||
"error handlers": "エラーハンドラーを追加",
|
||||
"triggers": "トリガーを追加"
|
||||
"triggers": "トリガーを追加",
|
||||
"finally": "finally ブロックを追加"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "タスクを保存",
|
||||
"triggers": "トリガーを保存",
|
||||
"error handlers": "エラーハンドラーを保存",
|
||||
"input": "入力を保存"
|
||||
"input": "入力を保存",
|
||||
"finally": "タスクを保存"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "コードエディタなし",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "주요 속성",
|
||||
"error_handlers": "오류 처리기",
|
||||
"tasks": "작업",
|
||||
"advanced": "고급 구성"
|
||||
"advanced": "고급 구성",
|
||||
"finally": "마지막으로"
|
||||
},
|
||||
"creation": {
|
||||
"select": "{section}을 선택하십시오",
|
||||
"tasks": "작업 추가",
|
||||
"error handlers": "오류 처리기 추가",
|
||||
"triggers": "트리거 추가"
|
||||
"triggers": "트리거 추가",
|
||||
"finally": "마지막 블록 추가"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "작업 저장",
|
||||
"triggers": "트리거 저장",
|
||||
"error handlers": "오류 처리기 저장",
|
||||
"input": "입력 저장"
|
||||
"input": "입력 저장",
|
||||
"finally": "작업 저장"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "코드 없는 편집기",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Główne właściwości",
|
||||
"error_handlers": "Obsługa błędów",
|
||||
"tasks": "Zadania",
|
||||
"advanced": "Zaawansowana konfiguracja"
|
||||
"advanced": "Zaawansowana konfiguracja",
|
||||
"finally": "Na koniec"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Wybierz {section}",
|
||||
"tasks": "Dodaj task",
|
||||
"error handlers": "Dodaj obsługę błędów",
|
||||
"triggers": "Dodaj trigger"
|
||||
"triggers": "Dodaj trigger",
|
||||
"finally": "Dodaj blok finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Zapisz task",
|
||||
"triggers": "Zapisz trigger",
|
||||
"error handlers": "Zapisz obsługę błędów",
|
||||
"input": "Zapisz input"
|
||||
"input": "Zapisz input",
|
||||
"finally": "Zapisz task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Edytor No Code",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Propriedades principais",
|
||||
"error_handlers": "Manipuladores de Erros",
|
||||
"tasks": "Tarefas",
|
||||
"advanced": "Configuração avançada"
|
||||
"advanced": "Configuração avançada",
|
||||
"finally": "Finalmente"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Selecione uma {section}",
|
||||
"tasks": "Adicionar uma task",
|
||||
"error handlers": "Adicionar um manipulador de erro",
|
||||
"triggers": "Adicionar um trigger"
|
||||
"triggers": "Adicionar um trigger",
|
||||
"finally": "Adicionar um bloco finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Salvar task",
|
||||
"triggers": "Salvar trigger",
|
||||
"error handlers": "Salvar manipulador de erro",
|
||||
"input": "Salvar input"
|
||||
"input": "Salvar input",
|
||||
"finally": "Salvar task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Editor Sem Código",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "Основные свойства",
|
||||
"error_handlers": "Обработчики ошибок",
|
||||
"tasks": "Задачи",
|
||||
"advanced": "Расширенная конфигурация"
|
||||
"advanced": "Расширенная конфигурация",
|
||||
"finally": "Наконец"
|
||||
},
|
||||
"creation": {
|
||||
"select": "Выберите {section}",
|
||||
"tasks": "Добавить task",
|
||||
"error handlers": "Добавить обработчик ошибок",
|
||||
"triggers": "Добавить trigger"
|
||||
"triggers": "Добавить trigger",
|
||||
"finally": "Добавить блок finally"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "Сохранить task",
|
||||
"triggers": "Сохранить trigger",
|
||||
"error handlers": "Сохранить обработчик ошибок",
|
||||
"input": "Сохранить input"
|
||||
"input": "Сохранить input",
|
||||
"finally": "Сохранить task"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "Редактор No Code",
|
||||
|
||||
@@ -1068,19 +1068,22 @@
|
||||
"main": "主要属性",
|
||||
"error_handlers": "错误处理程序",
|
||||
"tasks": "任务",
|
||||
"advanced": "高级配置"
|
||||
"advanced": "高级配置",
|
||||
"finally": "最后"
|
||||
},
|
||||
"creation": {
|
||||
"select": "选择一个{section}",
|
||||
"tasks": "添加任务",
|
||||
"error handlers": "添加错误处理程序",
|
||||
"triggers": "添加一个trigger"
|
||||
"triggers": "添加一个trigger",
|
||||
"finally": "添加一个finally块"
|
||||
},
|
||||
"save": {
|
||||
"tasks": "保存任务",
|
||||
"triggers": "保存 trigger",
|
||||
"error handlers": "保存错误处理程序",
|
||||
"input": "保存输入"
|
||||
"input": "保存输入",
|
||||
"finally": "保存任务"
|
||||
},
|
||||
"labels": {
|
||||
"no_code": "无代码编辑器",
|
||||
|
||||
@@ -286,4 +286,24 @@ export default class Utils {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
static getDateFormat(startDate, endDate) {
|
||||
if (!startDate || !endDate) {
|
||||
return "yyyy-MM-DD";
|
||||
}
|
||||
|
||||
const duration = moment.duration(moment(endDate).diff(moment(startDate)));
|
||||
|
||||
if (duration.asDays() > 365) {
|
||||
return "yyyy-MM";
|
||||
} else if (duration.asDays() > 180) {
|
||||
return "yyyy-'W'ww";
|
||||
} else if (duration.asDays() > 1) {
|
||||
return "yyyy-MM-DD";
|
||||
} else if (duration.asHours() > 1) {
|
||||
return "yyyy-MM-DD:HH:00";
|
||||
} else {
|
||||
return "yyyy-MM-DD:HH:mm";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,24 @@ export default class YamlUtils {
|
||||
return YamlUtils.cleanMetadata(yamlDoc.toString(TOSTRING_OPTIONS));
|
||||
}
|
||||
|
||||
static insertFinally(source, finallyTask) {
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
const newFinallyNode = yamlDoc.createNode(yaml.parseDocument(finallyTask));
|
||||
const items = yamlDoc.contents.items.find(item => item.key.value === "finally");
|
||||
if (items && items.value.items) {
|
||||
yamlDoc.contents.items[yamlDoc.contents.items.indexOf(items)].value.items.push(newFinallyNode);
|
||||
} else {
|
||||
if (items) {
|
||||
yamlDoc.contents.items.splice(yamlDoc.contents.items.indexOf(items), 1)
|
||||
}
|
||||
const finallySeq = new yaml.YAMLSeq();
|
||||
finallySeq.items.push(newFinallyNode);
|
||||
const newFinally = new yaml.Pair(new yaml.Scalar("finally"), finallySeq);
|
||||
yamlDoc.contents.items.push(newFinally);
|
||||
}
|
||||
return YamlUtils.cleanMetadata(yamlDoc.toString(TOSTRING_OPTIONS));
|
||||
}
|
||||
|
||||
static insertErrorInFlowable(source, errorTask, flowableTask) {
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
const newErrorNode = yamlDoc.createNode(yaml.parseDocument(errorTask));
|
||||
@@ -571,7 +589,7 @@ export default class YamlUtils {
|
||||
return source;
|
||||
}
|
||||
|
||||
const order = ["id", "namespace", "description", "retry", "labels", "inputs", "variables", "tasks", "triggers", "errors", "pluginDefaults", "taskDefaults", "concurrency", "outputs", "disabled"];
|
||||
const order = ["id", "namespace", "description", "retry", "labels", "inputs", "variables", "tasks", "triggers", "errors", "finally", "pluginDefaults", "taskDefaults", "concurrency", "outputs", "disabled"];
|
||||
const updatedItems = [];
|
||||
for (const prop of order) {
|
||||
const item = yamlDoc.contents.items.find(e => e.key.value === prop);
|
||||
|
||||
@@ -157,22 +157,23 @@ public class DashboardController {
|
||||
@Parameter(description = "The chart id") @PathVariable String chartId,
|
||||
@Parameter(description = "The filters to apply, some can override chart definition like labels & namespace") @Body GlobalFilter globalFilter
|
||||
) throws IOException {
|
||||
ZonedDateTime startDate = globalFilter.getStartDate();
|
||||
ZonedDateTime endDate = globalFilter.getEndDate();
|
||||
if (startDate == null || endDate == null) {
|
||||
throw new IllegalArgumentException("`startDate` and `endDate` filters are required.");
|
||||
}
|
||||
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new IllegalArgumentException("`endDate` must be after `startDate`.");
|
||||
}
|
||||
|
||||
String tenantId = tenantService.resolveTenant();
|
||||
Dashboard dashboard = dashboardRepository.get(tenantId, id).orElse(null);
|
||||
if (dashboard == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ZonedDateTime endDate = globalFilter.getEndDate();
|
||||
ZonedDateTime startDate = globalFilter.getStartDate();
|
||||
if (startDate == null || endDate == null) {
|
||||
endDate = ZonedDateTime.now();
|
||||
startDate = endDate.minus(dashboard.getTimeWindow().getDefaultDuration());
|
||||
}
|
||||
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new IllegalArgumentException("`endDate` must be after `startDate`.");
|
||||
}
|
||||
|
||||
Duration windowDuration = Duration.ofSeconds(endDate.minus(Duration.ofSeconds(startDate.toEpochSecond())).toEpochSecond());
|
||||
if (windowDuration.compareTo(dashboard.getTimeWindow().getMax()) > 0) {
|
||||
throw new IllegalArgumentException("The queried window is larger than the max allowed one.");
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
package io.kestra.webserver.controllers.api;
|
||||
|
||||
import io.kestra.core.docs.ClassInputDocumentation;
|
||||
import io.kestra.core.docs.ClassPluginDocumentation;
|
||||
import io.kestra.core.docs.DocumentationGenerator;
|
||||
import io.kestra.core.docs.DocumentationWithSchema;
|
||||
import io.kestra.core.docs.InputType;
|
||||
import io.kestra.core.docs.JsonSchemaGenerator;
|
||||
import io.kestra.core.docs.Plugin;
|
||||
import io.kestra.core.docs.PluginIcon;
|
||||
import io.kestra.core.docs.Schema;
|
||||
import io.kestra.core.docs.SchemaType;
|
||||
import io.kestra.core.docs.*;
|
||||
import io.kestra.core.models.dashboards.Dashboard;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.Input;
|
||||
@@ -39,11 +30,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -252,16 +239,18 @@ public class PluginController {
|
||||
@Get("/groups/subgroups")
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Operation(tags = {"Plugins"}, summary = "Get plugins group by subgroups")
|
||||
public List<Plugin> subgroups() {
|
||||
public List<Plugin> subgroups(
|
||||
@Parameter(description = "Whether to include deprecated plugins") @QueryValue(value = "includeDeprecated", defaultValue = "true") boolean includeDeprecated
|
||||
) {
|
||||
return Stream.concat(
|
||||
pluginRegistry.plugins()
|
||||
.stream()
|
||||
.map(p -> Plugin.of(p, null)),
|
||||
.map(p -> Plugin.of(p, null, includeDeprecated)),
|
||||
pluginRegistry.plugins()
|
||||
.stream()
|
||||
.flatMap(p -> p.subGroupNames()
|
||||
.stream()
|
||||
.map(subgroup -> Plugin.of(p, subgroup))
|
||||
.map(subgroup -> Plugin.of(p, subgroup, includeDeprecated))
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
|
||||
Reference in New Issue
Block a user