Compare commits

...

48 Commits

Author SHA1 Message Date
Loïc Mathieu
aca5a9ff4c chore: version 0.21.0 2025-02-04 13:42:40 +01:00
brian.mulier
a6ce86d702 fix(ui): null-safe search filters 2025-02-04 11:41:18 +01:00
Ludovic DEHON
4392c89ec7 fix(core): process runner are not serialized correctly on worker
close #7053
2025-02-03 21:28:03 +01:00
Loïc Mathieu
d74a31ba7f chore: version 0.21.0-rc2-SNAPSHOT 2025-02-03 16:08:11 +01:00
Bart Ledoux
cb3195900f fix: enterprise edition tag in light mode 2025-02-03 16:07:14 +01:00
Bart Ledoux
cf4b91f44d fix: bring back hover in main menu 2025-02-03 16:06:17 +01:00
Bart Ledoux
33ecf8d5f5 fix: sidemenu bring back the gray hover 2025-02-03 16:06:09 +01:00
Bart Ledoux
39a2293a45 fix: setup docId for blueprints 2025-02-03 16:05:45 +01:00
Miloš Paunović
88c93995df fix(ui): get the string fields in no code to use editor and have auto completion back (#7150) 2025-02-03 15:06:26 +01:00
MilosPaunovic
6afe5ff41f chore(ui): properly pass a prop related to saved searches 2025-02-03 15:06:26 +01:00
Miloš Paunović
a3a8863f46 feat(ui): multiple improvements of no code editor (#7146)
* refactor(ui): prevent multiple warning in console by adding inheritAttrs properly

* chore(ui): make plugin selector field not clearable

* feat(ui): allow re-ordering of array items

* fix(ui): remove concurrency when limit set to 0
2025-02-03 15:06:26 +01:00
Piyush Bhaskar
fcfee5116b fix(ui): Custom Dashboard name overflows. (#7124)
* fix(ui): Custom Dashboard name overflows.

* fix(ui): avoid dashboard button being too long

---------

Co-authored-by: YannC <ycoornaert@kestra.io>
2025-02-03 15:05:30 +01:00
brian.mulier
3f2d91014b fix(ui): switching from custom Flow blueprints tab to dashboard was not working 2025-02-03 14:00:53 +01:00
Florian Hussonnois
41149a83b3 ci: fix release workflows 2025-02-03 11:50:23 +01:00
Loïc Mathieu
1ed882e8f3 fix(core): remove the dynamic property patterns 2025-02-03 10:08:09 +01:00
brian-mulier-p
0f6e0de29c fix(ui): restore namespace filter manual typing & various improvements (#7127) 2025-02-01 10:09:19 +01:00
brian.mulier
238bc532c3 chore(deps): bump ui-libs to v0.0.125 2025-01-31 18:15:47 +01:00
Florian Hussonnois
6919848ab3 ci: fix runner on release workflows 2025-01-31 16:29:20 +01:00
Florian Hussonnois
86aec88de4 chore(version): update to version 'v0.21.0-rc1-SNAPSHOT' 2025-01-31 15:54:24 +01:00
Bart Ledoux
f609d57a0c build: prevent corepack crash 2025-01-31 15:54:24 +01:00
Bart Ledoux
f3852a3c24 build: try and fix FE CI 2025-01-31 15:54:24 +01:00
GitHub Action
804ff6a81c chore(translations): auto generate values for languages other than english 2025-01-31 13:52:51 +01:00
Miloš Paunović
7869f90edd feat(ui): add finally block to no code editor (#7123) 2025-01-31 13:52:45 +01:00
Florian Hussonnois
2b72306b3d fix(ci): update scripts/workflows for plugins 2025-01-31 12:10:30 +01:00
Florian Hussonnois
f0d5d4b93f ci: fix workflow docker for all plugins 2025-01-31 11:45:45 +01:00
Florian Hussonnois
4e4ab80b2f ci: fix workflow docker 2025-01-31 11:45:31 +01:00
Florian Hussonnois
c33d08afda ci: update workflow docker 2025-01-31 11:45:17 +01:00
Florian Hussonnois
a246ac38f5 ci: update workflow docker 2025-01-31 11:45:07 +01:00
Florian Hussonnois
7bdaa81dee fix(ui): fix missing param kind for blueprint in flow editor (#7087)
fix: #7087
2025-01-31 11:44:11 +01:00
Miloš Paunović
6a1d831849 feat(ui): allow task re-ordering from no code editor (#7120) 2025-01-31 10:56:16 +01:00
Loïc Mathieu
95d2d1dfa3 fix(core): retry flaky test TimeoutTest.timeout()
As its failure cannot be reproduced locally even with 100 repetitions, there is no other choice than retrying it.
2025-01-31 09:48:11 +01:00
Loïc Mathieu
d12dd179c2 fix(core): subflow labels must not be overriden by parent flow ones 2025-01-31 09:47:59 +01:00
Loïc Mathieu
ceda5eb8ee fix(core): subflow validation didn't work anymore 2025-01-30 16:17:59 +01:00
Miloš Paunović
1301aaac76 feat(ui): improve the task array component (#7095)
* feat(ui): improve the task array component

* chore(ui): replace existing task on editing during creation instead of re-adding them
2025-01-30 14:37:09 +01:00
AJ Emerich
5f7468a9a4 fix(docs): remove custom dashboard website component
https://github.com/kestra-io/kestra/issues/7085
2025-01-30 12:16:30 +01:00
Miloš Paunović
aa24c888a3 chore(ui): properly check the existence of fields inside schema
* chore(ui): remove unnecessary binding of listeners

* chore(ui): check the existence of fields
2025-01-30 11:42:37 +01:00
Loïc Mathieu
c792d9b6ea fix(cli): repeate flaky tests FileChangedEventListenerTest
This is inherently racy as it's async and watch the filesystem which cannot be done reliabily.
2025-01-30 10:55:57 +01:00
Loïc Mathieu
a921b95404 chore(deps): downgrade Protobuf to 3.25.5
3.25.6 is not compatible with 3.25.5 and Orc still uses 3.25.5
2025-01-30 10:40:57 +01:00
Miloš Paunović
e46df069a9 feat(ui): multiple improvements of no code editor (#7076)
* fix(ui): allow creation of multiple tasks from the no code editor

* chore(ui): make input text be of textarea type for resizability

* chore(ui): allow to add task from topology either before or after the target one
2025-01-30 10:33:11 +01:00
Loïc Mathieu
c08f4f24ca fix(script): AbstractExecScript.injectDefaults should throw IllegalVariableEvaluationException 2025-01-30 09:58:45 +01:00
Miloš Paunović
67b3937824 chore(ui): move apps link in left menu just below the flows (#7063) 2025-01-30 09:25:16 +01:00
Miloš Paunović
17e1623342 fix(ui): amend no code editor breadcrumbs issue (#7054)
* chore(ui): task array component to have margins between lines

* fix(ui): amend no code editor breadcrumbs issue
2025-01-30 09:25:01 +01:00
Loïc Mathieu
d12fbf05b0 fix(core): restartForEachItem() is flaky
With this test change, running 100 tests with MySQL pass!
2025-01-29 17:11:14 +01:00
YannC
efa2d44e76 feat(webserver): if no date provided for dashboard, then use default timewindow 2025-01-29 16:37:27 +01:00
YannC
acdb46cea0 fix(ui): dynamic format date
close #7015
2025-01-29 16:37:21 +01:00
Loïc Mathieu
c1807516f5 chore(deps): downgrade protobug
Orc uses an older version.
And probably also other libs that we're using are still in 3.x
2025-01-29 15:52:10 +01:00
brian.mulier
ab796dff93 feat(ui): don't show deprecated tasks in the plugins list
closes #4526
2025-01-29 15:49:23 +01:00
Loïc Mathieu
2d98f909de fix(cli): flow watcher should compute plugin defaults
fixes #6908
2025-01-29 15:42:55 +01:00
74 changed files with 872 additions and 444 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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' }}

View File

@@ -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 }}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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)
);
}
}

View File

@@ -0,0 +1,12 @@
micronaut:
io:
watch:
enabled: true
paths:
- build/file-watch
kestra:
repository:
type: memory
queue:
type: memory

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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))

View File

@@ -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()) {

View File

@@ -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"));

View File

@@ -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));
}
}

View File

@@ -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"))));
}
}
}

View File

@@ -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()) {

View File

@@ -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()));

View File

@@ -10,6 +10,7 @@ inputs:
labels:
switchFlowLabel: switchFoo
overriding: child
tasks:
- id: parent-seq

View File

@@ -7,6 +7,7 @@ inputs:
labels:
mainFlowLabel: flowFoo
overriding: parent
tasks:
- id: launch

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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"

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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.

View File

@@ -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},

View File

@@ -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">

View File

@@ -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">

View File

@@ -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},

View File

@@ -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},

View File

@@ -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>

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -32,6 +32,7 @@
import {SECTIONS} from "../../utils/constants.js";
export default {
inheritAttrs: false,
computed: {
...mapGetters("flow", ["taskError"]),
},

View File

@@ -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>

View File

@@ -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]) => {

View File

@@ -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() {

View File

@@ -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();

View File

@@ -293,10 +293,12 @@
);
};
const onCreateNewTask = () => {
const onCreateNewTask = (details) => {
emit("openNoCode", {
section: SECTIONS.TASKS.toLowerCase(),
identifier: "new",
target: details[0],
position: details[1],
});
};

View File

@@ -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 {

View File

@@ -70,7 +70,9 @@
},
methods: {
loadToc() {
this.$store.dispatch("plugin/listWithSubgroup")
this.$store.dispatch("plugin/listWithSubgroup", {
includeDeprecated: false
})
},
loadPlugin() {

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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,

View File

@@ -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"),

View File

@@ -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);
},
},
};

View File

@@ -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;

View File

@@ -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",

View File

@@ -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"
}
},

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "कोड संपादक नहीं",

View File

@@ -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",

View File

@@ -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": "コードエディタなし",

View File

@@ -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": "코드 없는 편집기",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "无代码编辑器",

View File

@@ -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";
}
}
}

View File

@@ -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);

View File

@@ -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.");

View File

@@ -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()