mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
80 Commits
run-develo
...
v0.21.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e891f64a2 | ||
|
|
47cc38d89e | ||
|
|
d2f9060b5c | ||
|
|
c36cc504eb | ||
|
|
8d3b3a8493 | ||
|
|
e7955ca7bf | ||
|
|
016cd09849 | ||
|
|
23846d6100 | ||
|
|
0b247b709e | ||
|
|
bfee53a9b1 | ||
|
|
70a3c98aca | ||
|
|
a923124108 | ||
|
|
92484c0333 | ||
|
|
eb21452a83 | ||
|
|
433fe963e2 | ||
|
|
7a2390ddf7 | ||
|
|
1c6a14d17a | ||
|
|
0ba64f7979 | ||
|
|
38720e96a9 | ||
|
|
0f7d9b2adc | ||
|
|
210fc246ac | ||
|
|
df0d037f66 | ||
|
|
07ea309a47 | ||
|
|
1f09f53a88 | ||
|
|
f356921daa | ||
|
|
3d50ef03f7 | ||
|
|
7b309eb2d2 | ||
|
|
b22b0642ed | ||
|
|
1cbc9195c4 | ||
|
|
b853dd0b6e | ||
|
|
f7df60419c | ||
|
|
9f76cae55e | ||
|
|
aca5a9ff4c | ||
|
|
a6ce86d702 | ||
|
|
4392c89ec7 | ||
|
|
d74a31ba7f | ||
|
|
cb3195900f | ||
|
|
cf4b91f44d | ||
|
|
33ecf8d5f5 | ||
|
|
39a2293a45 | ||
|
|
88c93995df | ||
|
|
6afe5ff41f | ||
|
|
a3a8863f46 | ||
|
|
fcfee5116b | ||
|
|
3f2d91014b | ||
|
|
41149a83b3 | ||
|
|
1ed882e8f3 | ||
|
|
0f6e0de29c | ||
|
|
238bc532c3 | ||
|
|
6919848ab3 | ||
|
|
86aec88de4 | ||
|
|
f609d57a0c | ||
|
|
f3852a3c24 | ||
|
|
804ff6a81c | ||
|
|
7869f90edd | ||
|
|
2b72306b3d | ||
|
|
f0d5d4b93f | ||
|
|
4e4ab80b2f | ||
|
|
c33d08afda | ||
|
|
a246ac38f5 | ||
|
|
7bdaa81dee | ||
|
|
6a1d831849 | ||
|
|
95d2d1dfa3 | ||
|
|
d12dd179c2 | ||
|
|
ceda5eb8ee | ||
|
|
1301aaac76 | ||
|
|
5f7468a9a4 | ||
|
|
aa24c888a3 | ||
|
|
c792d9b6ea | ||
|
|
a921b95404 | ||
|
|
e46df069a9 | ||
|
|
c08f4f24ca | ||
|
|
67b3937824 | ||
|
|
17e1623342 | ||
|
|
d12fbf05b0 | ||
|
|
efa2d44e76 | ||
|
|
acdb46cea0 | ||
|
|
c1807516f5 | ||
|
|
ab796dff93 | ||
|
|
2d98f909de |
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -9,6 +9,8 @@ jobs:
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
name: Check & Publish
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
24
.github/workflows/docker.yml
vendored
24
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Create Docker images on tag
|
||||
name: Create Docker images on Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
release-tag:
|
||||
description: 'Kestra Release Tag'
|
||||
required: false
|
||||
type: string
|
||||
plugin-version:
|
||||
description: 'Plugin version'
|
||||
required: false
|
||||
@@ -38,7 +42,6 @@ jobs:
|
||||
name: Publish Docker
|
||||
needs: [ plugins ]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
strategy:
|
||||
matrix:
|
||||
image:
|
||||
@@ -57,10 +60,19 @@ jobs:
|
||||
- name: Set image name
|
||||
id: vars
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/*/}
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ inputs.release-tag }}" == "" ]]; then
|
||||
TAG=${GITHUB_REF#refs/*/}
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
TAG="${{ inputs.release-tag }}"
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [[ "${{ env.PLUGIN_VERSION }}" == *"-SNAPSHOT" ]]; then
|
||||
echo "plugins=--repositories=https://s01.oss.sonatype.org/content/repositories/snapshots ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# Download release
|
||||
- name: Download release
|
||||
uses: robinraju/release-downloader@v1.11
|
||||
|
||||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -35,6 +35,8 @@ env:
|
||||
DOCKER_APT_PACKAGES: python3 python3-venv python-is-python3 python3-pip nodejs npm curl zip unzip
|
||||
DOCKER_PYTHON_LIBRARIES: kestra
|
||||
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build Artifacts
|
||||
@@ -45,13 +47,14 @@ jobs:
|
||||
docker-artifact-name: ${{ steps.vars.outputs.artifact }}
|
||||
plugins: ${{ steps.plugins-list.outputs.plugins }}
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout current ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout GitHub Actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
path: actions
|
||||
|
||||
25
.github/workflows/release-plugins.yml
vendored
25
.github/workflows/release-plugins.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releaseVersion:
|
||||
description: 'The release version (e.g., 0.21.0)'
|
||||
description: 'The release version (e.g., 0.21.0-RC1)'
|
||||
required: true
|
||||
type: string
|
||||
nextVersion:
|
||||
@@ -18,13 +18,29 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Release plugins
|
||||
runs-on: kestra-private-standard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
path: actions
|
||||
ref: main
|
||||
|
||||
# Setup build
|
||||
- uses: ./actions/.github/actions/setup-build
|
||||
id: build
|
||||
with:
|
||||
java-enabled: true
|
||||
node-enabled: true
|
||||
python-enabled: true
|
||||
caches-enabled: true
|
||||
|
||||
# Get Plugins List
|
||||
- name: Get Plugins List
|
||||
uses: ./.github/actions/plugins-list
|
||||
@@ -33,6 +49,11 @@ jobs:
|
||||
with:
|
||||
plugin-version: 'LATEST'
|
||||
|
||||
- name: 'Configure Git'
|
||||
run: |
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
# Execute
|
||||
- name: Run Gradle Release
|
||||
if: ${{ github.event.inputs.dryRun == 'false' }}
|
||||
|
||||
29
.github/workflows/tag-plugins.yml
vendored
29
.github/workflows/tag-plugins.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Update and Tag Kestra Plugins
|
||||
name: Set Version and Tag Plugins
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,13 +14,29 @@ on:
|
||||
jobs:
|
||||
tag:
|
||||
name: Release plugins
|
||||
runs-on: kestra-private-standard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
path: actions
|
||||
ref: main
|
||||
|
||||
# Setup build
|
||||
- uses: ./actions/.github/actions/setup-build
|
||||
id: build
|
||||
with:
|
||||
java-enabled: true
|
||||
node-enabled: true
|
||||
python-enabled: true
|
||||
caches-enabled: true
|
||||
|
||||
# Get Plugins List
|
||||
- name: Get Plugins List
|
||||
uses: ./.github/actions/plugins-list
|
||||
@@ -29,8 +45,13 @@ jobs:
|
||||
with:
|
||||
plugin-version: 'LATEST'
|
||||
|
||||
- name: 'Configure Git'
|
||||
run: |
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
# Execute
|
||||
- name: Tag Plugins
|
||||
- name: Set Version and Tag Plugins
|
||||
if: ${{ github.event.inputs.dryRun == 'false' }}
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
@@ -41,7 +62,7 @@ jobs:
|
||||
--yes \
|
||||
${{ steps.plugins-list.outputs.repositories }}
|
||||
|
||||
- name: Run Gradle Release (DRY_RUN)
|
||||
- name: Set Version and Tag Plugins (DRY_RUN)
|
||||
if: ${{ github.event.inputs.dryRun == 'true' }}
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.services.FlowListenersInterface;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.micronaut.context.annotation.Value;
|
||||
import io.micronaut.scheduling.io.watch.FileWatchConfiguration;
|
||||
@@ -36,6 +37,9 @@ public class FileChangedEventListener {
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepositoryInterface;
|
||||
|
||||
@Inject
|
||||
private PluginDefaultService pluginDefaultService;
|
||||
|
||||
@Inject
|
||||
private YamlParser yamlParser;
|
||||
|
||||
@@ -64,7 +68,7 @@ public class FileChangedEventListener {
|
||||
|
||||
public void startListeningFromConfig() throws IOException, InterruptedException {
|
||||
if (fileWatchConfiguration != null && fileWatchConfiguration.isEnabled()) {
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface);
|
||||
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface, pluginDefaultService);
|
||||
List<Path> paths = fileWatchConfiguration.getPaths();
|
||||
this.setup(paths);
|
||||
|
||||
@@ -107,7 +111,6 @@ public class FileChangedEventListener {
|
||||
} else {
|
||||
log.info("File watching is disabled.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void startListening(List<Path> paths) throws IOException, InterruptedException {
|
||||
@@ -118,60 +121,64 @@ public class FileChangedEventListener {
|
||||
WatchKey key;
|
||||
while ((key = watchService.take()) != null) {
|
||||
for (WatchEvent<?> watchEvent : key.pollEvents()) {
|
||||
WatchEvent.Kind<?> kind = watchEvent.kind();
|
||||
Path entry = (Path) watchEvent.context();
|
||||
try {
|
||||
WatchEvent.Kind<?> kind = watchEvent.kind();
|
||||
Path entry = (Path) watchEvent.context();
|
||||
|
||||
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
|
||||
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
|
||||
|
||||
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
if (Files.isDirectory(filePath)) {
|
||||
loadFlowsFromFolder(filePath);
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
if (Files.isDirectory(filePath)) {
|
||||
loadFlowsFromFolder(filePath);
|
||||
} else {
|
||||
|
||||
try {
|
||||
String content = Files.readString(filePath, Charset.defaultCharset());
|
||||
try {
|
||||
String content = Files.readString(filePath, Charset.defaultCharset());
|
||||
|
||||
Optional<Flow> flow = parseFlow(content, entry);
|
||||
if (flow.isPresent()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
// Check if we already have a file with the given path
|
||||
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
|
||||
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
|
||||
// Check if Flow from file has id/namespace updated
|
||||
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
|
||||
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
|
||||
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
|
||||
Optional<Flow> flow = parseFlow(content, entry);
|
||||
if (flow.isPresent()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
// Check if we already have a file with the given path
|
||||
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
|
||||
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
|
||||
// Check if Flow from file has id/namespace updated
|
||||
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
|
||||
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
|
||||
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
}
|
||||
} else {
|
||||
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
|
||||
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
|
||||
}
|
||||
|
||||
flowFilesManager.createOrUpdateFlow(flow.get(), content);
|
||||
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
|
||||
} catch (NoSuchFileException e) {
|
||||
log.error("File not found: {}", entry, e);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading file: {}", entry, e);
|
||||
}
|
||||
|
||||
} catch (NoSuchFileException e) {
|
||||
log.error("File not found: {}", entry, e);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading file: {}", entry, e);
|
||||
}
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
flows.stream()
|
||||
.filter(flow -> flow.getPath().equals(filePath.toString()))
|
||||
.findFirst()
|
||||
.ifPresent(flowWithPath -> {
|
||||
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
|
||||
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Path filePath = ((Path) key.watchable()).resolve(entry);
|
||||
flows.stream()
|
||||
.filter(flow -> flow.getPath().equals(filePath.toString()))
|
||||
.findFirst()
|
||||
.ifPresent(flowWithPath -> {
|
||||
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
|
||||
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error while watching flows", e);
|
||||
}
|
||||
}
|
||||
key.reset();
|
||||
@@ -230,7 +237,8 @@ public class FileChangedEventListener {
|
||||
private Optional<Flow> parseFlow(String content, Path entry) {
|
||||
try {
|
||||
Flow flow = yamlParser.parse(content, Flow.class);
|
||||
modelValidator.validate(flow);
|
||||
FlowWithSource withPluginDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
modelValidator.validate(withPluginDefault);
|
||||
return Optional.of(flow);
|
||||
} catch (ConstraintViolationException e) {
|
||||
log.warn("Error while parsing flow: {}", entry, e);
|
||||
|
||||
@@ -3,32 +3,36 @@ package io.kestra.cli.services;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.kestra.core.services.PluginDefaultService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Requires(property = "micronaut.io.watch.enabled", value = "true")
|
||||
@Slf4j
|
||||
public class LocalFlowFileWatcher implements FlowFilesManager {
|
||||
private FlowRepositoryInterface flowRepositoryInterface;
|
||||
private final FlowRepositoryInterface flowRepository;
|
||||
private final PluginDefaultService pluginDefaultService;
|
||||
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepositoryInterface) {
|
||||
this.flowRepositoryInterface = flowRepositoryInterface;
|
||||
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepository, PluginDefaultService pluginDefaultService) {
|
||||
this.flowRepository = flowRepository;
|
||||
this.pluginDefaultService = pluginDefaultService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlowWithSource createOrUpdateFlow(Flow flow, String content) {
|
||||
return flowRepositoryInterface.findById(null, flow.getNamespace(), flow.getId())
|
||||
.map(previous -> flowRepositoryInterface.update(flow, previous, content, flow))
|
||||
.orElseGet(() -> flowRepositoryInterface.create(flow, content, flow));
|
||||
FlowWithSource withDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
|
||||
return flowRepository.findById(null, flow.getNamespace(), flow.getId())
|
||||
.map(previous -> flowRepository.update(flow, previous, content, withDefault))
|
||||
.orElseGet(() -> flowRepository.create(flow, content, withDefault));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFlow(FlowWithSource toDelete) {
|
||||
flowRepositoryInterface.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepositoryInterface::delete);
|
||||
log.error("Flow {} has been deleted", toDelete.getId());
|
||||
flowRepository.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepository::delete);
|
||||
log.info("Flow {} has been deleted", toDelete.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFlow(String tenantId, String namespace, String id) {
|
||||
flowRepositoryInterface.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepositoryInterface::delete);
|
||||
log.error("Flow {} has been deleted", id);
|
||||
flowRepository.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepository::delete);
|
||||
log.info("Flow {} has been deleted", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package io.kestra.cli.services;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.utils.Await;
|
||||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
@MicronautTest(environments = {"test", "file-watch"}, transactional = false)
|
||||
class FileChangedEventListenerTest {
|
||||
public static final String FILE_WATCH = "build/file-watch";
|
||||
@Inject
|
||||
private FileChangedEventListener fileWatcher;
|
||||
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepository;
|
||||
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws IOException {
|
||||
if (!Files.exists(Path.of(FILE_WATCH))) {
|
||||
Files.createDirectories(Path.of(FILE_WATCH));
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() throws IOException {
|
||||
if (Files.exists(Path.of(FILE_WATCH))) {
|
||||
FileUtils.deleteDirectory(Path.of(FILE_WATCH).toFile());
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Exception {
|
||||
if (started.compareAndSet(false, true)) {
|
||||
executorService.execute(throwRunnable(() -> fileWatcher.startListeningFromConfig()));
|
||||
}
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but always pass locally
|
||||
void test() throws IOException, TimeoutException {
|
||||
// remove the flow if it already exists
|
||||
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "myflow").ifPresent(flow -> flowRepository.delete(flow));
|
||||
|
||||
// create a basic flow
|
||||
String flow = """
|
||||
id: myflow
|
||||
namespace: io.kestra.tests.watch
|
||||
|
||||
tasks:
|
||||
- id: hello
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello World! 🚀
|
||||
""";
|
||||
Files.write(Path.of(FILE_WATCH + "/myflow.yaml"), flow.getBytes());
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isPresent(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
Flow myflow = flowRepository.findById(null, "io.kestra.tests.watch", "myflow").orElseThrow();
|
||||
assertThat(myflow.getTasks(), hasSize(1));
|
||||
assertThat(myflow.getTasks().getFirst().getId(), is("hello"));
|
||||
assertThat(myflow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
|
||||
|
||||
// delete the flow
|
||||
Files.delete(Path.of(FILE_WATCH + "/myflow.yaml"));
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isEmpty(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but always pass locally
|
||||
void testWithPluginDefault() throws IOException, TimeoutException {
|
||||
// remove the flow if it already exists
|
||||
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "pluginDefault").ifPresent(flow -> flowRepository.delete(flow));
|
||||
|
||||
// create a flow with plugin default
|
||||
String pluginDefault = """
|
||||
id: pluginDefault
|
||||
namespace: io.kestra.tests.watch
|
||||
|
||||
tasks:
|
||||
- id: helloWithDefault
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
|
||||
pluginDefaults:
|
||||
- type: io.kestra.plugin.core.log.Log
|
||||
values:
|
||||
message: Hello World!
|
||||
""";
|
||||
Files.write(Path.of(FILE_WATCH + "/plugin-default.yaml"), pluginDefault.getBytes());
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isPresent(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
Flow pluginDefaultFlow = flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").orElseThrow();
|
||||
assertThat(pluginDefaultFlow.getTasks(), hasSize(1));
|
||||
assertThat(pluginDefaultFlow.getTasks().getFirst().getId(), is("helloWithDefault"));
|
||||
assertThat(pluginDefaultFlow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
|
||||
|
||||
// delete both files
|
||||
Files.delete(Path.of(FILE_WATCH + "/plugin-default.yaml"));
|
||||
Await.until(
|
||||
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isEmpty(),
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
}
|
||||
}
|
||||
12
cli/src/test/resources/application-file-watch.yml
Normal file
12
cli/src/test/resources/application-file-watch.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
micronaut:
|
||||
io:
|
||||
watch:
|
||||
enabled: true
|
||||
paths:
|
||||
- build/file-watch
|
||||
|
||||
kestra:
|
||||
repository:
|
||||
type: memory
|
||||
queue:
|
||||
type: memory
|
||||
@@ -311,10 +311,12 @@ public class JsonSchemaGenerator {
|
||||
if (member.getDeclaredType().isInstanceOf(Property.class)) {
|
||||
memberAttributes.put("$dynamic", true);
|
||||
// if we are in the String definition of a Property but the target type is not String: we configure the pattern
|
||||
Class<?> targetType = member.getDeclaredType().getTypeParameters().getFirst().getErasedType();
|
||||
if (!String.class.isAssignableFrom(targetType) && String.class.isAssignableFrom(member.getType().getErasedType())) {
|
||||
memberAttributes.put("pattern", ".*{{.*}}.*");
|
||||
}
|
||||
// TODO this was a good idea but their is too much cases where it didn't work like in List or Map so if we want it we need to make it more clever
|
||||
// I keep it for now commented but at some point we may want to re-do and improve it or remove these commented lines
|
||||
// Class<?> targetType = member.getDeclaredType().getTypeParameters().getFirst().getErasedType();
|
||||
// if (!String.class.isAssignableFrom(targetType) && String.class.isAssignableFrom(member.getType().getErasedType())) {
|
||||
// memberAttributes.put("pattern", ".*{{.*}}.*");
|
||||
// }
|
||||
} else if (member.getDeclaredType().isInstanceOf(Data.class)) {
|
||||
memberAttributes.put("$dynamic", false);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ public class Plugin {
|
||||
private String subGroup;
|
||||
|
||||
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup) {
|
||||
return Plugin.of(registeredPlugin, subgroup, true);
|
||||
}
|
||||
|
||||
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup, boolean includeDeprecated) {
|
||||
Plugin plugin = new Plugin();
|
||||
plugin.name = registeredPlugin.name();
|
||||
PluginSubGroup subGroupInfos = null;
|
||||
@@ -80,17 +84,17 @@ public class Plugin {
|
||||
|
||||
plugin.subGroup = subgroup;
|
||||
|
||||
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.apps = filterAndGetClassName(registeredPlugin.getApps()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.apps = filterAndGetClassName(registeredPlugin.getApps(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
|
||||
|
||||
return plugin;
|
||||
}
|
||||
@@ -100,12 +104,14 @@ public class Plugin {
|
||||
* Those classes are only filtered from the documentation to ensure backward compatibility.
|
||||
*
|
||||
* @param list The list of classes?
|
||||
* @param includeDeprecated whether to include deprecated plugins or not
|
||||
* @return a filtered streams.
|
||||
*/
|
||||
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list) {
|
||||
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list, boolean includeDeprecated) {
|
||||
return list
|
||||
.stream()
|
||||
.filter(not(io.kestra.core.models.Plugin::isInternal))
|
||||
.filter(p -> includeDeprecated || !io.kestra.core.models.Plugin.isDeprecated(p))
|
||||
.map(Class::getName)
|
||||
.filter(c -> !c.startsWith("org.kestra."))
|
||||
.toList();
|
||||
|
||||
@@ -13,9 +13,11 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
@Builder(toBuilder = true)
|
||||
@Getter
|
||||
@Jacksonized
|
||||
public class HttpConfiguration {
|
||||
@Schema(title = "The timeout configuration.")
|
||||
@PluginProperty
|
||||
|
||||
@@ -2,23 +2,26 @@ package io.kestra.core.models.tasks.runners;
|
||||
|
||||
import io.kestra.core.models.tasks.Output;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Builder
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
public class TaskRunnerResult<T extends TaskRunnerDetailResult> implements Output {
|
||||
private int exitCode;
|
||||
|
||||
private AbstractLogConsumer logConsumer;
|
||||
|
||||
@Nullable
|
||||
private T details;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public TaskRunnerResult(int exitCode, AbstractLogConsumer logConsumer) {
|
||||
this.exitCode = exitCode;
|
||||
this.logConsumer = logConsumer;
|
||||
this.details = (T) TaskRunnerDetailResult.builder().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.kestra.core.repositories.ExecutionRepositoryInterface;
|
||||
import io.kestra.core.services.ExecutionService;
|
||||
import io.kestra.core.storages.Storage;
|
||||
import io.kestra.core.trace.TracerFactory;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.utils.MapUtils;
|
||||
import io.kestra.core.trace.propagation.ExecutionTextMapSetter;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
@@ -153,7 +154,7 @@ public final class ExecutableUtils {
|
||||
throw new IllegalStateException("Cannot execute an invalid flow: " + fwe.getException());
|
||||
}
|
||||
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(currentExecution.getLabels()) : new ArrayList<>(systemLabels(currentExecution));
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
|
||||
if (labels != null) {
|
||||
labels.forEach(throwConsumer(label -> newLabels.add(new Label(runContext.render(label.key()), runContext.render(label.value())))));
|
||||
}
|
||||
@@ -201,6 +202,16 @@ public final class ExecutableUtils {
|
||||
}));
|
||||
}
|
||||
|
||||
private static List<Label> filterLabels(List<Label> labels, Flow flow) {
|
||||
if (ListUtils.isEmpty(flow.getLabels())) {
|
||||
return labels;
|
||||
}
|
||||
|
||||
return labels.stream()
|
||||
.filter(label -> flow.getLabels().stream().noneMatch(flowLabel -> flowLabel.key().equals(label.key())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<Label> systemLabels(Execution execution) {
|
||||
return Streams.of(execution.getLabels())
|
||||
.filter(label -> label.key().startsWith(Label.SYSTEM_PREFIX))
|
||||
|
||||
@@ -172,13 +172,15 @@ public class FlowService {
|
||||
subFlows.forEach(subflow -> {
|
||||
Optional<Flow> optional = findById(flow.getTenantId(), subflow.getNamespace(), subflow.getFlowId());
|
||||
|
||||
violations.add(ManualConstraintViolation.of(
|
||||
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
|
||||
flow,
|
||||
Flow.class,
|
||||
"flow.tasks",
|
||||
flow.getNamespace()
|
||||
));
|
||||
if (optional.isEmpty()) {
|
||||
violations.add(ManualConstraintViolation.of(
|
||||
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
|
||||
flow,
|
||||
Flow.class,
|
||||
"flow.tasks",
|
||||
flow.getNamespace()
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
|
||||
@@ -37,8 +37,8 @@ import static io.kestra.core.utils.Rethrow.throwPredicate;
|
||||
"- conditions:",
|
||||
" - type: io.kestra.plugin.core.condition.Not",
|
||||
" conditions:",
|
||||
" - type: io.kestra.plugin.core.condition.DateBetween",
|
||||
" after: \"2013-09-08T16:19:12\"",
|
||||
" - type: io.kestra.plugin.core.condition.DateTimeBetween",
|
||||
" after: \"2013-09-08T16:19:12Z\"",
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@ public class PurgeLogs extends Task implements RunnableTask<PurgeLogs.Output> {
|
||||
flowService.checkAllowedNamespace(flowInfo.tenantId(), runContext.render(namespace).as(String.class).orElse(null), flowInfo.tenantId(), flowInfo.namespace());
|
||||
}
|
||||
|
||||
var logLevelsRendered = runContext.render(this.logLevels).asList(String.class);
|
||||
var logLevelsRendered = runContext.render(this.logLevels).asList(Level.class);
|
||||
var renderedDate = runContext.render(startDate).as(String.class).orElse(null);
|
||||
int deleted = logService.purge(
|
||||
flowInfo.tenantId(),
|
||||
|
||||
@@ -149,7 +149,7 @@ class ClassPluginDocumentationTest {
|
||||
assertThat(oneOf.getFirst().get("type"), is("integer"));
|
||||
assertThat(oneOf.getFirst().get("$dynamic"), is(true));
|
||||
assertThat(oneOf.get(1).get("type"), is("string"));
|
||||
assertThat(oneOf.get(1).get("pattern"), is(".*{{.*}}.*"));
|
||||
// assertThat(oneOf.get(1).get("pattern"), is(".*{{.*}}.*"));
|
||||
|
||||
Map<String, Object> withDefault = (Map<String, Object>) properties.get("withDefault");
|
||||
assertThat(withDefault.get("type"), is("string"));
|
||||
|
||||
@@ -22,6 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KestraTest
|
||||
@@ -323,7 +324,7 @@ class FlowServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkValidSubflowsNotFound() {
|
||||
void checkSubflowNotFound() {
|
||||
Flow flow = create("mainFlow", "task", 1).toBuilder()
|
||||
.tasks(List.of(
|
||||
io.kestra.plugin.core.flow.Subflow.builder()
|
||||
@@ -342,4 +343,23 @@ class FlowServiceTest {
|
||||
assertThat(exception.getConstraintViolations().size(), is(1));
|
||||
assertThat(exception.getConstraintViolations().iterator().next().getMessage(), is("The subflow 'nonExistentSubflow' not found in namespace 'io.kestra.unittest'."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkValidSubflow() {
|
||||
Flow subflow = create("existingSubflow", "task", 1);
|
||||
flowRepository.create(subflow, subflow.generateSource(), subflow);
|
||||
|
||||
Flow flow = create("mainFlow", "task", 1).toBuilder()
|
||||
.tasks(List.of(
|
||||
io.kestra.plugin.core.flow.Subflow.builder()
|
||||
.id("subflowTask")
|
||||
.type(io.kestra.plugin.core.flow.Subflow.class.getName())
|
||||
.namespace("io.kestra.unittest")
|
||||
.flowId("existingSubflow")
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
|
||||
assertDoesNotThrow(() -> flowService.checkValidSubflows(flow));
|
||||
}
|
||||
}
|
||||
@@ -99,21 +99,24 @@ public class FlowCaseTest {
|
||||
assertThat(triggered.get().getState().getCurrent(), is(triggerState));
|
||||
|
||||
if (testInherited) {
|
||||
assertThat(triggered.get().getLabels().size(), is(5));
|
||||
assertThat(triggered.get().getLabels().size(), is(6));
|
||||
assertThat(triggered.get().getLabels(), hasItems(
|
||||
new Label(Label.CORRELATION_ID, execution.getId()),
|
||||
new Label("mainFlowExecutionLabel", "execFoo"),
|
||||
new Label("mainFlowLabel", "flowFoo"),
|
||||
new Label("launchTaskLabel", "launchFoo"),
|
||||
new Label("switchFlowLabel", "switchFoo")
|
||||
new Label("switchFlowLabel", "switchFoo"),
|
||||
new Label("overriding", "child")
|
||||
));
|
||||
} else {
|
||||
assertThat(triggered.get().getLabels().size(), is(3));
|
||||
assertThat(triggered.get().getLabels().size(), is(4));
|
||||
assertThat(triggered.get().getLabels(), hasItems(
|
||||
new Label(Label.CORRELATION_ID, execution.getId()),
|
||||
new Label("launchTaskLabel", "launchFoo"),
|
||||
new Label("switchFlowLabel", "switchFoo")
|
||||
new Label("switchFlowLabel", "switchFoo"),
|
||||
new Label("overriding", "child")
|
||||
));
|
||||
assertThat(triggered.get().getLabels(), not(hasItems(new Label("inherited", "label"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ public class ForEachItemCaseTest {
|
||||
}
|
||||
|
||||
public void restartForEachItem() throws Exception {
|
||||
CountDownLatch countDownLatch = new CountDownLatch(26);
|
||||
CountDownLatch countDownLatch = new CountDownLatch(6);
|
||||
Flux<Execution> receiveSubflows = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution subflowExecution = either.getLeft();
|
||||
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isFailed()) {
|
||||
@@ -285,7 +285,7 @@ public class ForEachItemCaseTest {
|
||||
});
|
||||
|
||||
URI file = storageUpload();
|
||||
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 4);
|
||||
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 20);
|
||||
Execution execution = runnerUtils.runOne(null, TEST_NAMESPACE, "restart-for-each-item", null,
|
||||
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
|
||||
Duration.ofSeconds(30));
|
||||
@@ -296,7 +296,7 @@ public class ForEachItemCaseTest {
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.MINUTES));
|
||||
receiveSubflows.blockLast();
|
||||
|
||||
CountDownLatch successLatch = new CountDownLatch(26);
|
||||
CountDownLatch successLatch = new CountDownLatch(6);
|
||||
receiveSubflows = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution subflowExecution = either.getLeft();
|
||||
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isSuccess()) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -43,7 +43,7 @@ class TimeoutTest {
|
||||
@Inject
|
||||
private RunnerUtils runnerUtils;
|
||||
|
||||
@Test
|
||||
@RetryingTest(5) // Flaky on CI but never locally even with 100 repetitions
|
||||
void timeout() throws TimeoutException, QueueException {
|
||||
List<LogEntry> logs = new CopyOnWriteArrayList<>();
|
||||
Flux<LogEntry> receive = TestsUtils.receive(workerTaskLogQueue, either -> logs.add(either.getLeft()));
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
package io.kestra.plugin.core.log;
|
||||
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.repositories.LogRepositoryInterface;
|
||||
import io.kestra.core.runners.RunContextFactory;
|
||||
import io.kestra.core.runners.RunnerUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KestraTest
|
||||
@KestraTest(startRunner = true)
|
||||
class PurgeLogsTest {
|
||||
@Inject
|
||||
private RunContextFactory runContextFactory;
|
||||
@@ -25,8 +30,12 @@ class PurgeLogsTest {
|
||||
@Inject
|
||||
private LogRepositoryInterface logRepository;
|
||||
|
||||
@Inject
|
||||
protected RunnerUtils runnerUtils;
|
||||
|
||||
@Test
|
||||
void run() throws Exception {
|
||||
@LoadFlows("flows/valids/purge_logs_no_arguments.yaml")
|
||||
void run_with_no_arguments() throws Exception {
|
||||
// create an execution to delete
|
||||
var logEntry = LogEntry.builder()
|
||||
.namespace("namespace")
|
||||
@@ -37,12 +46,71 @@ class PurgeLogsTest {
|
||||
.build();
|
||||
logRepository.save(logEntry);
|
||||
|
||||
var purge = PurgeLogs.builder()
|
||||
.endDate(Property.of(ZonedDateTime.now().plusMinutes(1).format(DateTimeFormatter.ISO_ZONED_DATE_TIME)))
|
||||
.build();
|
||||
var runContext = runContextFactory.of(Map.of("flow", Map.of("namespace", "namespace", "id", "flowId")));
|
||||
var output = purge.run(runContext);
|
||||
Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "purge_logs_no_arguments");
|
||||
|
||||
assertThat(output.getCount(), is(1));
|
||||
assertTrue(execution.getState().isSuccess());
|
||||
assertThat(execution.getTaskRunList().size(), is(1));
|
||||
assertThat(execution.getTaskRunList().getFirst().getOutputs().get("count"), is(1));
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("buildArguments")
|
||||
@LoadFlows("flows/valids/purge_logs_full_arguments.yaml")
|
||||
void run_with_full_arguments(LogEntry logEntry, int resultCount, String failingReason) throws Exception {
|
||||
logRepository.save(logEntry);
|
||||
|
||||
Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "purge_logs_full_arguments");
|
||||
|
||||
assertTrue(execution.getState().isSuccess());
|
||||
assertThat(execution.getTaskRunList().size(), is(1));
|
||||
assertThat(failingReason, execution.getTaskRunList().getFirst().getOutputs().get("count"), is(resultCount));
|
||||
}
|
||||
|
||||
static Stream<Arguments> buildArguments() {
|
||||
return Stream.of(
|
||||
Arguments.of(LogEntry.builder()
|
||||
.namespace("purge.namespace")
|
||||
.flowId("purgeFlowId")
|
||||
.timestamp(Instant.now().plus(5, ChronoUnit.HOURS))
|
||||
.level(Level.INFO)
|
||||
.message("Hello World")
|
||||
.build(), 0, "The log is too recent to be found"),
|
||||
Arguments.of(LogEntry.builder()
|
||||
.namespace("purge.namespace")
|
||||
.flowId("purgeFlowId")
|
||||
.timestamp(Instant.now().minus(5, ChronoUnit.HOURS))
|
||||
.level(Level.INFO)
|
||||
.message("Hello World")
|
||||
.build(), 0, "The log is too old to be found"),
|
||||
Arguments.of(LogEntry.builder()
|
||||
.namespace("uncorrect.namespace")
|
||||
.flowId("purgeFlowId")
|
||||
.timestamp(Instant.now().minusSeconds(10))
|
||||
.level(Level.INFO)
|
||||
.message("Hello World")
|
||||
.build(), 0, "The log has an incorrect namespace"),
|
||||
Arguments.of(LogEntry.builder()
|
||||
.namespace("purge.namespace")
|
||||
.flowId("wrongFlowId")
|
||||
.timestamp(Instant.now().minusSeconds(10))
|
||||
.level(Level.INFO)
|
||||
.message("Hello World")
|
||||
.build(), 0, "The log has an incorrect flow id"),
|
||||
Arguments.of(LogEntry.builder()
|
||||
.namespace("purge.namespace")
|
||||
.flowId("purgeFlowId")
|
||||
.timestamp(Instant.now().minusSeconds(10))
|
||||
.level(Level.WARN)
|
||||
.message("Hello World")
|
||||
.build(), 0, "The log has an incorrect LogLevel"),
|
||||
Arguments.of(LogEntry.builder()
|
||||
.namespace("purge.namespace")
|
||||
.flowId("purgeFlowId")
|
||||
.timestamp(Instant.now().minusSeconds(10))
|
||||
.level(Level.INFO)
|
||||
.message("Hello World")
|
||||
.build(), 1, "The log should be deleted")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
id: purge_logs_full_arguments
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
- id: purge_logs
|
||||
type: io.kestra.plugin.core.log.PurgeLogs
|
||||
endDate: "{{ now() | dateAdd(2, 'HOURS') }}"
|
||||
startDate: "{{ now() | dateAdd(-2, 'HOURS') }}"
|
||||
namespace: purge.namespace
|
||||
flowId: purgeFlowId
|
||||
logLevels:
|
||||
- INFO
|
||||
- ERROR
|
||||
@@ -0,0 +1,7 @@
|
||||
id: purge_logs_no_arguments
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
- id: purge_logs
|
||||
type: io.kestra.plugin.core.log.PurgeLogs
|
||||
endDate: "{{ now() | dateAdd(2, 'HOURS') }}"
|
||||
@@ -10,6 +10,7 @@ inputs:
|
||||
|
||||
labels:
|
||||
switchFlowLabel: switchFoo
|
||||
overriding: child
|
||||
|
||||
tasks:
|
||||
- id: parent-seq
|
||||
|
||||
@@ -7,6 +7,7 @@ inputs:
|
||||
|
||||
labels:
|
||||
mainFlowLabel: flowFoo
|
||||
overriding: parent
|
||||
|
||||
tasks:
|
||||
- id: launch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version=0.21.0-rc0-SNAPSHOT
|
||||
version=0.21.1
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.priority=low
|
||||
org.gradle.priority=low
|
||||
|
||||
@@ -4,8 +4,6 @@ import io.kestra.core.models.annotations.Plugin.Id;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -55,6 +53,18 @@ public interface Plugin {
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method to check whether a given plugin is deprecated.
|
||||
*
|
||||
* @param plugin The plugin type.
|
||||
* @return {@code true} if the plugin is deprecated.
|
||||
*/
|
||||
static boolean isDeprecated(final Class<?> plugin) {
|
||||
Objects.requireNonNull(plugin, "Cannot check if a plugin is deprecated from null");
|
||||
Deprecated annotation = plugin.getAnnotation(Deprecated.class);
|
||||
return annotation != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method to get the id of a plugin.
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ javaPlatform {
|
||||
dependencies {
|
||||
// versions for libraries with multiple module but no BOM
|
||||
def slf4jVersion = "2.0.16"
|
||||
def protobufVersion = "4.29.3"
|
||||
def protobufVersion = "3.25.5" // Orc still uses 3.25.5 see https://github.com/apache/orc/blob/main/java/pom.xml
|
||||
def bouncycastleVersion = "1.80"
|
||||
def aetherVersion = "1.1.0"
|
||||
def jollydayVersion = "0.32.0"
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT
|
||||
# To release a specific plugin:
|
||||
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT plugin-kubernetes
|
||||
|
||||
# To release specific plugins from file:
|
||||
# ./release-plugins.sh --release-version=0.20.0 --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
@@ -43,6 +44,7 @@ usage() {
|
||||
echo "Options:"
|
||||
echo " --release-version <version> Specify the release version (required)."
|
||||
echo " --next-version <version> Specify the next version (required)."
|
||||
echo " --plugin-file File containing the plugin list (default: .plugins)"
|
||||
echo " --dry-run Specify to run in DRY_RUN."
|
||||
echo " -y, --yes Automatically confirm prompts (non-interactive)."
|
||||
echo " -h, --help Show this help message and exit."
|
||||
@@ -81,6 +83,14 @@ while [[ "$#" -gt 0 ]]; do
|
||||
NEXT_VERSION="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--plugin-file)
|
||||
PLUGIN_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--plugin-file=*)
|
||||
PLUGIN_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
|
||||
@@ -143,7 +143,7 @@ public abstract class AbstractExecScript extends Task implements RunnableTask<Sc
|
||||
* protected DockerOptions docker = DockerOptions.builder().build();
|
||||
* }</pre>
|
||||
*/
|
||||
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) {
|
||||
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) throws IllegalVariableEvaluationException {
|
||||
// FIXME to keep backward compatibility, we call the old method from the new one by default
|
||||
return injectDefaults(original);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0
|
||||
# To release a specific plugin:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0 plugin-kubernetes
|
||||
|
||||
# To release specific plugins from file:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0 --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
@@ -40,6 +41,7 @@ usage() {
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " --release-version <version> Specify the release version (required)."
|
||||
echo " --plugin-file File containing the plugin list (default: .plugins)"
|
||||
echo " --dry-run Specify to run in DRY_RUN."
|
||||
echo " -y, --yes Automatically confirm prompts (non-interactive)."
|
||||
echo " -h, --help Show this help message and exit."
|
||||
@@ -70,6 +72,14 @@ while [[ "$#" -gt 0 ]]; do
|
||||
RELEASE_VERSION="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--plugin-file)
|
||||
PLUGIN_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--plugin-file=*)
|
||||
PLUGIN_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
@@ -163,7 +173,7 @@ do
|
||||
git checkout "$RELEASE_BRANCH";
|
||||
|
||||
# Update version
|
||||
sed -i.bak "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
||||
sed -i "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
||||
git add ./gradle.properties
|
||||
git commit -m"chore(version): update to version 'v$RELEASE_VERSION'."
|
||||
git push
|
||||
|
||||
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@js-joda/core": "^5.6.3",
|
||||
"@kestra-io/ui-libs": "^0.0.119",
|
||||
"@kestra-io/ui-libs": "^0.0.129",
|
||||
"@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.129",
|
||||
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.129.tgz",
|
||||
"integrity": "sha512-SacgVN8GeRfhBeq1K76/1xdc1ZwXW4lOzlKdpV0C2xAzGDkvhORzCyRHJF7vQX+i9OCD73N90Zvu5/UZpzfj7Q==",
|
||||
"dependencies": {
|
||||
"@nuxtjs/mdc": "^0.12.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@js-joda/core": "^5.6.3",
|
||||
"@kestra-io/ui-libs": "^0.0.119",
|
||||
"@kestra-io/ui-libs": "^0.0.129",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
"@vue-flow/core": "^1.42.1",
|
||||
|
||||
@@ -105,7 +105,10 @@
|
||||
},
|
||||
methods: {
|
||||
displayApp() {
|
||||
Utils.switchTheme();
|
||||
console.log("App is loaded");
|
||||
Utils.switchTheme(this.$store);
|
||||
|
||||
console.log(this.$store.getters["misc/theme"]);
|
||||
|
||||
document.getElementById("loader-wrapper").style.display = "none";
|
||||
document.getElementById("app-container").style.display = "block";
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
BIN
ui/src/assets/empty-ns-files.png
Normal file
BIN
ui/src/assets/empty-ns-files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
@@ -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.
|
||||
|
||||
|
||||
BIN
ui/src/assets/no_data.png
Normal file
BIN
ui/src/assets/no_data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -171,7 +171,7 @@
|
||||
|
||||
const onSwitchTheme = () => {
|
||||
themeIsDark.value = !themeIsDark.value;
|
||||
Utils.switchTheme(themeIsDark.value ? "dark" : "light");
|
||||
Utils.switchTheme(store, themeIsDark.value ? "dark" : "light");
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
29
ui/src/components/EnterpriseBadge.vue
Normal file
29
ui/src/components/EnterpriseBadge.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<span>
|
||||
<slot />
|
||||
<LockIcon v-if="enable" class="lock-ee" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LockIcon from "vue-material-design-icons/LockOutline.vue";
|
||||
defineProps({
|
||||
enable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.lock-ee {
|
||||
margin-left:.5rem;
|
||||
opacity:.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<el-tooltip
|
||||
data-component="FILENAME_PLACEHOLDER"
|
||||
:visible="visible"
|
||||
:persistent="false"
|
||||
:focus-on-show="true"
|
||||
popper-class="ee-tooltip"
|
||||
:disabled="!disabled"
|
||||
:placement="placement"
|
||||
>
|
||||
<template #content v-if="link">
|
||||
<el-button circle class="ee-tooltip-close" @click="changeVisibility(false)">
|
||||
<Close />
|
||||
</el-button>
|
||||
|
||||
<p>{{ $t("ee-tooltip.features-blocked") }}</p>
|
||||
|
||||
<a
|
||||
class="el-button el-button--primary d-block"
|
||||
type="primary"
|
||||
:href="link"
|
||||
target="_blank"
|
||||
>
|
||||
Talk to us
|
||||
</a>
|
||||
</template>
|
||||
<template #default>
|
||||
<span ref="slot-container" class="cursor-pointer" @click="changeVisibility()">
|
||||
<slot />
|
||||
<lock v-if="disabled" />
|
||||
</span>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Close from "vue-material-design-icons/Close.vue";
|
||||
import Lock from "vue-material-design-icons/Lock.vue";
|
||||
|
||||
export default {
|
||||
components: {Close, Lock},
|
||||
props: {
|
||||
top: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
term: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeVisibility(visible = true) {
|
||||
if (visible) document.querySelector(".ee-tooltip")?.remove();
|
||||
this.visible = visible
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
link() {
|
||||
|
||||
let link = "https://kestra.io/demo?utm_source=app&utm_campaign=ee-tooltip";
|
||||
|
||||
if (this.term) {
|
||||
link = link + "&utm_term=" + this.term;
|
||||
}
|
||||
|
||||
if (this.content) {
|
||||
link = link + "&utm_content=" + this.content;
|
||||
}
|
||||
|
||||
return link;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:global(.el-popper.ee-tooltip) {
|
||||
max-width: 320px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-lg);
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.material-design-icon) > .material-design-icon__svg {
|
||||
bottom: -0.125em;
|
||||
}
|
||||
|
||||
.ee-tooltip-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
<template>
|
||||
<div v-if="isLocked" v-bind="$attrs">
|
||||
<span ref="slotContainer" class="d-none">
|
||||
<slot />
|
||||
</span>
|
||||
<enterprise-tooltip v-if="term" :disabled="true" :term="term" content="left-menu">
|
||||
<slot />
|
||||
</enterprise-tooltip>
|
||||
</div>
|
||||
<a v-else-if="isHyperLink" v-bind="$attrs">
|
||||
<a v-if="isHyperLink" v-bind="$attrs">
|
||||
<slot />
|
||||
</a>
|
||||
<router-link v-else :to="$attrs.href" custom v-slot="{href:linkHref, navigate}">
|
||||
<a v-bind="$attrs" :href="linkHref" @click="navigate">
|
||||
<slot />
|
||||
<enterprise-badge :enable="isLocked">
|
||||
<slot />
|
||||
</enterprise-badge>
|
||||
</a>
|
||||
</router-link>
|
||||
</template>
|
||||
@@ -20,7 +14,7 @@
|
||||
<script setup>
|
||||
import {computed, ref, onMounted} from "vue"
|
||||
import {useRouter} from "vue-router";
|
||||
import EnterpriseTooltip from "./EnterpriseTooltip.vue";
|
||||
import EnterpriseBadge from "./EnterpriseBadge.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "LeftMenuLink",
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
<el-tooltip v-if="tab.disabled && tab.props && tab.props.showTooltip" :content="$t('add-trigger-in-editor')" placement="top">
|
||||
<span><strong>{{ tab.title }}</strong></span>
|
||||
</el-tooltip>
|
||||
<span v-if="!tab.hideTitle">
|
||||
<enterprise-tooltip :disabled="tab.locked" :term="tab.name" content="tabs">
|
||||
{{ tab.title }}
|
||||
<el-badge :type="tab.count > 0 ? 'danger' : 'primary'" :value="tab.count" v-if="tab.count !== undefined" />
|
||||
</enterprise-tooltip>
|
||||
</span>
|
||||
<enterprise-badge :enable="tab.locked">
|
||||
{{ tab.title }}
|
||||
<el-badge :type="tab.count > 0 ? 'danger' : 'primary'" :value="tab.count" v-if="tab.count !== undefined" />
|
||||
</enterprise-badge>
|
||||
</component>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
@@ -51,10 +49,10 @@
|
||||
import {mapState, mapMutations} from "vuex";
|
||||
|
||||
import EditorSidebar from "./inputs/EditorSidebar.vue";
|
||||
import EnterpriseTooltip from "./EnterpriseTooltip.vue";
|
||||
import EnterpriseBadge from "./EnterpriseBadge.vue";
|
||||
|
||||
export default {
|
||||
components: {EditorSidebar, EnterpriseTooltip},
|
||||
components: {EditorSidebar, EnterpriseBadge},
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:metadata
|
||||
@update-metadata="(k, v) => emits('updateMetadata', {[k]: v})"
|
||||
@update-task="(yaml) => emits('updateTask', yaml)"
|
||||
@reorder="(yaml) => emits('reorder', yaml)"
|
||||
@update-documentation="(task) => emits('updateDocumentation', task)"
|
||||
/>
|
||||
</div>
|
||||
@@ -30,6 +31,7 @@
|
||||
"updateTask",
|
||||
"updateMetadata",
|
||||
"updateDocumentation",
|
||||
"reorder",
|
||||
]);
|
||||
const props = defineProps({
|
||||
flow: {type: String, required: true},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class="item"
|
||||
@click="
|
||||
(store.commit('code/removeBreadcrumb', {position: index}),
|
||||
store.commit('code/unsetPanel'))
|
||||
store.commit('code/unsetPanel', false))
|
||||
"
|
||||
>
|
||||
<router-link :to="breadcrumb.to">
|
||||
|
||||
@@ -11,15 +11,22 @@
|
||||
<Creation :section="item.title" />
|
||||
</template>
|
||||
|
||||
<template v-if="creation">
|
||||
<Element
|
||||
v-for="(element, elementIndex) in item.elements"
|
||||
:key="elementIndex"
|
||||
:section="item.title"
|
||||
:element
|
||||
@remove-element="removeElement(item.title, elementIndex)"
|
||||
/>
|
||||
</template>
|
||||
<Element
|
||||
v-for="(element, elementIndex) in item.elements"
|
||||
:key="elementIndex"
|
||||
:section="item.title"
|
||||
:element
|
||||
@remove-element="removeElement(item.title, elementIndex)"
|
||||
@move-element="
|
||||
(direction: 'up' | 'down') =>
|
||||
moveElement(
|
||||
item.elements,
|
||||
element.id,
|
||||
elementIndex,
|
||||
direction,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
@@ -32,7 +39,7 @@
|
||||
import Creation from "./buttons/Creation.vue";
|
||||
import Element from "./Element.vue";
|
||||
|
||||
const emits = defineEmits(["remove"]);
|
||||
const emits = defineEmits(["remove", "reorder"]);
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
@@ -67,6 +74,27 @@
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
import {YamlUtils as YAML_FROM_UI_LIBS} from "@kestra-io/ui-libs";
|
||||
const moveElement = (
|
||||
items: Record<string, any>[] | undefined,
|
||||
elementID: string,
|
||||
index: number,
|
||||
direction: "up" | "down",
|
||||
) => {
|
||||
if (!items || !props.flow) return;
|
||||
if (
|
||||
(direction === "up" && index === 0) ||
|
||||
(direction === "down" && index === items.length - 1)
|
||||
)
|
||||
return;
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
emits(
|
||||
"reorder",
|
||||
YAML_FROM_UI_LIBS.swapTasks(props.flow, elementID, items[newIndex].id),
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -14,17 +14,21 @@
|
||||
size="small"
|
||||
class="border-0"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<ChevronUp @click.prevent.stop="emits('moveElement', 'up')" />
|
||||
<ChevronDown @click.prevent.stop="emits('moveElement', 'down')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
|
||||
import {DeleteOutline} from "../../utils/icons";
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../utils/icons";
|
||||
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
|
||||
const emits = defineEmits(["removeElement"]);
|
||||
const emits = defineEmits(["removeElement", "moveElement"]);
|
||||
|
||||
const props = defineProps({
|
||||
section: {type: String, required: true},
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
<span v-if="required" class="me-1 text-danger">*</span>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
<div class="mt-1 mb-2 wrapper" :class="props.class">
|
||||
<el-input v-model="input" @input="handleInput" :placeholder :disabled />
|
||||
<el-input
|
||||
v-model="input"
|
||||
@input="handleInput"
|
||||
:placeholder
|
||||
:disabled
|
||||
type="textarea"
|
||||
:autosize="{minRows: 1}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from "vue";
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({
|
||||
modelValue: {type: [String, Number, Boolean], default: undefined},
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
creation
|
||||
:flow
|
||||
@remove="(yaml) => emits('updateTask', yaml)"
|
||||
@reorder="(yaml) => emits('reorder', yaml)"
|
||||
/>
|
||||
|
||||
<hr class="my-4">
|
||||
@@ -96,6 +97,7 @@
|
||||
"updateTask",
|
||||
"updateMetadata",
|
||||
"updateDocumentation",
|
||||
"reorder",
|
||||
]);
|
||||
|
||||
const saveEvent = (e: KeyboardEvent) => {
|
||||
@@ -235,6 +237,7 @@
|
||||
"error_handlers",
|
||||
YamlUtils.parse(props.flow).errors ?? [],
|
||||
),
|
||||
getSectionTitle("finally", YamlUtils.parse(props.flow).finally ?? []),
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
v-else
|
||||
:is="lastBreadcumb.component.type"
|
||||
v-bind="lastBreadcumb.component.props"
|
||||
v-on="lastBreadcumb.component.listeners"
|
||||
:model-value="lastBreadcumb.component.props.modelValue"
|
||||
@update:model-value="validateTask"
|
||||
/>
|
||||
@@ -126,39 +125,51 @@
|
||||
YamlUtils.parse(yaml.value).id,
|
||||
);
|
||||
|
||||
if (route.query.section === SECTIONS.TRIGGERS.toLowerCase()) {
|
||||
const existingTask = YamlUtils.checkTaskAlreadyExist(
|
||||
source,
|
||||
CURRENT.value,
|
||||
);
|
||||
if (existingTask) {
|
||||
store.dispatch("core/showMessage", {
|
||||
variant: "error",
|
||||
title: "Trigger Id already exist",
|
||||
message: `Trigger Id ${existingTask} already exist in the flow.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const currentSection = route.query.section;
|
||||
const isCreation =
|
||||
props.creation &&
|
||||
(!route.query.identifier || route.query.identifier === "new");
|
||||
|
||||
emits("updateTask", YamlUtils.insertTrigger(source, CURRENT.value));
|
||||
CURRENT.value = null;
|
||||
} else {
|
||||
const action = props.creation
|
||||
? YamlUtils.insertTask(
|
||||
let result;
|
||||
|
||||
if (isCreation) {
|
||||
if (currentSection === "tasks") {
|
||||
const existing = YamlUtils.checkTaskAlreadyExist(
|
||||
source,
|
||||
YamlUtils.getLastTask(source),
|
||||
task,
|
||||
"after",
|
||||
)
|
||||
: YamlUtils.replaceTaskInDocument(
|
||||
source,
|
||||
route.query.identifier,
|
||||
task,
|
||||
CURRENT.value,
|
||||
);
|
||||
|
||||
emits("updateTask", action);
|
||||
if (existing) {
|
||||
store.dispatch("core/showMessage", {
|
||||
variant: "error",
|
||||
title: "Task with same ID already exist",
|
||||
message: `Task in ${route.query.section} block with ID: ${existing} already exist in the flow.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
result = YamlUtils.insertTask(
|
||||
source,
|
||||
route.query.target ?? YamlUtils.getLastTask(source),
|
||||
task,
|
||||
route.query.position ?? "after",
|
||||
);
|
||||
} else if (currentSection === "triggers") {
|
||||
result = YamlUtils.insertTrigger(source, CURRENT.value);
|
||||
} else if (currentSection === "error handlers") {
|
||||
result = YamlUtils.insertError(source, CURRENT.value);
|
||||
} else if (currentSection === "finally") {
|
||||
result = YamlUtils.insertFinally(source, CURRENT.value);
|
||||
}
|
||||
} else {
|
||||
result = YamlUtils.replaceTaskInDocument(
|
||||
source,
|
||||
route.query.identifier,
|
||||
task,
|
||||
);
|
||||
}
|
||||
|
||||
emits("updateTask", result);
|
||||
store.commit("code/removeBreadcrumb", {last: true});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -53,7 +53,8 @@ $code-font-sm: var(--el-font-size-small);
|
||||
font-size: $code-font-sm;
|
||||
}
|
||||
|
||||
.delete {
|
||||
.delete,
|
||||
.reorder {
|
||||
cursor: pointer;
|
||||
padding-left: 0;
|
||||
color: $code-gray-700;
|
||||
@@ -64,7 +65,8 @@ $code-font-sm: var(--el-font-size-small);
|
||||
:deep(*) {
|
||||
--el-disabled-text-color: #{$code-gray-700};
|
||||
|
||||
.el-input__inner {
|
||||
.el-input__inner,
|
||||
.el-textarea__inner {
|
||||
color: $code-gray-700;
|
||||
font-size: $code-font-sm;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Plus from "vue-material-design-icons/Plus.vue";
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
import DeleteOutline from "vue-material-design-icons/DeleteOutline.vue";
|
||||
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
||||
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
||||
|
||||
export {Plus, ContentSave, DeleteOutline};
|
||||
export {Plus, ContentSave, DeleteOutline, ChevronUp, ChevronDown};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="state">
|
||||
<span class="circle" :style="{backgroundColor: getScheme(label)}" />
|
||||
<span class="circle" :style="{backgroundColor: scheme[label]}" />
|
||||
|
||||
<p class="m-0 fw-light small">
|
||||
{{ label.toLowerCase().capitalize() }}
|
||||
@@ -9,7 +9,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getScheme} from "../../../utils/scheme.js";
|
||||
import {useScheme} from "../../../utils/scheme.js";
|
||||
|
||||
const scheme = useScheme();
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import {Bar} from "vue-chartjs";
|
||||
|
||||
import {customBarLegend} from "../legend.js";
|
||||
import {useTheme} from "../../../../../utils/utils.js";
|
||||
import {defaultConfig, getConsistentHEXColor,} from "../../../../../utils/charts.js";
|
||||
|
||||
import {useStore} from "vuex";
|
||||
@@ -52,6 +53,8 @@
|
||||
|
||||
const aggregator = Object.entries(data.columns).filter(([_, v]) => v.agg);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const options = computed(() => {
|
||||
return defaultConfig({
|
||||
skipNull: true,
|
||||
@@ -102,7 +105,7 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}, theme.value);
|
||||
});
|
||||
|
||||
function isDurationAgg() {
|
||||
@@ -142,7 +145,7 @@
|
||||
return Object.entries(grouped[xLabel]).map(subSectionsEntry => ({
|
||||
label: subSectionsEntry[0],
|
||||
data: xLabels.map(label => xLabel === label ? subSectionsEntry[1] : 0),
|
||||
backgroundColor: getConsistentHEXColor(subSectionsEntry[0]),
|
||||
backgroundColor: getConsistentHEXColor(theme.value, subSectionsEntry[0]),
|
||||
tooltip: `(${subSectionsEntry[0]}): ${aggregator[0][0]} = ${(isDurationAgg() ? Utils.humanDuration(subSectionsEntry[1]) : subSectionsEntry[1])}`,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
|
||||
import NoData from "../../../../layout/NoData.vue";
|
||||
import Utils from "../../../../../utils/utils.js";
|
||||
import Utils, {useTheme} from "../../../../../utils/utils.js";
|
||||
|
||||
import {Doughnut, Pie} from "vue-chartjs";
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
|
||||
const isDuration = Object.values(props.chart.data.columns).find(c => c.agg !== undefined).field === "DURATION";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const options = computed(() => {
|
||||
return defaultConfig({
|
||||
plugins: {
|
||||
@@ -77,13 +79,13 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}, theme.value);
|
||||
});
|
||||
|
||||
const centerPlugin = {
|
||||
const centerPlugin = computed(() => ({
|
||||
id: "centerPlugin",
|
||||
beforeDraw(chart) {
|
||||
const darkTheme = Utils.getTheme() === "dark";
|
||||
const darkTheme = theme.value === "dark";
|
||||
|
||||
const ctx = chart.ctx;
|
||||
const dataset = chart.data.datasets[0];
|
||||
@@ -106,7 +108,7 @@
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
const thicknessPlugin = {
|
||||
id: "thicknessPlugin",
|
||||
@@ -157,7 +159,7 @@
|
||||
const labels = Object.keys(results);
|
||||
const dataElements = labels.map((label) => results[label]);
|
||||
|
||||
const backgroundColor = labels.map((label) => getConsistentHEXColor(label));
|
||||
const backgroundColor = labels.map((label) => getConsistentHEXColor(theme.value, label));
|
||||
|
||||
const maxDataValue = Math.max(...dataElements);
|
||||
const thicknessScale = dataElements.map(
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
import {Bar} from "vue-chartjs";
|
||||
|
||||
import {customBarLegend} from "../legend.js";
|
||||
import {defaultConfig, getConsistentHEXColor,} from "../../../../../utils/charts.js";
|
||||
import {defaultConfig, getConsistentHEXColor} from "../../../../../utils/charts.js";
|
||||
|
||||
import {useStore} from "vuex";
|
||||
import moment from "moment";
|
||||
|
||||
import {useRoute} from "vue-router";
|
||||
import {Utils} from "@kestra-io/ui-libs";
|
||||
import KestraUtils, {useTheme} from "../../../../../utils/utils.js"
|
||||
|
||||
const store = useStore();
|
||||
|
||||
@@ -49,6 +50,8 @@
|
||||
.sort((a, b) => a[1].graphStyle.localeCompare(b[1].graphStyle));
|
||||
const yBShown = aggregator.length === 2;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const DEFAULTS = {
|
||||
display: true,
|
||||
stacked: true,
|
||||
@@ -119,7 +122,7 @@
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}, theme.value);
|
||||
});
|
||||
|
||||
function isDuration(field) {
|
||||
@@ -129,7 +132,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;
|
||||
@@ -165,6 +168,7 @@
|
||||
tooltip: stack,
|
||||
label: params[colorByColumn],
|
||||
backgroundColor: getConsistentHEXColor(
|
||||
theme.value,
|
||||
params[colorByColumn],
|
||||
),
|
||||
unique: new Set(),
|
||||
@@ -220,7 +224,7 @@
|
||||
pointRadius: 0,
|
||||
borderWidth: 0.75,
|
||||
label: label,
|
||||
borderColor: getConsistentHEXColor(label),
|
||||
borderColor: getConsistentHEXColor(theme.value, label),
|
||||
},
|
||||
...yDatasetData,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
placement="left"
|
||||
:persistent="false"
|
||||
:hide-after="0"
|
||||
transition=""
|
||||
:popper-class="tooltipContent === '' ? 'd-none' : 'tooltip-stats'"
|
||||
:disabled="!externalTooltip"
|
||||
:content="tooltipContent"
|
||||
raw-content
|
||||
>
|
||||
<div>
|
||||
<Bar
|
||||
:class="small ? 'small' : ''"
|
||||
:data="parsedData"
|
||||
:options="options"
|
||||
:total="total"
|
||||
:plugins="plugins"
|
||||
:duration="duration"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import moment from "moment";
|
||||
import {Bar} from "vue-chartjs";
|
||||
import {useRouter} from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
import Utils, {useTheme} from "../../../../../utils/utils.js";
|
||||
import {useScheme} from "../../../../../utils/scheme.js";
|
||||
import {defaultConfig, tooltip, getFormat} from "../../../../../utils/charts.js";
|
||||
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
plugins: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
duration: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
scales: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
externalTooltip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme()
|
||||
const scheme = useScheme();
|
||||
|
||||
const tooltipContent = ref("")
|
||||
|
||||
const parsedData = computed(() => {
|
||||
let datasets = props.data.reduce(function (accumulator, value) {
|
||||
Object.keys(value.executionCounts).forEach(function (state) {
|
||||
if (accumulator[state] === undefined) {
|
||||
accumulator[state] = {
|
||||
label: state,
|
||||
backgroundColor: scheme.value[state],
|
||||
yAxisID: "y",
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
accumulator[state].data.push(value.executionCounts[state]);
|
||||
});
|
||||
|
||||
return accumulator;
|
||||
}, Object.create(null));
|
||||
|
||||
return {
|
||||
labels: props.data.map((r) =>
|
||||
moment(r.startDate).format(getFormat(r.groupBy)),
|
||||
),
|
||||
datasets: props.duration
|
||||
? [
|
||||
{
|
||||
type: "line",
|
||||
label: t("duration"),
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
borderWidth: 0.75,
|
||||
borderColor: "#A2CDFF",
|
||||
yAxisID: "yB",
|
||||
data: props.data.map((value) => {
|
||||
return value.duration.avg === 0
|
||||
? 0
|
||||
: Utils.duration(value.duration.avg);
|
||||
}),
|
||||
},
|
||||
...Object.values(datasets),
|
||||
]
|
||||
: Object.values(datasets),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const options = computed(() =>
|
||||
defaultConfig({
|
||||
barThickness: props.small ? 8 : 12,
|
||||
skipNull: true,
|
||||
borderSkipped: false,
|
||||
borderColor: "transparent",
|
||||
borderWidth: 2,
|
||||
plugins: {
|
||||
barLegend: {
|
||||
containerID: "executions",
|
||||
},
|
||||
tooltip: {
|
||||
enabled: !props.externalTooltip,
|
||||
filter: (value) => value.raw,
|
||||
callbacks: {
|
||||
label: function (value) {
|
||||
const {label, yAxisID} = value.dataset;
|
||||
return `${label.toLowerCase().capitalize()}: ${value.raw}${yAxisID === "yB" ? "s" : ""}`;
|
||||
},
|
||||
},
|
||||
external: props.externalTooltip ? function (context) {
|
||||
let content = tooltip(context.tooltip);
|
||||
tooltipContent.value = content;
|
||||
} : undefined,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: props.scales,
|
||||
title: {
|
||||
display: true,
|
||||
text: t("date"),
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
position: "bottom",
|
||||
stacked: true,
|
||||
ticks: {
|
||||
maxTicksLimit: props.small ? 5 : 8,
|
||||
callback: function (value) {
|
||||
const label = this.getLabelForValue(value);
|
||||
|
||||
if (
|
||||
moment(label, ["h:mm A", "HH:mm"], true).isValid()
|
||||
) {
|
||||
// Handle time strings like "1:15 PM" or "13:15"
|
||||
return moment(label, ["h:mm A", "HH:mm"]).format(
|
||||
"h:mm A",
|
||||
);
|
||||
} else if (moment(new Date(label)).isValid()) {
|
||||
// Handle date strings
|
||||
const date = moment(new Date(label));
|
||||
const isCurrentYear =
|
||||
date.year() === moment().year();
|
||||
return date.format(
|
||||
isCurrentYear ? "MM/DD" : "MM/DD/YY",
|
||||
);
|
||||
}
|
||||
|
||||
// Return the label as-is if it's neither a valid date nor time
|
||||
return label;
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: props.scales,
|
||||
title: {
|
||||
display: !props.small,
|
||||
text: t("executions"),
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
position: "left",
|
||||
stacked: true,
|
||||
ticks: {
|
||||
maxTicksLimit: props.small ? 5 : 8,
|
||||
},
|
||||
},
|
||||
yB: {
|
||||
title: {
|
||||
display: props.duration && !props.small,
|
||||
text: t("duration"),
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
display: props.duration,
|
||||
position: "right",
|
||||
ticks: {
|
||||
maxTicksLimit: props.small ? 5 : 8,
|
||||
callback: function (value) {
|
||||
return `${this.getLabelForValue(value)}s`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
onClick: (e, elements) => {
|
||||
if (elements.length > 0) {
|
||||
const state = parsedData.value.datasets[elements[0].datasetIndex].label;
|
||||
router.push({
|
||||
name: "executions/list",
|
||||
query: {
|
||||
state: state,
|
||||
scope: "USER",
|
||||
size: 100,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}, theme.value),
|
||||
);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.small{
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -25,14 +25,17 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
import {Doughnut} from "vue-chartjs";
|
||||
|
||||
import {totalsLegend} from "../legend.js";
|
||||
|
||||
import Utils from "../../../../../utils/utils.js";
|
||||
import {useTheme} from "../../../../../utils/utils.js";
|
||||
import {defaultConfig} from "../../../../../utils/charts.js";
|
||||
import {getScheme} from "../../../../../utils/scheme.js";
|
||||
import {useScheme} from "../../../../../utils/scheme.js";
|
||||
|
||||
const router = useRouter();
|
||||
const scheme = useScheme();
|
||||
|
||||
import NoData from "../../../../layout/NoData.vue";
|
||||
|
||||
@@ -49,6 +52,8 @@
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const parsedData = computed(() => {
|
||||
let stateCounts = Object.create(null);
|
||||
|
||||
@@ -64,7 +69,7 @@
|
||||
|
||||
const labels = Object.keys(stateCounts);
|
||||
const data = labels.map((state) => stateCounts[state]);
|
||||
const backgroundColor = labels.map((state) => getScheme(state));
|
||||
const backgroundColor = labels.map((state) => scheme.value[state]);
|
||||
|
||||
const maxDataValue = Math.max(...data);
|
||||
const thicknessScale = data.map(
|
||||
@@ -77,6 +82,8 @@
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
const options = computed(() =>
|
||||
defaultConfig({
|
||||
plugins: {
|
||||
@@ -94,13 +101,28 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
onClick: (e, elements) => {
|
||||
if (elements.length > 0) {
|
||||
const index = elements[0].index;
|
||||
const state = parsedData.value.labels[index];
|
||||
router.push({
|
||||
name: "executions/list",
|
||||
query: {
|
||||
state: state,
|
||||
scope: "USER",
|
||||
size: 100,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}, theme.value),
|
||||
);
|
||||
|
||||
const centerPlugin = {
|
||||
const centerPlugin = computed(() => ({
|
||||
id: "centerPlugin",
|
||||
beforeDraw(chart) {
|
||||
const darkTheme = Utils.getTheme() === "dark";
|
||||
const darkTheme = theme.value === "dark";
|
||||
|
||||
const ctx = chart.ctx;
|
||||
const dataset = chart.data.datasets[0];
|
||||
@@ -118,7 +140,7 @@
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
const thicknessPlugin = {
|
||||
id: "thicknessPlugin",
|
||||
|
||||
@@ -31,13 +31,16 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRouter} from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
import {Bar} from "vue-chartjs";
|
||||
|
||||
import {barLegend} from "../legend.js";
|
||||
|
||||
import {defaultConfig} from "../../../../../utils/charts.js";
|
||||
import {getScheme} from "../../../../../utils/scheme.js";
|
||||
import {useScheme} from "../../../../../utils/scheme.js";
|
||||
import {useTheme} from "../../../../../utils/utils.js";
|
||||
|
||||
import NoData from "../../../../layout/NoData.vue";
|
||||
|
||||
@@ -54,6 +57,9 @@
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const scheme = useScheme()
|
||||
|
||||
const parsedData = computed(() => {
|
||||
const labels = Object.entries(props.data)
|
||||
.sort(([, a], [, b]) => b.total - a.total)
|
||||
@@ -71,7 +77,7 @@
|
||||
executionData[state] = {
|
||||
label: state,
|
||||
data: [],
|
||||
backgroundColor: getScheme(state),
|
||||
backgroundColor: scheme.value[state],
|
||||
stack: state,
|
||||
};
|
||||
}
|
||||
@@ -125,7 +131,6 @@
|
||||
position: "bottom",
|
||||
display: true,
|
||||
stacked: true,
|
||||
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
const namespaceName = this.getLabelForValue(value)
|
||||
@@ -149,7 +154,21 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
onClick: (e, elements) => {
|
||||
if (elements.length > 0) {
|
||||
const state = parsedData.value.datasets[elements[0].datasetIndex].label;
|
||||
router.push({
|
||||
name: "executions/list",
|
||||
query: {
|
||||
state: state,
|
||||
scope: "USER",
|
||||
size: 100,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}, theme.value),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -171,4 +190,4 @@ $height: 200px;
|
||||
color: $gray-300;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -142,7 +142,7 @@ export const customBarLegend = {
|
||||
};
|
||||
|
||||
const boxSpan = document.createElement("span");
|
||||
const color = getConsistentHEXColor(item.text);
|
||||
const color = getConsistentHEXColor(Utils.getTheme(), item.text);
|
||||
boxSpan.style.background = color;
|
||||
boxSpan.style.borderColor = "transparent";
|
||||
boxSpan.style.height = "5px";
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
import {barLegend} from "../legend.js";
|
||||
|
||||
import {defaultConfig, getFormat} from "../../../../../utils/charts.js";
|
||||
import {getScheme} from "../../../../../utils/scheme.js";
|
||||
import {useScheme} from "../../../../../utils/scheme.js";
|
||||
import Logs from "../../../../../utils/logs.js";
|
||||
|
||||
import NoData from "../../../../layout/NoData.vue";
|
||||
import {useTheme} from "../../../../../utils/utils.js";
|
||||
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
@@ -47,13 +48,16 @@
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const scheme = useScheme("logs");
|
||||
|
||||
const parsedData = computed(() => {
|
||||
let datasets = props.data.reduce(function (accumulator, value) {
|
||||
Object.keys(value.counts).forEach(function (state) {
|
||||
if (accumulator[state] === undefined) {
|
||||
accumulator[state] = {
|
||||
label: state,
|
||||
backgroundColor: getScheme(state, "logs"),
|
||||
backgroundColor: scheme.value[state],
|
||||
yAxisID: "y",
|
||||
data: [],
|
||||
};
|
||||
@@ -136,7 +140,7 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}, theme.value),
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -211,14 +211,6 @@ code {
|
||||
.nextscheduled {
|
||||
--el-table-tr-bg-color: var(--ks-background-body) !important;
|
||||
background: var(--ks-background-body);
|
||||
// FIXME: choose variables
|
||||
& a {
|
||||
color: #8e71f7;
|
||||
|
||||
html.dark & {
|
||||
color: #e0e0fc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.next-toggle {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -244,10 +244,7 @@
|
||||
}
|
||||
|
||||
:deep(.doc-alert) {
|
||||
border: 1px solid var(--ks-border-info);
|
||||
border-radius: 4px;
|
||||
color: var(--ks-content-info);
|
||||
background: var(--ks-background-info);
|
||||
padding-bottom: 1px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -159,6 +159,7 @@
|
||||
{
|
||||
name: "auditlogs",
|
||||
title: title("auditlogs"),
|
||||
maximized: true,
|
||||
locked: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -46,6 +46,14 @@
|
||||
refresh: {shown: true, callback: refresh},
|
||||
settings: {shown: true, charts: {shown: true, value: showChart, callback: onShowChartChange}}
|
||||
}"
|
||||
:properties-width="182"
|
||||
:properties="{
|
||||
shown: true,
|
||||
columns: optionalColumns,
|
||||
displayColumns,
|
||||
storageKey: 'executions'
|
||||
}"
|
||||
@update-properties="updateDisplayColumns"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -150,6 +158,7 @@
|
||||
<template #default>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
v-if="displayColumn('id')"
|
||||
sortable="custom"
|
||||
:sort-orders="['ascending', 'descending']"
|
||||
:label="$t('id')"
|
||||
@@ -247,6 +256,7 @@
|
||||
|
||||
<el-table-column
|
||||
prop="flowRevision"
|
||||
v-if="displayColumn('revision')"
|
||||
:label="$t('revision')"
|
||||
class-name="shrink"
|
||||
>
|
||||
@@ -291,7 +301,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="action" class-name="row-action">
|
||||
<el-table-column
|
||||
v-if="displayColumn('action')"
|
||||
column-key="action"
|
||||
class-name="row-action"
|
||||
:label="$t('actions')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<router-link
|
||||
:to="{name: 'executions/update', params: {namespace: scope.row.namespace, flowId: scope.row.flowId, id: scope.row.id}, query: {revision: scope.row.flowRevision}}"
|
||||
@@ -460,59 +475,59 @@
|
||||
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_CHART)),
|
||||
optionalColumns: [
|
||||
{
|
||||
label: "start date",
|
||||
label: this.$t("start date"),
|
||||
prop: "state.startDate",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "end date",
|
||||
label: this.$t("end date"),
|
||||
prop: "state.endDate",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "duration",
|
||||
label: this.$t("duration"),
|
||||
prop: "state.duration",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "state",
|
||||
label: this.$t("state"),
|
||||
prop: "state.current",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "triggers",
|
||||
prop: "triggers",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "labels",
|
||||
label: this.$t("labels"),
|
||||
prop: "labels",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "inputs",
|
||||
label: this.$t("inputs"),
|
||||
prop: "inputs",
|
||||
default: false
|
||||
},
|
||||
{
|
||||
label: "namespace",
|
||||
label: this.$t("namespace"),
|
||||
prop: "namespace",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "flow",
|
||||
label: this.$t("flow"),
|
||||
prop: "flowId",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "revision",
|
||||
label: this.$t("revision"),
|
||||
prop: "flowRevision",
|
||||
default: false
|
||||
},
|
||||
{
|
||||
label: "task id",
|
||||
label: this.$t("task id"),
|
||||
prop: "taskRunList.taskId",
|
||||
default: false
|
||||
},
|
||||
{
|
||||
label: this.$t("actions"),
|
||||
prop: "action",
|
||||
default: true
|
||||
}
|
||||
],
|
||||
displayColumns: [],
|
||||
@@ -645,6 +660,9 @@
|
||||
displayColumn(column) {
|
||||
return this.hidden ? !this.hidden.includes(column) : this.displayColumns.includes(column);
|
||||
},
|
||||
updateDisplayColumns(newColumns) {
|
||||
this.displayColumns = newColumns;
|
||||
},
|
||||
onShowChartChange(value) {
|
||||
this.showChart = value;
|
||||
localStorage.setItem(storageKeys.SHOW_CHART, value);
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
const taskRunList = [...execution.value.taskRunList];
|
||||
return taskRunList.find((e) => e.taskId === filter);
|
||||
};
|
||||
const onDebugExpression = (expression) => {
|
||||
const onDebugExpression = (expression: string) => {
|
||||
const taskRun = selectedTask();
|
||||
|
||||
if (!taskRun) return;
|
||||
@@ -236,7 +236,7 @@
|
||||
debugExpression.value = response.data.result;
|
||||
|
||||
// Parsing failed, therefore, copy raw result
|
||||
if (response.status === 200)
|
||||
if (response.status === 200 && response.data.result)
|
||||
selected.value.push(response.data.result);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,12 +21,13 @@
|
||||
@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,
|
||||
settings: buttons.settings.shown,
|
||||
dashboards: dashboards.shown,
|
||||
properties: properties.shown,
|
||||
}"
|
||||
@focus="handleFocus"
|
||||
data-test-id="KestraFilter__select"
|
||||
@@ -60,12 +62,13 @@
|
||||
</template>
|
||||
<template v-else-if="dropdowns.second.shown">
|
||||
<el-option
|
||||
v-for="(comparator, index) in dropdowns.first.value.comparators"
|
||||
v-for="(comparator, index) in dropdowns.first.value
|
||||
.comparators"
|
||||
:key="comparator.value"
|
||||
:value="comparator"
|
||||
:label="comparator.label"
|
||||
:class="{
|
||||
selected: current.some(
|
||||
selected: currentFilters.some(
|
||||
(c) => c.comparator === comparator,
|
||||
),
|
||||
}"
|
||||
@@ -75,16 +78,15 @@
|
||||
</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
|
||||
'level-3': true,
|
||||
}"
|
||||
@click="
|
||||
() => !isOptionDisabled(filter) && valueCallback(filter)
|
||||
@@ -92,7 +94,10 @@
|
||||
:data-test-id="`KestraFilter__value__${index}`"
|
||||
>
|
||||
<template v-if="filter.label.component">
|
||||
<component :is="filter.label.component" v-bind="filter.label.props" />
|
||||
<component
|
||||
:is="filter.label.component"
|
||||
v-bind="filter.label.props"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ filter.label }}
|
||||
@@ -107,7 +112,8 @@
|
||||
'me-1':
|
||||
buttons.refresh.shown ||
|
||||
buttons.settings.shown ||
|
||||
dashboards.shown,
|
||||
dashboards.shown ||
|
||||
properties.shown,
|
||||
}"
|
||||
>
|
||||
<KestraIcon :tooltip="$t('search')" placement="bottom">
|
||||
@@ -117,13 +123,17 @@
|
||||
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
|
||||
v-if="buttons.refresh.shown || buttons.settings.shown"
|
||||
class="d-inline-flex ms-1"
|
||||
:class="{'me-1': dashboards.shown}"
|
||||
:class="{'me-1': dashboards.shown || properties.shown}"
|
||||
>
|
||||
<Refresh
|
||||
v-if="buttons.refresh.shown"
|
||||
@@ -141,14 +151,22 @@
|
||||
@dashboard="(value) => emits('dashboard', value)"
|
||||
class="ms-1"
|
||||
/>
|
||||
<Properties
|
||||
v-if="properties.shown"
|
||||
:columns="properties.columns"
|
||||
:model-value="properties.displayColumns"
|
||||
:storage-key="properties.storageKey"
|
||||
@update-properties="(v) => emits('updateProperties', v)"
|
||||
class="ms-1"
|
||||
/>
|
||||
</section>
|
||||
</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, Pair, Property} from "./utils/types";
|
||||
|
||||
import Refresh from "../layout/RefreshButton.vue";
|
||||
import Items from "./segments/Items.vue";
|
||||
@@ -156,28 +174,36 @@
|
||||
import Save from "./segments/Save.vue";
|
||||
import Settings from "./segments/Settings.vue";
|
||||
import Dashboards from "./segments/Dashboards.vue";
|
||||
import Properties from "./segments/Properties.vue";
|
||||
import KestraIcon from "../Kicon.vue";
|
||||
import DateRange from "../layout/DateRange.vue";
|
||||
import Status from "../../components/Status.vue";
|
||||
import Status from "./components/Status.vue";
|
||||
|
||||
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();
|
||||
|
||||
const emits = defineEmits(["dashboard", "input"]);
|
||||
const emits = defineEmits(["dashboard", "input", "updateProperties"]);
|
||||
const props = defineProps({
|
||||
prefix: {type: String, default: undefined},
|
||||
include: {type: Array, default: () => []},
|
||||
values: {type: Object, default: undefined},
|
||||
decode: {type: Boolean, default: true},
|
||||
propertiesWidth: {type: Number, default: 144},
|
||||
buttons: {
|
||||
type: Object as () => Buttons,
|
||||
default: () => ({
|
||||
@@ -192,18 +218,33 @@
|
||||
type: Object as () => Shown,
|
||||
default: () => ({shown: false}),
|
||||
},
|
||||
properties: {
|
||||
type: Object as () => Property,
|
||||
default: () => ({shown: false}),
|
||||
},
|
||||
placeholder: {type: String, default: undefined},
|
||||
searchCallback: {type: Function, default: undefined},
|
||||
});
|
||||
|
||||
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 +280,8 @@
|
||||
} else if (dropdowns.value.third.shown) {
|
||||
valueCallback(option);
|
||||
}
|
||||
|
||||
prefixFilter.value = "";
|
||||
};
|
||||
|
||||
const getInputValue = () => select.value?.states.inputValue;
|
||||
@@ -250,20 +293,20 @@
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
const activeParentFilter = ref<string | null>(null);
|
||||
const lastClickedParent = ref<string | null>(null);
|
||||
const showSubFilterDropdown = ref(false);
|
||||
const valueOptions = ref([]);
|
||||
const valueOptions = ref<Pair[]>([]);
|
||||
const parentValue = ref<string | null>(null);
|
||||
|
||||
const filterCallback = (option) => {
|
||||
@@ -279,7 +322,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 +339,11 @@
|
||||
} 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 +355,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 +365,29 @@
|
||||
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 +396,36 @@
|
||||
// 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"
|
||||
["namespace", "log level"].includes(
|
||||
lastClickedParent.value.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
const values = current.value[parentIndex].value;
|
||||
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 +436,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
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 +476,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,
|
||||
);
|
||||
});
|
||||
@@ -462,18 +499,15 @@
|
||||
break;
|
||||
|
||||
case "state":
|
||||
valueOptions.value = (props.values?.state || VALUES.EXECUTION_STATES).
|
||||
map(value => {
|
||||
value.label = {
|
||||
"component": shallowRef(Status),
|
||||
"props": {
|
||||
"class": "justify-content-center",
|
||||
"status": value.value,
|
||||
"size": "small"
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
valueOptions.value = (
|
||||
props.values?.state || VALUES.EXECUTION_STATES
|
||||
).map((value) => {
|
||||
value.label = {
|
||||
component: shallowRef(Status),
|
||||
props: {status: value.value},
|
||||
};
|
||||
return value;
|
||||
});
|
||||
break;
|
||||
|
||||
case "trigger_state":
|
||||
@@ -541,40 +575,57 @@
|
||||
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 +634,7 @@
|
||||
};
|
||||
|
||||
const removeItem = (value) => {
|
||||
current.value = current.value.filter(
|
||||
currentFilters.value = currentFilters.value.filter(
|
||||
(item) => JSON.stringify(item) !== JSON.stringify(value),
|
||||
);
|
||||
|
||||
@@ -591,22 +642,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 +684,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 +698,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 +724,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 +790,7 @@
|
||||
const label = labelElement?.textContent;
|
||||
|
||||
if (label) {
|
||||
const existingFilterIndex = current.value.findIndex(
|
||||
const existingFilterIndex = currentFilters.value.findIndex(
|
||||
(item) =>
|
||||
item?.label.toLowerCase() ===
|
||||
label
|
||||
@@ -757,7 +806,10 @@
|
||||
.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: {
|
||||
@@ -801,6 +853,7 @@ $included: 144px;
|
||||
$refresh: 104px;
|
||||
$settins: 52px;
|
||||
$dashboards: 52px;
|
||||
$properties: v-bind('props.propertiesWidth + "px"');
|
||||
|
||||
.filters {
|
||||
@include width-available;
|
||||
@@ -808,6 +861,13 @@ $dashboards: 52px;
|
||||
& .el-select {
|
||||
width: 100%;
|
||||
|
||||
&.refresh.settings.dashboards.properties {
|
||||
max-width: calc(
|
||||
100% - $included - $refresh - $settins - $dashboards -
|
||||
#{$properties}
|
||||
);
|
||||
}
|
||||
|
||||
&.refresh.settings.dashboards {
|
||||
max-width: calc(
|
||||
100% - $included - $refresh - $settins - $dashboards
|
||||
@@ -822,10 +882,22 @@ $dashboards: 52px;
|
||||
max-width: calc(100% - $included - $settins - $dashboards);
|
||||
}
|
||||
|
||||
&.settings.properties {
|
||||
max-width: calc(100% - $included - $settins - #{$properties});
|
||||
}
|
||||
|
||||
&.refresh.dashboards {
|
||||
max-width: calc(100% - $included - $refresh - $dashboards);
|
||||
}
|
||||
|
||||
&.refresh.properties {
|
||||
max-width: calc(100% - $included - $refresh - #{$properties});
|
||||
}
|
||||
|
||||
&.dashboards.properties {
|
||||
max-width: calc(100% - $included - $dashboards - #{$properties});
|
||||
}
|
||||
|
||||
&.refresh {
|
||||
max-width: calc(100% - $included - $refresh);
|
||||
}
|
||||
@@ -835,8 +907,13 @@ $dashboards: 52px;
|
||||
}
|
||||
|
||||
&.dashboards {
|
||||
min-width: $dashboards;
|
||||
max-width: calc(100% - $included - $dashboards);
|
||||
}
|
||||
|
||||
&.properties {
|
||||
max-width: calc(100% - $included - #{$properties});
|
||||
}
|
||||
}
|
||||
|
||||
& .el-select__placeholder {
|
||||
@@ -872,6 +949,7 @@ $dashboards: 52px;
|
||||
.filters-select {
|
||||
& .el-select-dropdown {
|
||||
width: auto !important;
|
||||
max-width: 300px;
|
||||
|
||||
&:has(.el-select-dropdown__empty) {
|
||||
width: auto !important;
|
||||
|
||||
25
ui/src/components/filter/components/Status.vue
Normal file
25
ui/src/components/filter/components/Status.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="d-flex align-items-center cursor-pointer">
|
||||
<div :style class="circle" />
|
||||
<span>{{ $filters.cap(status) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({status: {type: String, required: true}});
|
||||
|
||||
const style = computed(() => ({
|
||||
backgroundColor: `var(--ks-chart-${props.status.toLowerCase()})`,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.circle {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -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">
|
||||
<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>
|
||||
265
ui/src/components/filter/segments/Properties.vue
Normal file
265
ui/src/components/filter/segments/Properties.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div class="properties-wrapper">
|
||||
<KestraIcon :tooltip="$t('properties.hint')" placement="bottom">
|
||||
<el-button :icon="TableColumn" @click.stop="toggleContainer">
|
||||
{{ $t("properties.label") }}
|
||||
</el-button>
|
||||
</KestraIcon>
|
||||
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="properties-container mt-2"
|
||||
:class="{visible: showContainer}"
|
||||
@click.stop
|
||||
>
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
:placeholder="$t('search')"
|
||||
:prefix-icon="Magnify"
|
||||
class="rounded-2 w-100 mb-2"
|
||||
/>
|
||||
<div class="pe-2 scrollable-container">
|
||||
<div class="mt-2 shown-group" v-if="shownProperties.length > 0">
|
||||
<div class="text-start mb-1 group-title">
|
||||
{{ $t("properties.shown") }}
|
||||
</div>
|
||||
<ul
|
||||
class="property-list list-style-none m-0 p-0"
|
||||
id="shown-list"
|
||||
>
|
||||
<li
|
||||
v-for="property in filteredShownProperties"
|
||||
:key="property"
|
||||
>
|
||||
<span class="property-name">{{
|
||||
getColumnLabel(property)
|
||||
}}</span>
|
||||
<div
|
||||
class="eye-icon"
|
||||
@click="toggleProperty(property, true)"
|
||||
>
|
||||
<Eye />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<el-divider v-if="filteredHiddenProperties.length > 0" />
|
||||
<div class="hidden-group">
|
||||
<div
|
||||
class="text-start mb-1 group-title"
|
||||
id="hidden-title"
|
||||
v-if="hiddenProperties.length > 0"
|
||||
>
|
||||
{{ $t("properties.hidden") }}
|
||||
</div>
|
||||
<ul
|
||||
class="property-list list-style-none m-0 p-0"
|
||||
id="hidden-list"
|
||||
>
|
||||
<li
|
||||
v-for="property in filteredHiddenProperties"
|
||||
:key="property"
|
||||
>
|
||||
<span class="property-name fw-bold">{{
|
||||
getColumnLabel(property)
|
||||
}}</span>
|
||||
<div
|
||||
class="eye-icon hidden"
|
||||
@click="toggleProperty(property, false)"
|
||||
>
|
||||
<EyeOff />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, computed, onMounted, onUnmounted} from "vue";
|
||||
import KestraIcon from "../../Kicon.vue";
|
||||
import {Magnify, Eye, EyeOff, TableColumn} from "../utils/icons";
|
||||
|
||||
const props = defineProps({
|
||||
columns: {type: Array, required: true},
|
||||
modelValue: {type: Array, required: true},
|
||||
storageKey: {type: String, required: true},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["updateProperties"]);
|
||||
const containerRef = ref(null);
|
||||
const showContainer = ref(false);
|
||||
const searchQuery = ref("");
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (containerRef.value && !containerRef.value.contains(event.target)) {
|
||||
showContainer.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => document.addEventListener("click", handleClickOutside));
|
||||
onUnmounted(() => document.removeEventListener("click", handleClickOutside));
|
||||
|
||||
const shownProperties = computed(() => props.modelValue);
|
||||
const hiddenProperties = computed(() =>
|
||||
props.columns
|
||||
.map((col) => col.prop)
|
||||
.filter((prop) => !shownProperties.value.includes(prop)),
|
||||
);
|
||||
|
||||
const toggleContainer = () => {
|
||||
showContainer.value = !showContainer.value;
|
||||
};
|
||||
|
||||
const toggleProperty = (prop, isShown) => {
|
||||
const newValue = isShown
|
||||
? shownProperties.value.filter((p) => p !== prop)
|
||||
: [...shownProperties.value, prop];
|
||||
|
||||
localStorage.setItem(`columns_${props.storageKey}`, newValue.join(","));
|
||||
emit("updateProperties", newValue);
|
||||
};
|
||||
|
||||
const getColumnLabel = (prop) => {
|
||||
const column = props.columns.find((col) => col.prop === prop);
|
||||
return column ? column.label : prop;
|
||||
};
|
||||
// Column list based on order defined in Table
|
||||
const filteredShownProperties = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return props.columns
|
||||
.filter(
|
||||
(col) =>
|
||||
shownProperties.value.includes(col.prop) &&
|
||||
getColumnLabel(col.prop).toLowerCase().includes(query),
|
||||
)
|
||||
.map((col) => col.prop);
|
||||
});
|
||||
|
||||
const filteredHiddenProperties = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return props.columns
|
||||
.filter(
|
||||
(col) =>
|
||||
hiddenProperties.value.includes(col.prop) &&
|
||||
getColumnLabel(col.prop).toLowerCase().includes(query),
|
||||
)
|
||||
.map((col) => col.prop);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.properties-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.properties-container {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background-color: var(--ks-background-body);
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
width: 280px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-bottom: 7px;
|
||||
transform: translateY(-10px);
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease,
|
||||
visibility 0.3s ease;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable-container {
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--el-scrollbar-bg-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
}
|
||||
|
||||
.property-list {
|
||||
li {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
line-height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-name {
|
||||
font-size: var(--el-font-size-small);
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
:deep(.el-input__prefix-inner) {
|
||||
color: var(--bs-gray-700);
|
||||
font-size: var(--el-font-size-large);
|
||||
}
|
||||
|
||||
.eye-icon {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__inner::placeholder) {
|
||||
color: var(--bs-gray-700);
|
||||
font-size: var(--el-font-size-small);
|
||||
}
|
||||
</style>
|
||||
@@ -19,6 +19,9 @@ import StateMachine from "vue-material-design-icons/StateMachine.vue";
|
||||
import TagOutline from "vue-material-design-icons/TagOutline.vue";
|
||||
import TimelineTextOutline from "vue-material-design-icons/TimelineTextOutline.vue";
|
||||
import ViewDashboardEdit from "vue-material-design-icons/ViewDashboardEdit.vue";
|
||||
import TableColumn from "vue-material-design-icons/TableColumnPlusAfter.vue";
|
||||
import Eye from "vue-material-design-icons/Eye.vue";
|
||||
import EyeOff from "vue-material-design-icons/EyeOff.vue";
|
||||
import Pencil from "vue-material-design-icons/Pencil.vue";
|
||||
import Menu from "vue-material-design-icons/Menu.vue";
|
||||
|
||||
@@ -44,6 +47,9 @@ export {
|
||||
TagOutline,
|
||||
TimelineTextOutline,
|
||||
ViewDashboardEdit,
|
||||
TableColumn,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Pencil,
|
||||
Menu
|
||||
};
|
||||
|
||||
@@ -2,6 +2,13 @@ export type Shown = {
|
||||
shown: boolean;
|
||||
};
|
||||
|
||||
export type Property = {
|
||||
shown: boolean;
|
||||
columns?: any[];
|
||||
displayColumns?: string[];
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
export type Buttons = {
|
||||
refresh: Shown & {
|
||||
callback: () => void;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<script>
|
||||
import {Bar} from "vue-chartjs";
|
||||
import {mapState} from "vuex";
|
||||
import {mapState, mapGetters} from "vuex";
|
||||
import moment from "moment";
|
||||
import {defaultConfig, getFormat, tooltip} from "../../utils/charts";
|
||||
import {cssVariable} from "@kestra-io/ui-libs";
|
||||
@@ -80,9 +80,7 @@
|
||||
"aggregatedMetric",
|
||||
"tasksWithMetrics",
|
||||
]),
|
||||
theme() {
|
||||
return localStorage.getItem("theme") || "light";
|
||||
},
|
||||
...mapGetters("misc", ["theme"]),
|
||||
xGrid() {
|
||||
return this.theme === "light"
|
||||
? {}
|
||||
@@ -171,7 +169,7 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}, this.theme);
|
||||
},
|
||||
display() {
|
||||
return this.$route.query.metric && this.$route.query.aggregation;
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
containerClass: "demo-container",
|
||||
props:{
|
||||
embed: true
|
||||
}
|
||||
},
|
||||
locked: true
|
||||
});
|
||||
|
||||
return tabs;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@
|
||||
import {SECTIONS} from "../../utils/constants.js";
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
computed: {
|
||||
...mapGetters("flow", ["taskError"]),
|
||||
},
|
||||
|
||||
@@ -55,9 +55,13 @@
|
||||
</span>
|
||||
</el-header>
|
||||
<el-main>
|
||||
{{ warnings.join("\n") }}
|
||||
<br v-if="infos">
|
||||
{{ infos?.join("\n") }}
|
||||
<span v-for="(warning, index) in warnings" :key="index">
|
||||
{{ warning }}<br v-if="index < warnings.length - 1">
|
||||
</span>
|
||||
<br v-if="infos && infos.length > 0">
|
||||
<span v-for="(info, index) in infos" :key="index">
|
||||
{{ info }}<br v-if="index < infos.length - 1">
|
||||
</span>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<!-- Non required properties shown collapsed-->
|
||||
<el-collapse class="collapse">
|
||||
<el-collapse-item :title="$t('no_code.sections.advanced')">
|
||||
<el-collapse-item :title="$t('no_code.sections.optional')">
|
||||
<el-form-item
|
||||
:key="index"
|
||||
:required="isRequired(key)"
|
||||
@@ -135,7 +135,7 @@
|
||||
emits: ["update:modelValue"],
|
||||
methods: {
|
||||
properties(requiredFields) {
|
||||
if (this.schema) {
|
||||
if (this.schema?.properties) {
|
||||
const properties = Object.entries(
|
||||
this.schema.properties,
|
||||
).reduce((acc, [key, value]) => {
|
||||
|
||||
@@ -8,20 +8,24 @@
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<InputText
|
||||
<editor
|
||||
:model-value="editorValue"
|
||||
:navbar="false"
|
||||
:full-height="false"
|
||||
schema-type="flow"
|
||||
lang="plaintext"
|
||||
input
|
||||
@update:model-value="onInput"
|
||||
class="w-100"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import InputText from "../../../components/code/components/inputs/InputText.vue";
|
||||
import Editor from "../../../components/inputs/Editor.vue";
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
components: {InputText},
|
||||
components: {Editor},
|
||||
emits: ["update:modelValue"],
|
||||
computed: {
|
||||
isValid() {
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
</div>
|
||||
</slot>
|
||||
</nav>
|
||||
<slot name="absolute" />
|
||||
<div class="editor-absolute-container pe-none">
|
||||
<slot name="absolute" />
|
||||
</div>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
<div class="editor-container" ref="container" :class="[containerClass, {'mb-2': label}]">
|
||||
<div ref="editorContainer" class="editor-wrapper position-relative">
|
||||
@@ -513,6 +515,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.editor-absolute-container {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.editor-absolute-container > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="!isNamespace && (isAllowedEdit || canDelete)" class="mx-2">
|
||||
<div v-if="!isNamespace && (isAllowedEdit || canDelete)" class="me-2">
|
||||
<el-dropdown>
|
||||
<el-button type="default" :disabled="isReadOnly">
|
||||
<DotsVertical title="" />
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
>
|
||||
<template v-if="editorViewType === 'YAML'">
|
||||
<editor
|
||||
class="position-relative"
|
||||
v-if="isCreating || openedTabs.length"
|
||||
ref="editorDomElement"
|
||||
@save="save"
|
||||
@@ -144,7 +145,11 @@
|
||||
@restart-guided-tour="() => persistViewType(editorViewTypes.SOURCE)"
|
||||
:read-only="isReadOnly"
|
||||
:navbar="false"
|
||||
/>
|
||||
>
|
||||
<template #absolute>
|
||||
<KeyShortcuts />
|
||||
</template>
|
||||
</editor>
|
||||
<section v-else class="no-tabs-opened">
|
||||
<div class="img" />
|
||||
|
||||
@@ -166,6 +171,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>
|
||||
@@ -329,6 +335,7 @@
|
||||
import ValidationError from "../flows/ValidationError.vue";
|
||||
import Blueprints from "override/components/flows/blueprints/Blueprints.vue";
|
||||
import SwitchView from "./SwitchView.vue";
|
||||
import KeyShortcuts from "./KeyShortcuts.vue";
|
||||
import PluginDocumentation from "../plugins/PluginDocumentation.vue";
|
||||
import permission from "../../models/permission";
|
||||
import action from "../../models/action";
|
||||
@@ -799,8 +806,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 +946,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 +964,12 @@
|
||||
haveChange.value = true;
|
||||
};
|
||||
|
||||
const handleReorder = (yaml) => {
|
||||
flowYaml.value = yaml;
|
||||
haveChange.value = true;
|
||||
save()
|
||||
};
|
||||
|
||||
const editorUpdate = (event) => {
|
||||
const currentIsFlow = isFlow();
|
||||
|
||||
@@ -1471,7 +1482,7 @@
|
||||
text-align: center;
|
||||
|
||||
.img {
|
||||
background: url("../../assets/errors/kestra-error.png") no-repeat center;
|
||||
background: url("../../assets/empty-ns-files.png") no-repeat center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
|
||||
111
ui/src/components/inputs/KeyShortcuts.vue
Normal file
111
ui/src/components/inputs/KeyShortcuts.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<el-tooltip
|
||||
:content="$t('editor_shortcuts.label')"
|
||||
:hide-after="0"
|
||||
:persistent="false"
|
||||
effect="light"
|
||||
placement="top"
|
||||
>
|
||||
<Keyboard @click="isShown = true" class="keyboard" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-dialog v-model="isShown" top="25vh" header-class="p-3" body-class="p-2">
|
||||
<template #header>
|
||||
<div class="d-flex align-items-center gap-2 fw-normal">
|
||||
<el-icon :size="30">
|
||||
<Keyboard />
|
||||
</el-icon>
|
||||
<span class="fs-6">
|
||||
{{ $t("editor_shortcuts.label") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="d-flex flex-column gap-3 fw-normal">
|
||||
<div
|
||||
v-for="(command, i) in commands"
|
||||
:key="i"
|
||||
class="d-flex align-items-center gap-3"
|
||||
>
|
||||
<div class="d-flex align-items-center gap-2 keys">
|
||||
<template v-for="(key, index) in command.keys" :key="index">
|
||||
<el-tag>{{ key }}</el-tag>
|
||||
<span
|
||||
v-if="index < command.keys.length - 1"
|
||||
class="fw-bold"
|
||||
>+</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="text-break">
|
||||
{{ $t(command.description) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import Keyboard from "vue-material-design-icons/Keyboard.vue";
|
||||
|
||||
const isShown = ref(false);
|
||||
|
||||
const commands = [
|
||||
{
|
||||
keys: ["⌘ Cmd/Ctrl", "p"],
|
||||
description: "editor_shortcuts.command_palette",
|
||||
},
|
||||
{
|
||||
keys: ["⌘ Cmd/Ctrl", "s"],
|
||||
description: "editor_shortcuts.save_flow",
|
||||
},
|
||||
{
|
||||
keys: ["⌥ Option/Alt", "↑", "↓"],
|
||||
description: "editor_shortcuts.move_line",
|
||||
},
|
||||
{
|
||||
keys: ["⇧ Shift", "⌥ Option/Alt", "↑", "↓"],
|
||||
description: "editor_shortcuts.duplicate_cursor",
|
||||
},
|
||||
{
|
||||
keys: ["⌘ Cmd/Ctrl", "k", "l"],
|
||||
description: "editor_shortcuts.fold_unfold",
|
||||
},
|
||||
{
|
||||
keys: ["⌘ Cmd/Ctrl", "/"],
|
||||
description: "editor_shortcuts.comment_uncomment",
|
||||
},
|
||||
{
|
||||
keys: ["⌘ Cmd/Ctrl", "k", "c"],
|
||||
description: "editor_shortcuts.comment",
|
||||
},
|
||||
{
|
||||
keys: ["⌘ Cmd/Ctrl", "k", "u"],
|
||||
description: "editor_shortcuts.uncomment",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.keyboard {
|
||||
color: var(--ks-content-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
background-color: var(--ks-tag-background);
|
||||
color: var(--ks-tag-content);
|
||||
font-size: var(--el-tag-font-size);
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.el-tag::after {
|
||||
content: attr(data-content);
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
||||
@@ -293,10 +293,12 @@
|
||||
);
|
||||
};
|
||||
|
||||
const onCreateNewTask = () => {
|
||||
const onCreateNewTask = (details) => {
|
||||
emit("openNoCode", {
|
||||
section: SECTIONS.TASKS.toLowerCase(),
|
||||
identifier: "new",
|
||||
target: details[0],
|
||||
position: details[1],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import "monaco-editor/esm/vs/editor/editor.all.js";
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard.js";
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.js"
|
||||
import "monaco-editor/esm/vs/language/json/monaco.contribution";
|
||||
import "monaco-editor/esm/vs/basic-languages/monaco.contribution";
|
||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
@@ -629,6 +630,11 @@
|
||||
command: "editor.action.triggerSuggest"
|
||||
})
|
||||
|
||||
monaco.editor.addKeybindingRule({
|
||||
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyP,
|
||||
command: "editor.action.quickCommand"
|
||||
})
|
||||
|
||||
this.editor = monaco.editor.create(this.$el, options);
|
||||
|
||||
if(!this.input){
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-container data-component="FILENAME_PLACEHOLDER#container" direction="vertical" v-loading="isLoading">
|
||||
<slot name="top" data-component="FILENAME_PLACEHOLDER#top" />
|
||||
|
||||
<pagination v-if="!embed" :size="size" :top="true" :page="page" :total="total" @page-changed="onPageChanged">
|
||||
<pagination v-if="!embed && !hideTopPagination" :size="size" :top="true" :page="page" :total="total" @page-changed="onPageChanged">
|
||||
<template #search>
|
||||
<slot name="search" />
|
||||
</template>
|
||||
@@ -44,6 +44,7 @@
|
||||
size: {type: Number, default: 25},
|
||||
page: {type: Number, default: 1},
|
||||
embed: {type: Boolean, default: false},
|
||||
hideTopPagination: {type: Boolean, default: false},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -56,13 +57,3 @@
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-table) {
|
||||
td {
|
||||
.el-tag {
|
||||
margin-right: .3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-empty :image :image-size="180">
|
||||
<el-empty :image="noData" :image-size="180">
|
||||
<template #description>
|
||||
<span v-html="description" />
|
||||
</template>
|
||||
@@ -9,26 +9,19 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
|
||||
import Utils from "../../utils/utils";
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
import Dark from "../../assets/dark.png";
|
||||
import Light from "../../assets/light.png";
|
||||
import noData from "../../assets/no_data.png";
|
||||
|
||||
const props = defineProps({text: {type: String, default: undefined}});
|
||||
|
||||
const image = computed(() => (Utils.getTheme() === "light" ? Light : Dark));
|
||||
const description = computed(() => props.text ?? t("no_data"));
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-empty {
|
||||
padding-top: 0;
|
||||
|
||||
.el-empty__description {
|
||||
font-size: var(--el-font-size-small);
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-empty__description) {
|
||||
font-size: var(--el-font-size-small);
|
||||
color: var(--ks-content-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
position: absolute;
|
||||
height: var(--table-header-height);
|
||||
width: var(--table-header-width);
|
||||
background-color: var(--bs-gray-100-darken-3);
|
||||
background-color: var(--ks-background-table-header);
|
||||
border-radius: var(--bs-border-radius-lg) var(--bs-border-radius-lg) 0 0;
|
||||
border-bottom: 1px solid var(--ks-border-primary);
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
class: "menu-icon",
|
||||
},
|
||||
child: [{
|
||||
|
||||
// here we use only one component for all bookmarks
|
||||
// so when one edits the bookmark, it will be updated without closing the section
|
||||
component: () => h(BookmarkLinkList, {pages: store.state.bookmarks.pages}),
|
||||
}]
|
||||
}] : []),
|
||||
@@ -258,7 +259,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 +270,7 @@
|
||||
}
|
||||
|
||||
&:hover, body &_hover {
|
||||
background-color: var(--ks-button-background-primary);
|
||||
background-color: var(--ks-button-background-secondary-hover);
|
||||
}
|
||||
|
||||
.el-tooltip__trigger {
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
title: this.$t("flows"),
|
||||
props: {
|
||||
tab: "flows",
|
||||
embed: true,
|
||||
embed: false,
|
||||
},
|
||||
query: {
|
||||
id: this.$route.query.id
|
||||
@@ -189,7 +189,8 @@
|
||||
containerClass: "demo-container",
|
||||
props: {
|
||||
tab: "edit",
|
||||
}
|
||||
},
|
||||
locked: true
|
||||
},
|
||||
{
|
||||
name: "variables",
|
||||
@@ -198,7 +199,8 @@
|
||||
containerClass: "demo-container",
|
||||
props: {
|
||||
tab: "variables",
|
||||
}
|
||||
},
|
||||
locked: true
|
||||
},
|
||||
{
|
||||
name: "plugin-defaults",
|
||||
@@ -207,7 +209,8 @@
|
||||
containerClass: "demo-container",
|
||||
props: {
|
||||
tab: "plugin-defaults",
|
||||
}
|
||||
},
|
||||
locked: true
|
||||
},
|
||||
{
|
||||
name: "secrets",
|
||||
@@ -216,7 +219,8 @@
|
||||
containerClass: "demo-container",
|
||||
props: {
|
||||
tab: "secrets",
|
||||
}
|
||||
},
|
||||
locked: true
|
||||
},
|
||||
{
|
||||
name: "audit-logs",
|
||||
@@ -225,7 +229,8 @@
|
||||
containerClass: "demo-container",
|
||||
props: {
|
||||
tab: "audit-logs",
|
||||
}
|
||||
},
|
||||
locked: true
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -8,13 +8,35 @@
|
||||
<Toc @router-change="onRouterChange" v-if="plugins" :plugins="plugins.filter(p => !p.subGroup)" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="markdown" v-loading="isLoading">
|
||||
<markdown :source="plugin.markdown" :permalink="true" />
|
||||
<div class="plugin-doc">
|
||||
<div class="d-flex gap-3 mb-3 align-items-center">
|
||||
<task-icon
|
||||
class="plugin-icon"
|
||||
:cls="$route.params.cls"
|
||||
only-icon
|
||||
:icons="icons"
|
||||
/>
|
||||
<h4 class="mb-0">
|
||||
{{ pluginName }}
|
||||
</h4>
|
||||
</div>
|
||||
<Suspense v-loading="isLoading">
|
||||
<schema-to-html class="plugin-schema" :dark-mode="Utils.getTheme() === 'dark'" :schema="plugin.schema" :props-initially-expanded="true" :plugin-type="$route.params.cls">
|
||||
<template #markdown="{content}">
|
||||
<markdown font-size-var="font-size-base" :source="content" />
|
||||
</template>
|
||||
</schema-to-html>
|
||||
</Suspense>
|
||||
</div>
|
||||
</template>
|
||||
</docs-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Utils from "../../utils/utils.js";
|
||||
import {TaskIcon} from "@kestra-io/ui-libs";
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import RouteContext from "../../mixins/routeContext";
|
||||
import TopNavBar from "../../components/layout/TopNavBar.vue";
|
||||
@@ -23,10 +45,12 @@
|
||||
import {mapState} from "vuex";
|
||||
import PluginHome from "./PluginHome.vue";
|
||||
import DocsLayout from "../docs/DocsLayout.vue";
|
||||
import {SchemaToHtml} from "@kestra-io/ui-libs";
|
||||
|
||||
export default {
|
||||
mixins: [RouteContext],
|
||||
components: {
|
||||
SchemaToHtml,
|
||||
DocsLayout,
|
||||
PluginHome,
|
||||
Markdown,
|
||||
@@ -34,7 +58,7 @@
|
||||
TopNavBar
|
||||
},
|
||||
computed: {
|
||||
...mapState("plugin", ["plugin", "plugins"]),
|
||||
...mapState("plugin", ["plugin", "plugins", "icons"]),
|
||||
routeInfo() {
|
||||
return {
|
||||
title: this.$route.params.cls ? this.$route.params.cls : this.$t("plugins.names"),
|
||||
@@ -48,6 +72,10 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
pluginName() {
|
||||
const split = this.$route.params.cls.split(".");
|
||||
return split[split.length - 1];
|
||||
},
|
||||
pluginIsSelected() {
|
||||
return this.plugin && this.$route.params.cls
|
||||
}
|
||||
@@ -70,7 +98,9 @@
|
||||
},
|
||||
methods: {
|
||||
loadToc() {
|
||||
this.$store.dispatch("plugin/listWithSubgroup")
|
||||
this.$store.dispatch("plugin/listWithSubgroup", {
|
||||
includeDeprecated: false
|
||||
})
|
||||
},
|
||||
|
||||
loadPlugin() {
|
||||
@@ -97,3 +127,6 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/components/plugin-doc";
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
<Suspense>
|
||||
<schema-to-html class="plugin-schema" :dark-mode="themeComputed === 'dark'" :schema="editorPlugin.schema" :plugin-type="editorPlugin.cls">
|
||||
<schema-to-html class="plugin-schema" :dark-mode="Utils.getTheme() === 'dark'" :schema="editorPlugin.schema" :plugin-type="editorPlugin.cls">
|
||||
<template #markdown="{content}">
|
||||
<markdown font-size-var="font-size-base" :source="content" />
|
||||
</template>
|
||||
@@ -24,6 +24,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Utils from "../../utils/utils.js";
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Markdown from "../layout/Markdown.vue";
|
||||
import {SchemaToHtml, TaskIcon} from "@kestra-io/ui-libs";
|
||||
@@ -46,16 +50,6 @@
|
||||
pluginName() {
|
||||
const split = this.editorPlugin.cls.split(".");
|
||||
return split[split.length - 1];
|
||||
},
|
||||
themeComputed() {
|
||||
const savedEditorTheme = localStorage.getItem("editorTheme");
|
||||
return savedEditorTheme === "syncWithSystem"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: savedEditorTheme === "light"
|
||||
? "light"
|
||||
: "dark";
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -65,72 +59,5 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "@kestra-io/ui-libs/src/scss/color-palette" as color-palette;
|
||||
|
||||
.plugin-doc {
|
||||
background-color: var(--ks-background-body) !important;
|
||||
|
||||
:deep(.plugin-title) {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.plugin-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.plugin-schema {
|
||||
:deep(button) {
|
||||
color: var(--ks-content-primary);
|
||||
}
|
||||
|
||||
:deep(.code-block) {
|
||||
background-color: var(--ks-background-card);
|
||||
border: 1px solid var(--ks-border-primary)
|
||||
}
|
||||
|
||||
:deep(.language) {
|
||||
color: var(--ks-content-tertiary);
|
||||
}
|
||||
|
||||
:deep(.plugin-section) {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.collapse-button {
|
||||
padding: 3px 0;
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
> .collapse-button:not(.collapsed) {
|
||||
color: var(--ks-content-link);
|
||||
}
|
||||
|
||||
.property {
|
||||
&:has(.collapsed):hover {
|
||||
background-color: var(--ks-dropdown-background-hover);
|
||||
}
|
||||
|
||||
&:not(:has(.collapsed)) {
|
||||
background-color: var(--ks-dropdown-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
.type-box{
|
||||
.ref-type {
|
||||
border-right: 1px solid var(--ks-border-primary);
|
||||
}
|
||||
|
||||
&:has(.ref-type):hover {
|
||||
background: var(--ks-button-background-secondary-hover) !important;
|
||||
|
||||
.ref-type {
|
||||
border-right: 1px solid var(--ks-border-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "../../styles/components/plugin-doc";
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@
|
||||
<KestraFilter :placeholder="$t('pluginPage.search', {count: countPlugin})" :search-callback="(input)=> searchInput = input" />
|
||||
</el-row>
|
||||
<section class="px-3 plugins-container">
|
||||
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="index" effect="light">
|
||||
<el-tooltip v-for="(plugin, index) in pluginsList" :show-after="1000" :key="plugin.name + '-' + index" effect="light">
|
||||
<template #content>
|
||||
<div class="tasks-tooltips">
|
||||
<p v-if="plugin?.tasks.filter(t => t.toLowerCase().includes(searchInput)).length > 0" class="mb-0">
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
:model-value="modelValue"
|
||||
:placeholder="$t('no_code.creation.select', {section: section.toLowerCase().slice(0, -1)})"
|
||||
filterable
|
||||
clearable
|
||||
@update:model-value="onInput"
|
||||
>
|
||||
<el-option
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
label: $t('no_code.labels.yaml'),
|
||||
value: 'YAML'
|
||||
|
||||
},
|
||||
},
|
||||
{
|
||||
label: $t('no_code.labels.no_code'),
|
||||
value: 'NO_CODE'
|
||||
@@ -135,8 +135,8 @@
|
||||
:max="50"
|
||||
/>
|
||||
</Column>
|
||||
|
||||
<Column :label="$t('settings.blocks.theme.fields.logs_font_size')">
|
||||
|
||||
<Column :label="$t('settings.blocks.theme.fields.logs_font_size')">
|
||||
<el-input-number
|
||||
:model-value="pendingSettings.logsFontSize"
|
||||
@update:model-value="onLogsFontSize"
|
||||
@@ -312,7 +312,7 @@
|
||||
};
|
||||
}).sort((a, b) => a.offset - b.offset),
|
||||
guidedTour: undefined,
|
||||
now: this.$moment(),
|
||||
now: this.$moment(),
|
||||
localeKey: this.$moment.locale(),
|
||||
};
|
||||
},
|
||||
@@ -323,7 +323,7 @@
|
||||
this.pendingSettings.editorType = localStorage.getItem(storageKeys.EDITOR_VIEW_TYPE) || "YAML";
|
||||
this.pendingSettings.defaultLogLevel = localStorage.getItem("defaultLogLevel") || "INFO";
|
||||
this.pendingSettings.lang = Utils.getLang();
|
||||
|
||||
|
||||
this.pendingSettings.theme = Utils.getTheme();
|
||||
this.pendingSettings.editorTheme = Utils.getTheme("editorTheme")
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
},
|
||||
updateThemeBasedOnSystem() {
|
||||
if (this.theme === "syncWithSystem") {
|
||||
Utils.switchTheme("syncWithSystem");
|
||||
Utils.switchTheme(this.$store, "syncWithSystem");
|
||||
}
|
||||
},
|
||||
onDateFormat(value) {
|
||||
@@ -446,9 +446,9 @@
|
||||
case "logsFontSize":
|
||||
localStorage.setItem(key, this.pendingSettings[key])
|
||||
this.$store.commit("layout/setLogsFontSize", this.pendingSettings[key])
|
||||
break
|
||||
break
|
||||
case "theme":
|
||||
Utils.switchTheme(this.pendingSettings[key]);
|
||||
Utils.switchTheme(this.$store, this.pendingSettings[key]);
|
||||
localStorage.setItem(key, Utils.getTheme())
|
||||
break
|
||||
case "lang":
|
||||
@@ -611,7 +611,7 @@
|
||||
}
|
||||
|
||||
.el-input__count {
|
||||
color: var(--bs-white) !important;
|
||||
color: var(--ks-content-primary) !important;
|
||||
|
||||
.el-input__count-inner {
|
||||
background: none !important;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user