mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
123 Commits
executor_v
...
v0.21.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23bde6b716 | ||
|
|
0b2df61c2e | ||
|
|
d30b331b3c | ||
|
|
1fa026f0ee | ||
|
|
3a39c65829 | ||
|
|
b174a81562 | ||
|
|
077421d59c | ||
|
|
fcf999ff61 | ||
|
|
3e2f798ccf | ||
|
|
69faecf339 | ||
|
|
aa3a6854ae | ||
|
|
bb6edfff98 | ||
|
|
f7b495d22f | ||
|
|
eaf63f307c | ||
|
|
905f778204 | ||
|
|
0b15711b23 | ||
|
|
a51b193f4b | ||
|
|
42a7938d38 | ||
|
|
5783a95db3 | ||
|
|
785afe7884 | ||
|
|
28fea2e5dc | ||
|
|
dcc59fde35 | ||
|
|
4e9ac8b3a2 | ||
|
|
5d5b74613b | ||
|
|
44c149e8d5 | ||
|
|
c262525341 | ||
|
|
7da24df76f | ||
|
|
2664307517 | ||
|
|
8c0f0f86b6 | ||
|
|
b651f53e8a | ||
|
|
10fad29923 | ||
|
|
d9962a89a7 | ||
|
|
60b189d101 | ||
|
|
6b065815b7 | ||
|
|
8c943b43f0 | ||
|
|
8b813115a9 | ||
|
|
4a6bb0ba87 | ||
|
|
a2daf0f493 | ||
|
|
0e3218c7be | ||
|
|
d98c5e19fc | ||
|
|
e086099d6c | ||
|
|
df3bec4d6c | ||
|
|
4b946175bf | ||
|
|
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
|
||||
|
||||
27
.github/workflows/docker.yml
vendored
27
.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
|
||||
@@ -87,6 +99,11 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Docker - Fix Qemu
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -c yes
|
||||
|
||||
# Docker Build and push
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -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,14 +49,20 @@ 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' }}
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
chmod +x ./release-plugins.sh;
|
||||
./release-plugins.sh \
|
||||
chmod +x ./dev-tools/release-plugins.sh;
|
||||
|
||||
./dev-tools/release-plugins.sh \
|
||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
||||
--next-version=${{github.event.inputs.nextVersion}} \
|
||||
--yes \
|
||||
@@ -51,8 +73,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
chmod +x ./release-plugins.sh;
|
||||
./release-plugins.sh \
|
||||
chmod +x ./dev-tools/release-plugins.sh;
|
||||
|
||||
./dev-tools/release-plugins.sh \
|
||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
||||
--next-version=${{github.event.inputs.nextVersion}} \
|
||||
--dry-run \
|
||||
89
.github/workflows/gradle-release.yml
vendored
Normal file
89
.github/workflows/gradle-release.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: Run Gradle Release
|
||||
run-name: "Releasing Kestra ${{ github.event.inputs.releaseVersion }} 🚀"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releaseVersion:
|
||||
description: 'The release version (e.g., 0.21.0-rc1)'
|
||||
required: true
|
||||
type: string
|
||||
nextVersion:
|
||||
description: 'The next version (e.g., 0.22.0-SNAPSHOT)'
|
||||
required: true
|
||||
type: string
|
||||
env:
|
||||
RELEASE_VERSION: "${{ github.event.inputs.releaseVersion }}"
|
||||
NEXT_VERSION: "${{ github.event.inputs.nextVersion }}"
|
||||
jobs:
|
||||
release:
|
||||
name: Release Kestra
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
# Checks
|
||||
- name: Check Inputs
|
||||
run: |
|
||||
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+)\.0-rc[01](-SNAPSHOT)?$ ]]; then
|
||||
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)\.0-rc[01](-SNAPSHOT)?$"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$NEXT_VERSION" =~ ^[0-9]+(\.[0-9]+)\.0-SNAPSHOT$ ]]; then
|
||||
echo "Invalid next version. Must match regex: ^[0-9]+(\.[0-9]+)\.0-SNAPSHOT$"
|
||||
exit 1;
|
||||
fi
|
||||
# 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
|
||||
|
||||
- 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
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
# Extract the major and minor versions
|
||||
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
|
||||
PUSH_RELEASE_BRANCH="releases/v${BASE_VERSION}.x"
|
||||
|
||||
# Create and push release branch
|
||||
git checkout -b "$PUSH_RELEASE_BRANCH";
|
||||
git push -u origin "$PUSH_RELEASE_BRANCH";
|
||||
|
||||
# Run gradle release
|
||||
git checkout develop;
|
||||
|
||||
if [[ "$RELEASE_VERSION" == *"-SNAPSHOT" ]]; then
|
||||
# -SNAPSHOT qualifier maybe used to test release-candidates
|
||||
./gradlew release -Prelease.useAutomaticVersion=true \
|
||||
-Prelease.releaseVersion="${RELEASE_VERSION}" \
|
||||
-Prelease.newVersion="${NEXT_VERSION}" \
|
||||
-Prelease.pushReleaseVersionBranch="${PUSH_RELEASE_BRANCH}" \
|
||||
-Prelease.failOnSnapshotDependencies=false
|
||||
else
|
||||
./gradlew release -Prelease.useAutomaticVersion=true \
|
||||
-Prelease.releaseVersion="${RELEASE_VERSION}" \
|
||||
-Prelease.newVersion="${NEXT_VERSION}" \
|
||||
-Prelease.pushReleaseVersionBranch="${PUSH_RELEASE_BRANCH}"
|
||||
fi
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Update and Tag Kestra Plugins
|
||||
name: Set Version and Tag Plugins
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
tag:
|
||||
name: Release plugins
|
||||
runs-on: kestra-private-standard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
@@ -29,25 +29,32 @@ 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 }}
|
||||
run: |
|
||||
chmod +x ./tag-release-plugins.sh;
|
||||
./tag-release-plugins.sh \
|
||||
chmod +x ./dev-tools/setversion-tag-plugins.sh;
|
||||
|
||||
./dev-tools/setversion-tag-plugins.sh \
|
||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
||||
--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 }}
|
||||
run: |
|
||||
chmod +x ./tag-release-plugins.sh;
|
||||
./tag-release-plugins.sh \
|
||||
chmod +x ./dev-tools/setversion-tag-plugins.sh;
|
||||
|
||||
./dev-tools/setversion-tag-plugins.sh \
|
||||
--release-version=${{github.event.inputs.releaseVersion}} \
|
||||
--dry-run \
|
||||
--yes \
|
||||
58
.github/workflows/setversion-tag.yml
vendored
Normal file
58
.github/workflows/setversion-tag.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Set Version and Tag
|
||||
run-name: "Set version and Tag Kestra to ${{ github.event.inputs.releaseVersion }} 🚀"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releaseVersion:
|
||||
description: 'The release version (e.g., 0.21.1)'
|
||||
required: true
|
||||
type: string
|
||||
env:
|
||||
RELEASE_VERSION: "${{ github.event.inputs.releaseVersion }}"
|
||||
jobs:
|
||||
release:
|
||||
name: Release Kestra
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/heads/releases/v')
|
||||
steps:
|
||||
# Checks
|
||||
- name: Check Inputs
|
||||
run: |
|
||||
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+)(\.[0-9]+)(-rc[0-9])?(-SNAPSHOT)?$ ]]; then
|
||||
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)(\.[0-9]+)-(rc[0-9])?(-SNAPSHOT)?$"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_BRANCH="{{ github.ref }}"
|
||||
|
||||
# Extract the major and minor versions
|
||||
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
|
||||
RELEASE_BRANCH="refs/heads/releases/v${BASE_VERSION}.x"
|
||||
|
||||
if ! [[ "$CURRENT_BRANCH" == "$RELEASE_BRANCH" ]]; then
|
||||
echo "Invalid release branch. Expected $RELEASE_BRANCH, was $CURRENT_BRANCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Checkout
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
# Update version
|
||||
sed -i "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
|
||||
git add ./gradle.properties
|
||||
git commit -m"chore(version): update to version '$RELEASE_VERSION'"
|
||||
git push
|
||||
git tag -a "v$RELEASE_VERSION" -m"v$RELEASE_VERSION"
|
||||
git push origin "v$RELEASE_VERSION"
|
||||
1
.plugins
1
.plugins
@@ -40,6 +40,7 @@
|
||||
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-db2:LATEST
|
||||
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-duckdb:LATEST
|
||||
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-druid:LATEST
|
||||
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-mariadb:LATEST
|
||||
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-mysql:LATEST
|
||||
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-oracle:LATEST
|
||||
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-pinot:LATEST
|
||||
|
||||
@@ -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
|
||||
@@ -82,6 +82,7 @@ public class JsonSchemaGenerator {
|
||||
}
|
||||
replaceAnyOfWithOneOf(objectNode);
|
||||
pullOfDefaultFromOneOf(objectNode);
|
||||
removeRequiredOnPropsWithDefaults(objectNode);
|
||||
|
||||
return JacksonMapper.toMap(objectNode);
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -89,6 +90,27 @@ public class JsonSchemaGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void removeRequiredOnPropsWithDefaults(ObjectNode objectNode) {
|
||||
objectNode.findParents("required").forEach(jsonNode -> {
|
||||
if (jsonNode instanceof ObjectNode clazzSchema && clazzSchema.get("required") instanceof ArrayNode requiredPropsNode && clazzSchema.get("properties") instanceof ObjectNode properties) {
|
||||
List<String> requiredFieldValues = StreamSupport.stream(requiredPropsNode.spliterator(), false)
|
||||
.map(JsonNode::asText)
|
||||
.toList();
|
||||
|
||||
properties.fields().forEachRemaining(e -> {
|
||||
int indexInRequiredArray = requiredFieldValues.indexOf(e.getKey());
|
||||
if (indexInRequiredArray != -1 && e.getValue() instanceof ObjectNode valueNode && valueNode.has("default")) {
|
||||
requiredPropsNode.remove(indexInRequiredArray);
|
||||
}
|
||||
});
|
||||
|
||||
if (requiredPropsNode.isEmpty()) {
|
||||
clazzSchema.remove("required");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void replaceAnyOfWithOneOf(ObjectNode objectNode) {
|
||||
objectNode.findParents("anyOf").forEach(jsonNode -> {
|
||||
if (jsonNode instanceof ObjectNode oNode) {
|
||||
@@ -311,10 +333,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);
|
||||
}
|
||||
@@ -603,6 +627,7 @@ public class JsonSchemaGenerator {
|
||||
ObjectNode objectNode = generator.generateSchema(cls);
|
||||
replaceAnyOfWithOneOf(objectNode);
|
||||
pullOfDefaultFromOneOf(objectNode);
|
||||
removeRequiredOnPropsWithDefaults(objectNode);
|
||||
|
||||
return JacksonMapper.toMap(extractMainRef(objectNode));
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -95,7 +95,7 @@ public class HttpClient implements Closeable {
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (this.configuration.getProxy() != null) {
|
||||
if (this.configuration.getProxy() != null && configuration.getProxy().getAddress() != null) {
|
||||
SocketAddress proxyAddr = new InetSocketAddress(
|
||||
runContext.render(configuration.getProxy().getAddress()).as(String.class).orElse(null),
|
||||
runContext.render(configuration.getProxy().getPort()).as(Integer.class).orElse(null)
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
|
||||
@@ -14,6 +15,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
@JsonSubTypes.Type(value = BearerAuthConfiguration.class, name = "BEARER")
|
||||
})
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@NoArgsConstructor
|
||||
public abstract class AbstractAuthConfiguration {
|
||||
public abstract Property<AuthType> getType();
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.core5.http.HttpHeaders;
|
||||
@@ -16,8 +15,9 @@ import org.apache.hc.core5.http.message.BasicHeader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
@Getter
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class BasicAuthConfiguration extends AbstractAuthConfiguration {
|
||||
@NotNull
|
||||
@JsonInclude
|
||||
@@ -25,10 +25,10 @@ public class BasicAuthConfiguration extends AbstractAuthConfiguration {
|
||||
protected Property<AuthType> type = Property.of(AuthType.BASIC);
|
||||
|
||||
@Schema(title = "The username for HTTP basic authentication.")
|
||||
private final Property<String> username;
|
||||
private Property<String> username;
|
||||
|
||||
@Schema(title = "The password for HTTP basic authentication.")
|
||||
private final Property<String> password;
|
||||
private Property<String> password;
|
||||
|
||||
@Override
|
||||
public void configure(HttpClientBuilder builder, RunContext runContext) throws IllegalVariableEvaluationException {
|
||||
|
||||
@@ -8,13 +8,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.core5.http.HttpHeaders;
|
||||
import org.apache.hc.core5.http.message.BasicHeader;
|
||||
|
||||
@Getter
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class BearerAuthConfiguration extends AbstractAuthConfiguration {
|
||||
@NotNull
|
||||
@JsonInclude
|
||||
@@ -22,7 +24,7 @@ public class BearerAuthConfiguration extends AbstractAuthConfiguration {
|
||||
protected Property<AuthType> type = Property.of(AuthType.BEARER);
|
||||
|
||||
@Schema(title = "The token for bearer token authentication.")
|
||||
private final Property<String> token;
|
||||
private Property<String> token;
|
||||
|
||||
@Override
|
||||
public void configure(HttpClientBuilder builder, RunContext runContext) throws IllegalVariableEvaluationException {
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.micronaut.logging.LogLevel;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -16,6 +17,7 @@ import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Builder(toBuilder = true)
|
||||
@Getter
|
||||
@Jacksonized
|
||||
public class HttpConfiguration {
|
||||
@Schema(title = "The timeout configuration.")
|
||||
@PluginProperty
|
||||
@@ -55,261 +57,217 @@ public class HttpConfiguration {
|
||||
}
|
||||
|
||||
// Deprecated properties
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The time allowed to establish a connection to the server before failing.")
|
||||
@Deprecated
|
||||
private final Property<Duration> connectTimeout;
|
||||
private final Duration connectTimeout;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setConnectTimeout(Property<Duration> connectTimeout) {
|
||||
if (this.timeout == null) {
|
||||
this.timeout = TimeoutConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.timeout = this.timeout.toBuilder()
|
||||
.connectTimeout(connectTimeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The maximum time allowed for reading data from the server before failing.")
|
||||
@Builder.Default
|
||||
@Deprecated
|
||||
private final Property<Duration> readTimeout = Property.of(Duration.ofSeconds(HttpClientConfiguration.DEFAULT_READ_TIMEOUT_SECONDS));
|
||||
private final Duration readTimeout = Duration.ofSeconds(HttpClientConfiguration.DEFAULT_READ_TIMEOUT_SECONDS);
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setReadTimeout(Property<Duration> readTimeout) {
|
||||
if (this.timeout == null) {
|
||||
this.timeout = TimeoutConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.timeout = this.timeout.toBuilder()
|
||||
.readIdleTimeout(readTimeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The type of proxy to use.")
|
||||
@Builder.Default
|
||||
@Deprecated
|
||||
private final Property<Proxy.Type> proxyType = Property.of(Proxy.Type.DIRECT);
|
||||
private final Proxy.Type proxyType = Proxy.Type.DIRECT;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setProxyType(Property<Proxy.Type> proxyType) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.type(proxyType)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The address of the proxy server.")
|
||||
@Deprecated
|
||||
private final Property<String> proxyAddress;
|
||||
private final String proxyAddress;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setProxyAddress(Property<String> proxyAddress) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.address(proxyAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The port of the proxy server.")
|
||||
@Deprecated
|
||||
private final Property<Integer> proxyPort;
|
||||
private final Integer proxyPort;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setProxyPort(Property<Integer> proxyPort) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.port(proxyPort)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The username for proxy authentication.")
|
||||
@Deprecated
|
||||
private final Property<String> proxyUsername;
|
||||
private final String proxyUsername;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setProxyUsername(Property<String> proxyUsername) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.username(proxyUsername)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The password for proxy authentication.")
|
||||
@Deprecated
|
||||
private final Property<String> proxyPassword;
|
||||
private final String proxyPassword;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setProxyPassword(Property<String> proxyPassword) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.password(proxyPassword)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The username for HTTP basic authentication.")
|
||||
@Deprecated
|
||||
private final Property<String> basicAuthUser;
|
||||
private final String basicAuthUser;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setBasicAuthUser(Property<String> basicAuthUser) {
|
||||
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
|
||||
this.auth = BasicAuthConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
|
||||
.username(basicAuthUser)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The password for HTTP basic authentication.")
|
||||
@Deprecated
|
||||
private final Property<String> basicAuthPassword;
|
||||
private final String basicAuthPassword;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
private void setBasicAuthPassword(Property<String> basicAuthPassword) {
|
||||
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
|
||||
this.auth = BasicAuthConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
|
||||
.password(basicAuthPassword)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The log level for the HTTP client.")
|
||||
@PluginProperty
|
||||
@Deprecated
|
||||
private final LogLevel logLevel;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
private void setLogLevel(LogLevel logLevel) {
|
||||
if (logLevel == LogLevel.TRACE) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.REQUEST_HEADERS,
|
||||
LoggingType.REQUEST_BODY,
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
LoggingType.RESPONSE_BODY
|
||||
};
|
||||
} else if (logLevel == LogLevel.DEBUG) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.REQUEST_HEADERS,
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
};
|
||||
} else if (logLevel == LogLevel.INFO) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated properties with no real value to be kept, silently ignore
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
// Deprecated properties with no equivalent value to be kept, silently ignore
|
||||
@Schema(title = "The time allowed for a read connection to remain idle before closing it.")
|
||||
@Builder.Default
|
||||
@Deprecated
|
||||
private final Property<Duration> readIdleTimeout = Property.of(Duration.of(HttpClientConfiguration.DEFAULT_READ_IDLE_TIMEOUT_MINUTES, ChronoUnit.MINUTES));
|
||||
private final Duration readIdleTimeout = Duration.of(HttpClientConfiguration.DEFAULT_READ_IDLE_TIMEOUT_MINUTES, ChronoUnit.MINUTES);
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The time an idle connection can remain in the client's connection pool before being closed.")
|
||||
@Builder.Default
|
||||
@Deprecated
|
||||
private final Property<Duration> connectionPoolIdleTimeout = Property.of(Duration.ofSeconds(HttpClientConfiguration.DEFAULT_CONNECTION_POOL_IDLE_TIMEOUT_SECONDS));
|
||||
private final Duration connectionPoolIdleTimeout = Duration.ofSeconds(HttpClientConfiguration.DEFAULT_CONNECTION_POOL_IDLE_TIMEOUT_SECONDS);
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Schema(title = "The maximum content length of the response.")
|
||||
@Builder.Default
|
||||
@Deprecated
|
||||
private final Property<Integer> maxContentLength = Property.of(HttpClientConfiguration.DEFAULT_MAX_CONTENT_LENGTH);
|
||||
private final Integer maxContentLength = HttpClientConfiguration.DEFAULT_MAX_CONTENT_LENGTH;
|
||||
|
||||
public static class HttpConfigurationBuilder {
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder connectTimeout(Duration connectTimeout) {
|
||||
if (this.timeout == null) {
|
||||
this.timeout = TimeoutConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.timeout = this.timeout.toBuilder()
|
||||
.connectTimeout(Property.of(connectTimeout))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder readTimeout(Duration readTimeout) {
|
||||
if (this.timeout == null) {
|
||||
this.timeout = TimeoutConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.timeout = this.timeout.toBuilder()
|
||||
.readIdleTimeout(Property.of(readTimeout))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyType(Proxy.Type proxyType) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.type(Property.of(proxyType))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyAddress(String proxyAddress) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.address(Property.of(proxyAddress))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyPort(Integer proxyPort) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.port(Property.of(proxyPort))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyUsername(String proxyUsername) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.username(Property.of(proxyUsername))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyPassword(String proxyPassword) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.password(Property.of(proxyPassword))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder basicAuthUser(String basicAuthUser) {
|
||||
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
|
||||
this.auth = BasicAuthConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
|
||||
.username(Property.of(basicAuthUser))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder basicAuthPassword(String basicAuthPassword) {
|
||||
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
|
||||
this.auth = BasicAuthConfiguration.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
|
||||
.password(Property.of(basicAuthPassword))
|
||||
.build();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder logLevel(LogLevel logLevel) {
|
||||
if (logLevel == LogLevel.TRACE) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.REQUEST_HEADERS,
|
||||
LoggingType.REQUEST_BODY,
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
LoggingType.RESPONSE_BODY
|
||||
};
|
||||
} else if (logLevel == LogLevel.DEBUG) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.REQUEST_HEADERS,
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
};
|
||||
} else if (logLevel == LogLevel.INFO) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.kestra.core.models.tasks.FlowableTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
|
||||
@@ -176,6 +177,14 @@ public class Flow extends AbstractFlow implements HasUID {
|
||||
);
|
||||
}
|
||||
|
||||
public static String uid(Trigger trigger) {
|
||||
return IdUtils.fromParts(
|
||||
trigger.getTenantId(),
|
||||
trigger.getNamespace(),
|
||||
trigger.getFlowId()
|
||||
);
|
||||
}
|
||||
|
||||
public static String uidWithoutRevision(Execution execution) {
|
||||
return IdUtils.fromParts(
|
||||
execution.getTenantId(),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -69,7 +69,6 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
|
||||
public class FlowInputOutput {
|
||||
private static final Pattern URI_PATTERN = Pattern.compile("^[a-z]+:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$");
|
||||
private static final ObjectMapper YAML_MAPPER = JacksonMapper.ofYaml();
|
||||
private static final ObjectMapper JSON_MAPPER = JacksonMapper.ofJson();
|
||||
|
||||
private final StorageInterface storageInterface;
|
||||
private final Optional<String> secretKey;
|
||||
@@ -95,11 +94,12 @@ public class FlowInputOutput {
|
||||
* @return The list of {@link InputAndValue}.
|
||||
*/
|
||||
public Mono<List<InputAndValue>> validateExecutionInputs(final List<Input<?>> inputs,
|
||||
final Execution execution,
|
||||
final Publisher<CompletedPart> data) {
|
||||
final Flow flow,
|
||||
final Execution execution,
|
||||
final Publisher<CompletedPart> data) {
|
||||
if (ListUtils.isEmpty(inputs)) return Mono.just(Collections.emptyList());
|
||||
|
||||
return readData(inputs, execution, data, false).map(inputData -> resolveInputs(inputs, execution, inputData));
|
||||
return readData(inputs, execution, data, false).map(inputData -> resolveInputs(inputs, flow, execution, inputData));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,9 +111,9 @@ public class FlowInputOutput {
|
||||
* @return The Map of typed inputs.
|
||||
*/
|
||||
public Mono<Map<String, Object>> readExecutionInputs(final Flow flow,
|
||||
final Execution execution,
|
||||
final Publisher<CompletedPart> data) {
|
||||
return this.readExecutionInputs(flow.getInputs(), execution, data);
|
||||
final Execution execution,
|
||||
final Publisher<CompletedPart> data) {
|
||||
return this.readExecutionInputs(flow.getInputs(), flow, execution, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,9 +125,10 @@ public class FlowInputOutput {
|
||||
* @return The Map of typed inputs.
|
||||
*/
|
||||
public Mono<Map<String, Object>> readExecutionInputs(final List<Input<?>> inputs,
|
||||
final Flow flow,
|
||||
final Execution execution,
|
||||
final Publisher<CompletedPart> data) {
|
||||
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, execution, inputData));
|
||||
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, flow, execution, inputData));
|
||||
}
|
||||
|
||||
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
|
||||
@@ -192,15 +193,16 @@ public class FlowInputOutput {
|
||||
final Execution execution,
|
||||
final Map<String, ?> data
|
||||
) {
|
||||
return readExecutionInputs(flow.getInputs(), execution, data);
|
||||
return readExecutionInputs(flow.getInputs(), flow, execution, data);
|
||||
}
|
||||
|
||||
private Map<String, Object> readExecutionInputs(
|
||||
final List<Input<?>> inputs,
|
||||
final Flow flow,
|
||||
final Execution execution,
|
||||
final Map<String, ?> data
|
||||
) {
|
||||
Map<String, Object> resolved = this.resolveInputs(inputs, execution, data)
|
||||
Map<String, Object> resolved = this.resolveInputs(inputs, flow, execution, data)
|
||||
.stream()
|
||||
.filter(InputAndValue::enabled)
|
||||
.map(it -> {
|
||||
@@ -225,6 +227,7 @@ public class FlowInputOutput {
|
||||
@VisibleForTesting
|
||||
public List<InputAndValue> resolveInputs(
|
||||
final List<Input<?>> inputs,
|
||||
final Flow flow,
|
||||
final Execution execution,
|
||||
final Map<String, ?> data
|
||||
) {
|
||||
@@ -240,7 +243,7 @@ public class FlowInputOutput {
|
||||
})
|
||||
.collect(Collectors.toMap(it -> it.get().input().getId(), Function.identity(), (o1, o2) -> o1, LinkedHashMap::new)));
|
||||
|
||||
resolvableInputMap.values().forEach(input -> resolveInputValue(input, execution, resolvableInputMap));
|
||||
resolvableInputMap.values().forEach(input -> resolveInputValue(input, flow, execution, resolvableInputMap));
|
||||
|
||||
return resolvableInputMap.values().stream().map(ResolvableInput::get).toList();
|
||||
}
|
||||
@@ -248,6 +251,7 @@ public class FlowInputOutput {
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private InputAndValue resolveInputValue(
|
||||
final @NotNull ResolvableInput resolvable,
|
||||
final Flow flow,
|
||||
final @NotNull Execution execution,
|
||||
final @NotNull Map<String, ResolvableInput> inputs) {
|
||||
|
||||
@@ -258,8 +262,8 @@ public class FlowInputOutput {
|
||||
|
||||
try {
|
||||
// resolve all input dependencies and check whether input is enabled
|
||||
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, execution, inputs);
|
||||
final RunContext runContext = buildRunContextForExecutionAndInputs(execution, dependencies);
|
||||
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, flow, execution, inputs);
|
||||
final RunContext runContext = buildRunContextForExecutionAndInputs(flow, execution, dependencies);
|
||||
|
||||
boolean isInputEnabled = dependencies.isEmpty() || dependencies.values().stream().allMatch(InputAndValue::enabled);
|
||||
|
||||
@@ -325,15 +329,15 @@ public class FlowInputOutput {
|
||||
return resolvable.get();
|
||||
}
|
||||
|
||||
private RunContext buildRunContextForExecutionAndInputs(Execution execution, Map<String, InputAndValue> dependencies) {
|
||||
private RunContext buildRunContextForExecutionAndInputs(final Flow flow, final Execution execution, Map<String, InputAndValue> dependencies) {
|
||||
Map<String, Object> flattenInputs = MapUtils.flattenToNestedMap(dependencies.entrySet()
|
||||
.stream()
|
||||
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue().value()), HashMap::putAll)
|
||||
);
|
||||
return runContextFactory.of(null, execution, vars -> vars.withInputs(flattenInputs));
|
||||
return runContextFactory.of(flow, execution, vars -> vars.withInputs(flattenInputs));
|
||||
}
|
||||
|
||||
private Map<String, InputAndValue> resolveAllDependentInputs(final Input<?> input, final Execution execution, final Map<String, ResolvableInput> inputs) {
|
||||
private Map<String, InputAndValue> resolveAllDependentInputs(final Input<?> input, final Flow flow, final Execution execution, final Map<String, ResolvableInput> inputs) {
|
||||
return Optional.ofNullable(input.getDependsOn())
|
||||
.map(DependsOn::inputs)
|
||||
.stream()
|
||||
@@ -341,7 +345,7 @@ public class FlowInputOutput {
|
||||
.filter(id -> !id.equals(input.getId()))
|
||||
.map(inputs::get)
|
||||
.filter(Objects::nonNull) // input may declare unknown or non-necessary dependencies. Let's ignore.
|
||||
.map(it -> resolveInputValue(it, execution, inputs))
|
||||
.map(it -> resolveInputValue(it, flow, execution, inputs))
|
||||
.collect(Collectors.toMap(it -> it.input().getId(), Function.identity()));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import io.kestra.core.utils.Await;
|
||||
import io.kestra.core.utils.Either;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.models.triggers.RecoverMissedSchedules;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.context.event.ApplicationEventPublisher;
|
||||
import io.micronaut.inject.qualifiers.Qualifiers;
|
||||
@@ -70,7 +70,8 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
private final QueueInterface<WorkerJob> workerTaskQueue;
|
||||
private final WorkerTriggerResultQueueInterface workerTriggerResultQueue;
|
||||
private final QueueInterface<ExecutionKilled> executionKilledQueue;
|
||||
@SuppressWarnings("rawtypes") private final Optional<QueueInterface> clusterEventQueue;
|
||||
@SuppressWarnings("rawtypes")
|
||||
private final Optional<QueueInterface> clusterEventQueue;
|
||||
protected final FlowListenersInterface flowListeners;
|
||||
private final RunContextFactory runContextFactory;
|
||||
private final RunContextInitializer runContextInitializer;
|
||||
@@ -408,6 +409,16 @@ public abstract class AbstractScheduler implements Scheduler, Service {
|
||||
private List<FlowWithTriggers> computeSchedulable(List<FlowWithSource> flows, List<Trigger> triggerContextsToEvaluate, ScheduleContextInterface scheduleContext) {
|
||||
List<String> flowToKeep = triggerContextsToEvaluate.stream().map(Trigger::getFlowId).toList();
|
||||
|
||||
triggerContextsToEvaluate.stream()
|
||||
.filter(trigger -> !flows.stream().map(FlowWithSource::uidWithoutRevision).toList().contains(Flow.uid(trigger)))
|
||||
.forEach(trigger -> {
|
||||
try {
|
||||
this.triggerState.delete(trigger);
|
||||
} catch (QueueException e) {
|
||||
log.error("Unable to delete the trigger: {}.{}.{}", trigger.getNamespace(), trigger.getFlowId(), trigger.getTriggerId(), e);
|
||||
}
|
||||
});
|
||||
|
||||
return flows
|
||||
.stream()
|
||||
.filter(flow -> flowToKeep.contains(flow.getId()))
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.models.triggers.TriggerContext;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -25,7 +26,10 @@ public interface SchedulerTriggerStateInterface {
|
||||
|
||||
Trigger update(Flow flow, AbstractTrigger abstractTrigger, ConditionContext conditionContext) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
* QueueException required for Kafka implementation
|
||||
*/
|
||||
void delete(Trigger trigger) throws QueueException;
|
||||
/**
|
||||
* Used by the JDBC implementation: find triggers in all tenants.
|
||||
*/
|
||||
|
||||
@@ -486,7 +486,7 @@ public class ExecutionService {
|
||||
return getFirstPausedTaskOr(execution, flow)
|
||||
.flatMap(task -> {
|
||||
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
|
||||
return Mono.just(flowInputOutput.resolveInputs(pauseTask.getOnResume(), execution, Map.of()));
|
||||
return Mono.just(flowInputOutput.resolveInputs(pauseTask.getOnResume(), flow, execution, Map.of()));
|
||||
} else {
|
||||
return Mono.just(Collections.emptyList());
|
||||
}
|
||||
@@ -507,7 +507,7 @@ public class ExecutionService {
|
||||
return getFirstPausedTaskOr(execution, flow)
|
||||
.flatMap(task -> {
|
||||
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
|
||||
return flowInputOutput.validateExecutionInputs(pauseTask.getOnResume(), execution, inputs);
|
||||
return flowInputOutput.validateExecutionInputs(pauseTask.getOnResume(), flow, execution, inputs);
|
||||
} else {
|
||||
return Mono.just(Collections.emptyList());
|
||||
}
|
||||
@@ -528,7 +528,7 @@ public class ExecutionService {
|
||||
return getFirstPausedTaskOr(execution, flow)
|
||||
.flatMap(task -> {
|
||||
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
|
||||
return flowInputOutput.readExecutionInputs(pauseTask.getOnResume(), execution, inputs);
|
||||
return flowInputOutput.readExecutionInputs(pauseTask.getOnResume(), flow, execution, inputs);
|
||||
} else {
|
||||
return Mono.just(Collections.<String, Object>emptyMap());
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ public class FlowService {
|
||||
}
|
||||
|
||||
// check if subflow is present in given namespace
|
||||
public void checkValidSubflows(Flow flow) {
|
||||
public void checkValidSubflows(Flow flow, String tenantId) {
|
||||
List<io.kestra.plugin.core.flow.Subflow> subFlows = ListUtils.emptyOnNull(flow.getTasks()).stream()
|
||||
.filter(io.kestra.plugin.core.flow.Subflow.class::isInstance)
|
||||
.map(io.kestra.plugin.core.flow.Subflow.class::cast)
|
||||
@@ -170,15 +170,23 @@ public class FlowService {
|
||||
Set<ConstraintViolation<?>> violations = new HashSet<>();
|
||||
|
||||
subFlows.forEach(subflow -> {
|
||||
Optional<Flow> optional = findById(flow.getTenantId(), subflow.getNamespace(), subflow.getFlowId());
|
||||
String regex = ".*\\{\\{.+}}.*"; // regex to check if string contains pebble
|
||||
String subflowId = subflow.getFlowId();
|
||||
String namespace = subflow.getNamespace();
|
||||
if (subflowId.matches(regex) || namespace.matches(regex)) {
|
||||
return;
|
||||
}
|
||||
Optional<Flow> optional = findById(tenantId, 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()) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import jakarta.annotation.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -54,9 +55,9 @@ public final class LabelService {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean containsAll(List<Label> labelsContainer, List<Label> labelsThatMustBeIncluded) {
|
||||
Map<String, String> labelsContainerMap = labelsContainer.stream().collect(HashMap::new, (m, label)-> m.put(label.key(), label.value()), HashMap::putAll);
|
||||
public static boolean containsAll(@Nullable List<Label> labelsContainer, @Nullable List<Label> labelsThatMustBeIncluded) {
|
||||
Map<String, String> labelsContainerMap = ListUtils.emptyOnNull(labelsContainer).stream().collect(HashMap::new, (m, label)-> m.put(label.key(), label.value()), HashMap::putAll);
|
||||
|
||||
return labelsThatMustBeIncluded.stream().allMatch(label -> Objects.equals(labelsContainerMap.get(label.key()), label.value()));
|
||||
return ListUtils.emptyOnNull(labelsThatMustBeIncluded).stream().allMatch(label -> Objects.equals(labelsContainerMap.get(label.key()), label.value()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.kestra.plugin.core.flow.Dag;
|
||||
import io.kestra.plugin.core.log.Log;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.hamcrest.Matchers;
|
||||
@@ -238,6 +239,15 @@ class JsonSchemaGeneratorTest {
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("beta").get("$beta"), is(true));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void requiredAreRemovedIfThereIsADefault() {
|
||||
Map<String, Object> generate = jsonSchemaGenerator.properties(Task.class, RequiredWithDefault.class);
|
||||
assertThat(generate, is(not(nullValue())));
|
||||
assertThat((List<String>) generate.get("required"), not(containsInAnyOrder("requiredWithDefault")));
|
||||
assertThat((List<String>) generate.get("required"), containsInAnyOrder("requiredWithNoDefault"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void dashboard() throws URISyntaxException {
|
||||
@@ -324,6 +334,7 @@ class JsonSchemaGeneratorTest {
|
||||
}
|
||||
|
||||
@Schema(title = "Test class")
|
||||
@Builder
|
||||
private static class TestClass {
|
||||
@Schema(title = "Test property")
|
||||
public String testProperty;
|
||||
@@ -360,4 +371,21 @@ class JsonSchemaGeneratorTest {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuperBuilder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@Plugin
|
||||
public static class RequiredWithDefault extends Task {
|
||||
@PluginProperty
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
private Property<TaskWithEnum.TestClass> requiredWithDefault = Property.of(TaskWithEnum.TestClass.builder().testProperty("test").build());
|
||||
|
||||
@PluginProperty
|
||||
@NotNull
|
||||
private Property<TaskWithEnum.TestClass> requiredWithNoDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class FlowInputOutputTest {
|
||||
Map<String, Object> data = Map.of("input1", "value1", "input2", "value2");
|
||||
|
||||
// When
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
|
||||
|
||||
// Then
|
||||
Assertions.assertEquals(
|
||||
@@ -98,7 +98,7 @@ class FlowInputOutputTest {
|
||||
Map<String, Object> data = Map.of("input1", "v1", "input2", "v2", "input3", "v3");
|
||||
|
||||
// When
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
|
||||
|
||||
// Then
|
||||
Assertions.assertEquals(
|
||||
@@ -132,7 +132,7 @@ class FlowInputOutputTest {
|
||||
Map<String, Object> data = Map.of("input1", "v1", "input2", "v2", "input3", "v3");
|
||||
|
||||
// When
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
|
||||
|
||||
// Then
|
||||
Assertions.assertEquals(
|
||||
@@ -162,7 +162,7 @@ class FlowInputOutputTest {
|
||||
Map<String, Object> data = Map.of("input1", "value1", "input2", "value2");
|
||||
|
||||
// When
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
|
||||
|
||||
// Then
|
||||
Assertions.assertEquals(
|
||||
@@ -191,7 +191,7 @@ class FlowInputOutputTest {
|
||||
Map<String, Object> data = Map.of("input1", "value1", "input2", "value2");
|
||||
|
||||
// When
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
|
||||
|
||||
// Then
|
||||
Assertions.assertEquals(2, values.size());
|
||||
@@ -211,7 +211,7 @@ class FlowInputOutputTest {
|
||||
Publisher<CompletedPart> data = Mono.just(new MemoryCompletedFileUpload("input", "input", "???".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
// When
|
||||
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), DEFAULT_TEST_EXECUTION, data).block();
|
||||
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, data).block();
|
||||
|
||||
// Then
|
||||
Assertions.assertNull(values.getFirst().exception());
|
||||
@@ -238,7 +238,7 @@ class FlowInputOutputTest {
|
||||
Map<String, Object> data = Map.of("input42", "foo");
|
||||
|
||||
// When
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
|
||||
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
|
||||
|
||||
// Then
|
||||
Assertions.assertEquals(
|
||||
|
||||
@@ -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()
|
||||
@@ -336,10 +337,29 @@ class FlowServiceTest {
|
||||
.build();
|
||||
|
||||
ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, () -> {
|
||||
flowService.checkValidSubflows(flow);
|
||||
flowService.checkValidSubflows(flow, null);
|
||||
});
|
||||
|
||||
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, null));
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,14 @@ import io.kestra.plugin.core.trigger.Schedule;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KestraTest
|
||||
class LabelServiceTest {
|
||||
@@ -65,4 +68,15 @@ class LabelServiceTest {
|
||||
assertThat(labels, hasSize(2));
|
||||
assertThat(labels, hasItems(new Label("key", "value"), new Label("scheduleLabel", "scheduleValue")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsAll() {
|
||||
assertFalse(LabelService.containsAll(null, List.of(new Label("key", "value"))));
|
||||
assertFalse(LabelService.containsAll(Collections.emptyList(), List.of(new Label("key", "value"))));
|
||||
assertFalse(LabelService.containsAll(List.of(new Label("key1", "value1")), List.of(new Label("key2", "value2"))));
|
||||
assertTrue(LabelService.containsAll(List.of(new Label("key", "value")), null));
|
||||
assertTrue(LabelService.containsAll(List.of(new Label("key", "value")), Collections.emptyList()));
|
||||
assertTrue(LabelService.containsAll(List.of(new Label("key1", "value1")), List.of(new Label("key1", "value1"))));
|
||||
assertTrue(LabelService.containsAll(List.of(new Label("key1", "value1"), new Label("key2", "value2")), List.of(new Label("key1", "value1"))));
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -437,6 +437,33 @@ class RequestTest {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
void basicAuthOld() throws Exception {
|
||||
try (
|
||||
ApplicationContext applicationContext = ApplicationContext.run();
|
||||
EmbeddedServer server = applicationContext.getBean(EmbeddedServer.class).start();
|
||||
) {
|
||||
Request task = Request.builder()
|
||||
.id(RequestTest.class.getSimpleName())
|
||||
.type(RequestTest.class.getName())
|
||||
.uri(Property.of(server.getURL().toString() + "/auth/basic"))
|
||||
.options(HttpConfiguration.builder()
|
||||
.basicAuthUser("John")
|
||||
.basicAuthPassword("p4ss")
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
RunContext runContext = TestsUtils.mockRunContext(this.runContextFactory, task, Map.of());
|
||||
|
||||
Request.Output output = task.run(runContext);
|
||||
|
||||
assertThat(output.getBody(), is("{\"hello\":\"John\"}"));
|
||||
assertThat(output.getCode(), is(200));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void bearerAuth() throws Exception {
|
||||
try (
|
||||
@@ -464,6 +491,7 @@ class RequestTest {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
static class MockController {
|
||||
@Get("/hello")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
id: request-basicauth-deprecated
|
||||
namespace: sanitycheck.plugin.core.http
|
||||
|
||||
tasks:
|
||||
- id: request
|
||||
type: io.kestra.plugin.core.http.Request
|
||||
uri: https://testpages.eviltester.com/styled/auth/basic-auth-results.html
|
||||
method: GET
|
||||
options:
|
||||
basicAuthUser: authorized
|
||||
basicAuthPassword: password001
|
||||
|
||||
- id: assert
|
||||
type: io.kestra.plugin.core.execution.Assert
|
||||
errorMessage: "Invalid response code {{ outputs.request.code }}"
|
||||
conditions:
|
||||
- "{{ outputs.request.code == 200 }}"
|
||||
19
core/src/test/resources/sanity-checks/request-basicauth.yaml
Normal file
19
core/src/test/resources/sanity-checks/request-basicauth.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
id: request-basicauth
|
||||
namespace: sanitycheck.plugin.core.http
|
||||
|
||||
tasks:
|
||||
- id: request
|
||||
type: io.kestra.plugin.core.http.Request
|
||||
uri: https://testpages.eviltester.com/styled/auth/basic-auth-results.html
|
||||
method: GET
|
||||
options:
|
||||
auth:
|
||||
type: BASIC
|
||||
username: authorized
|
||||
password: password001
|
||||
|
||||
- id: assert
|
||||
type: io.kestra.plugin.core.execution.Assert
|
||||
errorMessage: "Invalid response code {{ outputs.request.code }}"
|
||||
conditions:
|
||||
- "{{ outputs.request.code == 200 }}"
|
||||
@@ -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;
|
||||
@@ -29,7 +30,7 @@ set -e;
|
||||
###############################################################
|
||||
BASEDIR=$(dirname "$(readlink -f $0)")
|
||||
WORKING_DIR=/tmp/kestra-release-plugins-$(date +%s);
|
||||
PLUGIN_FILE="$BASEDIR/.plugins"
|
||||
PLUGIN_FILE="$BASEDIR/../.plugins"
|
||||
GIT_BRANCH=master
|
||||
|
||||
###############################################################
|
||||
@@ -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
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/bin/bash
|
||||
#===============================================================================
|
||||
# SCRIPT: tag-release-plugins.sh
|
||||
# SCRIPT: setversion-tag-plugins.sh
|
||||
#
|
||||
# DESCRIPTION:
|
||||
# This script can be used to update and tag plugins from a release branch .e.g., releases/v0.21.x.
|
||||
# By default, if no `GITHUB_PAT` environment variable exist, the script will attempt to clone GitHub repositories using SSH_KEY.
|
||||
#
|
||||
# USAGE: ./tag-release-plugins.sh [options]
|
||||
# USAGE: ./setversion-tag-plugins.sh [options]
|
||||
# OPTIONS:
|
||||
# --release-version <version> Specify the release version (required)
|
||||
# --dry-run Specify to run in DRY_RUN.
|
||||
@@ -15,10 +15,11 @@
|
||||
|
||||
# EXAMPLES:
|
||||
# To release all plugins:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0
|
||||
# ./setversion-tag-plugins.sh --release-version=0.20.0
|
||||
# To release a specific plugin:
|
||||
# ./tag-release-plugins.sh --release-version=0.20.0 plugin-kubernetes
|
||||
|
||||
# ./setversion-tag-plugins.sh --release-version=0.20.0 plugin-kubernetes
|
||||
# To release specific plugins from file:
|
||||
# ./setversion-tag-plugins.sh --release-version=0.20.0 --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
@@ -28,7 +29,7 @@ set -e;
|
||||
###############################################################
|
||||
BASEDIR=$(dirname "$(readlink -f $0)")
|
||||
WORKING_DIR=/tmp/kestra-release-plugins-$(date +%s);
|
||||
PLUGIN_FILE="$BASEDIR/.plugins"
|
||||
PLUGIN_FILE="$BASEDIR/../.plugins"
|
||||
|
||||
###############################################################
|
||||
# Functions
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
version=0.21.0-rc0-SNAPSHOT
|
||||
version=0.21.3
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS dashboards (
|
||||
"key" VARCHAR(250) NOT NULL PRIMARY KEY,
|
||||
"key" VARCHAR(250) NOT NULL PRIMARY KEY,
|
||||
"value" TEXT NOT NULL,
|
||||
"deleted" BOOL NOT NULL GENERATED ALWAYS AS (JQ_BOOLEAN("value", '.deleted')),
|
||||
"tenant_id" VARCHAR(250) NOT NULL GENERATED ALWAYS AS (JQ_STRING("value", '.tenantId')),
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.models.triggers.TriggerContext;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.schedulers.ScheduleContextInterface;
|
||||
import io.kestra.core.schedulers.SchedulerTriggerStateInterface;
|
||||
import io.kestra.jdbc.repository.AbstractJdbcTriggerRepository;
|
||||
@@ -77,6 +78,10 @@ public class JdbcSchedulerTriggerState implements SchedulerTriggerStateInterface
|
||||
return this.triggerRepository.update(flow, abstractTrigger, conditionContext);
|
||||
}
|
||||
|
||||
public void delete(Trigger trigger) throws QueueException {
|
||||
this.triggerRepository.delete(trigger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Trigger> findByNextExecutionDateReadyForAllTenants(ZonedDateTime now, ScheduleContextInterface scheduleContext) {
|
||||
return this.triggerRepository.findByNextExecutionDateReadyForAllTenants(now, scheduleContext);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.jdbc.runner;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.conditions.ConditionContext;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
@@ -11,14 +12,7 @@ import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.queues.QueueFactoryInterface;
|
||||
import io.kestra.core.queues.QueueInterface;
|
||||
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
||||
import io.kestra.core.runners.RunContextFactory;
|
||||
import io.kestra.core.runners.StandAloneRunner;
|
||||
import io.kestra.core.runners.Worker;
|
||||
import io.kestra.core.runners.WorkerJob;
|
||||
import io.kestra.core.runners.WorkerTask;
|
||||
import io.kestra.core.runners.WorkerTaskResult;
|
||||
import io.kestra.core.runners.WorkerTrigger;
|
||||
import io.kestra.core.runners.WorkerTriggerResult;
|
||||
import io.kestra.core.runners.*;
|
||||
import io.kestra.core.services.SkipExecutionService;
|
||||
import io.kestra.core.tasks.test.SleepTrigger;
|
||||
import io.kestra.core.utils.Await;
|
||||
@@ -28,7 +22,6 @@ import io.kestra.jdbc.JdbcTestUtils;
|
||||
import io.kestra.plugin.core.flow.Sleep;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.context.annotation.Property;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
@@ -157,7 +150,7 @@ public abstract class JdbcServiceLivenessCoordinatorTest {
|
||||
});
|
||||
|
||||
workerJobQueue.emit(workerTask);
|
||||
boolean runningLatchAwait = runningLatch.await(2, TimeUnit.SECONDS);
|
||||
boolean runningLatchAwait = runningLatch.await(10, TimeUnit.SECONDS);
|
||||
assertThat(runningLatchAwait, is(true));
|
||||
worker.shutdown();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -336,6 +336,8 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
|
||||
|
||||
@Override
|
||||
public TaskRunnerResult<DockerTaskRunnerDetailResult> run(RunContext runContext, TaskCommands taskCommands, List<String> filesToDownload) throws Exception {
|
||||
Boolean renderedDelete = runContext.render(delete).as(Boolean.class).orElseThrow();
|
||||
|
||||
if (taskCommands.getContainerImage() == null && this.image == null) {
|
||||
throw new IllegalArgumentException("This task runner needs the `containerImage` property to be set");
|
||||
}
|
||||
@@ -535,7 +537,7 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
|
||||
// come to a normal end.
|
||||
kill();
|
||||
|
||||
if (Boolean.TRUE.equals(runContext.render(delete).as(Boolean.class).orElseThrow())) {
|
||||
if (Boolean.TRUE.equals(renderedDelete)) {
|
||||
dockerClient.removeContainerCmd(exec.getId()).exec();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Container deleted: {}", exec.getId());
|
||||
|
||||
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",
|
||||
|
||||
28
ui/scripts/product/flow.ts
Normal file
28
ui/scripts/product/flow.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
const MINIMUM: number = 100000;
|
||||
const MAXIMUM: number = 999999;
|
||||
const ANIMALS: string[] = [
|
||||
"Aardvark", "Albatross", "Alligator", "Alpaca", "Ant", "Anteater", "Antelope", "Ape", "Armadillo", "Baboon",
|
||||
"Badger", "Barracuda", "Bat", "Bear", "Beaver", "Bee", "Bison", "Boar", "Buffalo", "Butterfly", "Camel", "Capybara",
|
||||
"Caribou", "Cassowary", "Cat", "Caterpillar", "Cattle", "Chamois", "Cheetah", "Chicken", "Chimpanzee", "Chinchilla",
|
||||
"Chough", "Clam", "Cobra", "Cockroach", "Cod", "Cormorant", "Coyote", "Crab", "Crane", "Crocodile", "Crow", "Curlew",
|
||||
"Deer", "Dinosaur", "Dog", "Dogfish", "Dolphin", "Dotterel", "Dove", "Dragonfly", "Duck", "Dugong", "Dunlin", "Eagle",
|
||||
"Echidna", "Eel", "Eland", "Elephant", "Elk", "Emu", "Falcon", "Ferret", "Finch", "Fish", "Flamingo", "Fly", "Fox",
|
||||
"Frog", "Gaur", "Gazelle", "Gerbil", "Giraffe", "Gnat", "Gnu", "Goat", "Goldfinch", "Goldfish", "Goose", "Gorilla",
|
||||
"Goshawk", "Grasshopper", "Grouse", "Guanaco", "Gull", "Hamster", "Hare", "Hawk", "Hedgehog", "Heron", "Herring",
|
||||
"Hippopotamus", "Hornet", "Horse", "Human", "Hummingbird", "Hyena", "Ibex", "Ibis", "Jackal", "Jaguar", "Jay",
|
||||
"Jellyfish", "Kangaroo", "Kingfisher", "Koala", "Kookabura", "Kouprey", "Kudu", "Lapwing", "Lark", "Lemur", "Leopard",
|
||||
"Lion", "Llama", "Lobster", "Locust", "Loris", "Louse", "Lyrebird", "Magpie", "Mallard", "Manatee", "Mandrill", "Mantis",
|
||||
"Marten", "Meerkat", "Mink", "Mole", "Mongoose", "Monkey", "Moose", "Mosquito", "Mouse", "Mule", "Narwhal", "Newt",
|
||||
"Nightingale", "Octopus", "Okapi", "Opossum", "Oryx", "Ostrich", "Otter", "Owl", "Oyster", "Panther", "Parrot", "Partridge",
|
||||
"Peafowl", "Pelican", "Penguin", "Pheasant", "Pigeon", "Pony", "Porcupine", "Porpoise", "Quail", "Quelea", "Quetzal",
|
||||
"Rabbit", "Rail", "Ram", "Rat", "Raven", "Rhinoceros", "Rook", "Salamander", "Salmon", "Sandpiper", "Sardine", "Scorpion",
|
||||
"Seahorse", "Seal", "Shark", "Shrew", "Skunk", "Snail", "Snake", "Sparrow", "Spider", "Spoonbill", "Squid", "Squirrel",
|
||||
"Starling", "Stingray", "Stinkbug", "Stork", "Swallow", "Swan", "Tapir", "Tarsier", "Termite", "Tiger", "Toad",
|
||||
"Trout", "Turkey", "Turtle", "Viper", "Vulture", "Wallaby", "Walrus", "Wasp", "Weasel", "Whale", "Wildcat", "Wolf",
|
||||
"Wolverine", "Wombat", "Woodcock", "Woodpecker", "Worm", "Wren", "Yak", "Zebra"
|
||||
];
|
||||
|
||||
const getRandomNumber = (minimum: number = MINIMUM, maximum: number = MAXIMUM): number => Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
|
||||
const getRandomAnimal = (): string => ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
|
||||
|
||||
export const getRandomFlowID = (): string => `${getRandomAnimal()}_${getRandomNumber()}`.toLowerCase();
|
||||
@@ -105,7 +105,7 @@
|
||||
},
|
||||
methods: {
|
||||
displayApp() {
|
||||
Utils.switchTheme();
|
||||
Utils.switchTheme(this.$store);
|
||||
|
||||
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};
|
||||
|
||||
@@ -194,14 +194,8 @@
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :lg="props.flow ? 7 : 12">
|
||||
<ExecutionsDoughnut
|
||||
v-if="props.flow"
|
||||
:data="graphData"
|
||||
:total="stats.total"
|
||||
class="ms-2"
|
||||
/>
|
||||
<ExecutionsNextScheduled
|
||||
v-else-if="isAllowedTriggers"
|
||||
v-if="isAllowedTriggers"
|
||||
:flow="props.flowId"
|
||||
:namespace="props.namespace"
|
||||
class="ms-2"
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
<div
|
||||
class="d-flex justify-content-end align-items-center switch-content"
|
||||
>
|
||||
<span class="pe-2 fw-light small">{{ t("duration") }}</span>
|
||||
<el-switch
|
||||
v-model="duration"
|
||||
:active-icon="Check"
|
||||
inline-prompt
|
||||
/>
|
||||
<span class="d-flex align-items-center ps-2 fw-light small">{{ t("duration") }}</span>
|
||||
</div>
|
||||
<div id="executions" />
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
import {barLegend} from "../legend.js";
|
||||
|
||||
import Utils from "../../../../../utils/utils.js";
|
||||
import Utils, {useTheme} from "../../../../../utils/utils.js";
|
||||
import {defaultConfig, getFormat} from "../../../../../utils/charts.js";
|
||||
import {getScheme} from "../../../../../utils/scheme.js";
|
||||
|
||||
@@ -73,13 +73,14 @@
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme()
|
||||
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: getScheme(state),
|
||||
backgroundColor: getScheme(theme.value, state),
|
||||
yAxisID: "y",
|
||||
data: [],
|
||||
};
|
||||
@@ -286,9 +287,5 @@ $height: 200px;
|
||||
.small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.pe-2 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -247,6 +255,7 @@
|
||||
|
||||
<el-table-column
|
||||
prop="flowRevision"
|
||||
v-if="displayColumn('revision')"
|
||||
:label="$t('revision')"
|
||||
class-name="shrink"
|
||||
>
|
||||
@@ -291,7 +300,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="action" class-name="row-action">
|
||||
<el-table-column
|
||||
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,57 +473,52 @@
|
||||
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",
|
||||
prop: "state.current",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "triggers",
|
||||
prop: "triggers",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "labels",
|
||||
prop: "labels",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "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("labels"),
|
||||
prop: "labels",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: this.$t("state"),
|
||||
prop: "state.current",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: this.$t("revision"),
|
||||
prop: "flowRevision",
|
||||
default: false
|
||||
},
|
||||
{
|
||||
label: "task id",
|
||||
label: this.$t("inputs"),
|
||||
prop: "inputs",
|
||||
default: false
|
||||
},
|
||||
{
|
||||
label: this.$t("task id"),
|
||||
prop: "taskRunList.taskId",
|
||||
default: false
|
||||
}
|
||||
@@ -532,7 +540,7 @@
|
||||
this.storageKey = storageKeys.DISPLAY_FLOW_EXECUTIONS_COLUMNS;
|
||||
this.optionalColumns = this.optionalColumns.filter(col => col.prop !== "namespace" && col.prop !== "flowId")
|
||||
}
|
||||
this.displayColumns = localStorage.getItem(this.storageKey)?.split(",")
|
||||
this.displayColumns = localStorage.getItem("columns_executions")?.split(",")
|
||||
|| this.optionalColumns.filter(col => col.default).map(col => col.prop);
|
||||
if (this.isConcurrency) {
|
||||
this.emitStateCount([State.RUNNING, State.PAUSED])
|
||||
@@ -645,6 +653,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);
|
||||
@@ -834,18 +845,18 @@
|
||||
h(ElSwitch, {
|
||||
modelValue: includeNonTerminated.value,
|
||||
"onUpdate:modelValue": (val) => {
|
||||
includeNonTerminated.value = val
|
||||
includeNonTerminated.value = val;
|
||||
},
|
||||
}),
|
||||
]),
|
||||
h(ElAlert, {
|
||||
title: this.$t("execution-warn-title"),
|
||||
includeNonTerminated.value ? h(ElAlert, {
|
||||
title: this.$t("execution-warn-title"),
|
||||
description: this.$t("execution-warn-deleting-still-running"),
|
||||
type: "warning",
|
||||
showIcon: true,
|
||||
closable: false,
|
||||
class: "custom-warning"
|
||||
}),
|
||||
}) : null,
|
||||
h(ElCheckbox, {
|
||||
modelValue: deleteLogs.value,
|
||||
label: this.$t("execution_deletion.logs"),
|
||||
@@ -876,7 +887,7 @@
|
||||
"execution/queryDeleteExecution",
|
||||
"execution/bulkDeleteExecution",
|
||||
"executions deleted"
|
||||
)
|
||||
);
|
||||
});
|
||||
},
|
||||
killExecutions() {
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<duration :histories="scope.row.value" />
|
||||
</span>
|
||||
<span v-else-if="scope.row.key === $t('labels')">
|
||||
<labels :labels="scope.row.value" :filter-enabled="false" />
|
||||
<labels :labels="scope.row.value" read-only />
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-if="scope.row.key === $t('revision')">
|
||||
|
||||
@@ -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,67 @@
|
||||
break;
|
||||
}
|
||||
};
|
||||
const current = ref<CurrentItem[]>([]);
|
||||
const currentFilters = ref<CurrentItem[]>([]);
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(q: any) => {
|
||||
// Handling change of label filters from direct click events
|
||||
const routeFilters = decodeParams(route.path, q, props.include, OPTIONS);
|
||||
currentFilters.value = routeFilters;
|
||||
},
|
||||
{immediate: true},
|
||||
);
|
||||
|
||||
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 +644,7 @@
|
||||
};
|
||||
|
||||
const removeItem = (value) => {
|
||||
current.value = current.value.filter(
|
||||
currentFilters.value = currentFilters.value.filter(
|
||||
(item) => JSON.stringify(item) !== JSON.stringify(value),
|
||||
);
|
||||
|
||||
@@ -591,22 +652,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,21 +694,21 @@
|
||||
|
||||
const addNamespaceFilter = (namespace) => {
|
||||
if (!props.decode || !namespace) return;
|
||||
current.value.push({
|
||||
currentFilters.value.push({
|
||||
label: "namespace",
|
||||
value: [namespace],
|
||||
comparator: COMPARATORS.STARTS_WITH,
|
||||
persistent: true,
|
||||
});
|
||||
};
|
||||
const {name, params} = route;
|
||||
const {name, params, query} = route;
|
||||
|
||||
if (name === "flows/update") {
|
||||
// Single flow page
|
||||
addNamespaceFilter(params?.namespace);
|
||||
|
||||
if (props.decode && params.id) {
|
||||
current.value.push({
|
||||
currentFilters.value.push({
|
||||
label: "flow",
|
||||
value: [`${params.id}`],
|
||||
comparator: COMPARATORS.IS,
|
||||
@@ -659,6 +718,24 @@
|
||||
} else if (name === "namespaces/update") {
|
||||
// Single namespace page
|
||||
addNamespaceFilter(params.id);
|
||||
} else if (name === "admin/triggers") {
|
||||
if(query.namespace) addNamespaceFilter(query.namespace);
|
||||
if(query.flowId){
|
||||
currentFilters.value.push({
|
||||
label: "flow",
|
||||
value: [`${query.flowId}`],
|
||||
comparator: COMPARATORS.EQUALS,
|
||||
persistent: true,
|
||||
});
|
||||
}
|
||||
if(query.q) {
|
||||
currentFilters.value.push({
|
||||
label: "text",
|
||||
value: [`${query.q}`],
|
||||
comparator: COMPARATORS.EQUALS,
|
||||
persistent: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -675,12 +752,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 +818,7 @@
|
||||
const label = labelElement?.textContent;
|
||||
|
||||
if (label) {
|
||||
const existingFilterIndex = current.value.findIndex(
|
||||
const existingFilterIndex = currentFilters.value.findIndex(
|
||||
(item) =>
|
||||
item?.label.toLowerCase() ===
|
||||
label
|
||||
@@ -757,7 +834,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 +881,7 @@ $included: 144px;
|
||||
$refresh: 104px;
|
||||
$settins: 52px;
|
||||
$dashboards: 52px;
|
||||
$properties: v-bind('props.propertiesWidth + "px"');
|
||||
|
||||
.filters {
|
||||
@include width-available;
|
||||
@@ -808,6 +889,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 +910,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 +935,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 +977,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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user