mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 14:00:23 -05:00
Compare commits
78 Commits
dependabot
...
v0.19.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b29965c239 | ||
|
|
05d1eeadef | ||
|
|
acd2ce9041 | ||
|
|
a3829c3d7e | ||
|
|
17c18f94dd | ||
|
|
14daa96295 | ||
|
|
aa9aa80f0a | ||
|
|
705d17340d | ||
|
|
cf70c99e59 | ||
|
|
c5e0cddca5 | ||
|
|
4d14464191 | ||
|
|
ed12797b46 | ||
|
|
ec85a748ce | ||
|
|
3e8a63888a | ||
|
|
8d0bcc1da3 | ||
|
|
0b53f1cf25 | ||
|
|
3621aad6a1 | ||
|
|
dbb1cc5007 | ||
|
|
0d6e655b22 | ||
|
|
7a1a180fdb | ||
|
|
ce2daf52ff | ||
|
|
f086da3a2a | ||
|
|
1886a443c7 | ||
|
|
5a4e2b791d | ||
|
|
a595cecb3d | ||
|
|
472b699ca7 | ||
|
|
f55f52b43a | ||
|
|
c796308839 | ||
|
|
37a880164d | ||
|
|
5f1408c560 | ||
|
|
4186900fdb | ||
|
|
4338437a6f | ||
|
|
68ee5e4df0 | ||
|
|
2def5cf7f8 | ||
|
|
d184858abf | ||
|
|
dfa5875fa1 | ||
|
|
ac4f7f261d | ||
|
|
ae55685d2e | ||
|
|
dd34317e4f | ||
|
|
f95e3073dd | ||
|
|
9f20988997 | ||
|
|
5da3ab4f71 | ||
|
|
243eaab826 | ||
|
|
6d362d688d | ||
|
|
39a01e0e7d | ||
|
|
a44b2ef7cb | ||
|
|
6bcad13444 | ||
|
|
02acf01ea5 | ||
|
|
55193361b8 | ||
|
|
8d509a3ba5 | ||
|
|
500680bcf7 | ||
|
|
412c27cb12 | ||
|
|
8d7d9a356f | ||
|
|
d2ab2e97b4 | ||
|
|
6a0f360fc6 | ||
|
|
0484fd389a | ||
|
|
e92aac3b39 | ||
|
|
39b8ac8804 | ||
|
|
f928ed5876 | ||
|
|
54856af0a8 | ||
|
|
8bd79e82ab | ||
|
|
104a491b92 | ||
|
|
5f46a0dd16 | ||
|
|
24c3703418 | ||
|
|
e5af245855 | ||
|
|
d58e8f98a2 | ||
|
|
ce2f1bfdb3 | ||
|
|
b619f88eff | ||
|
|
1f1775752b | ||
|
|
b2475e53a2 | ||
|
|
7e8956a0b7 | ||
|
|
6537ee984b | ||
|
|
573aa48237 | ||
|
|
66ddeaa219 | ||
|
|
02c5e8a1a2 | ||
|
|
733c7897b9 | ||
|
|
c051287688 | ||
|
|
1af8de6bce |
19
.github/CONTRIBUTING.md
vendored
19
.github/CONTRIBUTING.md
vendored
@@ -52,14 +52,17 @@ The backend is made with [Micronaut](https://micronaut.io).
|
|||||||
Open the cloned repository in your favorite IDE. In most of decent IDEs, Gradle build will be detected and all dependencies will be downloaded.
|
Open the cloned repository in your favorite IDE. In most of decent IDEs, Gradle build will be detected and all dependencies will be downloaded.
|
||||||
You can also build it from a terminal using `./gradlew build`, the Gradle wrapper will download the right Gradle version to use.
|
You can also build it from a terminal using `./gradlew build`, the Gradle wrapper will download the right Gradle version to use.
|
||||||
|
|
||||||
- You may need to enable java annotation processors since we are using it a lot.
|
- You may need to enable java annotation processors since we are using them.
|
||||||
- The main class is `io.kestra.cli.App` from module `kestra.cli.main`
|
- On IntelliJ IDEA, click on **Run -> Edit Configurations -> + Add new Configuration** to create a run configuration to start Kestra.
|
||||||
- Pass as program arguments the server you want to develop, for example `server local` will start the [standalone local](https://kestra.io/docs/administrator-guide/server-cli#kestra-local-development-server-with-no-dependencies)
|
- The main class is `io.kestra.cli.App` from module `kestra.cli.main`.
|
||||||
-  Intellij Idea configuration can be found in screenshot below.
|
- Pass as program arguments the server you want to work with, for example `server local` will start the [standalone local](https://kestra.io/docs/administrator-guide/server-cli#kestra-local-development-server-with-no-dependencies). You can also use `server standalone` and use the provided `docker-compose-ci.yml` Docker compose file to start a standalone server with a real database as a backend that would need to be configured properly.
|
||||||
- `MICRONAUT_ENVIRONMENTS`: can be set any string and will load a custom configuration file in `cli/src/main/resources/application-{env}.yml`
|
- Configure the following environment variables:
|
||||||
- `KESTRA_PLUGINS_PATH`: is the path where you will save plugins as Jar and will be load on the startup.
|
- `MICRONAUT_ENVIRONMENTS`: can be set to any string and will load a custom configuration file in `cli/src/main/resources/application-{env}.yml`.
|
||||||
- You can also use the gradle task `./gradlew runLocal` that will run a standalone server with `MICRONAUT_ENVIRONMENTS=override` and plugins path `local/plugins`
|
- `KESTRA_PLUGINS_PATH`: is the path where you will save plugins as Jar and will be load on startup.
|
||||||
- The server start by default on port 8080 and is reachable on `http://localhost:8080`
|
- See the screenshot bellow for an example: 
|
||||||
|
- If you encounter **JavaScript memory heap out** error during startup, configure `NODE_OPTIONS` environment variable with some large value.
|
||||||
|
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` 
|
||||||
|
- The server starts by default on port 8080 and is reachable on `http://localhost:8080`
|
||||||
|
|
||||||
If you want to launch all tests, you need Python and some packages installed on your machine, on Ubuntu you can install them with:
|
If you want to launch all tests, you need Python and some packages installed on your machine, on Ubuntu you can install them with:
|
||||||
|
|
||||||
|
|||||||
BIN
.github/node_option_env_var.png
vendored
Normal file
BIN
.github/node_option_env_var.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
122
.github/workflows/generate_translations.yml
vendored
122
.github/workflows/generate_translations.yml
vendored
@@ -1,45 +1,111 @@
|
|||||||
name: Generate Translations
|
name: Generate Translations
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
paths:
|
||||||
|
- "ui/src/translations/en.json"
|
||||||
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
paths:
|
|
||||||
- 'ui/src/translations/en.json'
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-translations:
|
commit:
|
||||||
name: Generate Translations and Create PR
|
name: Commit directly to PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.pull_request.head.repo.fork == false }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 10 # Ensures that at least 10 commits are fetched for comparison
|
fetch-depth: 10
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: "3.x"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install Python dependencies
|
||||||
run: pip install gitpython openai
|
run: pip install gitpython openai
|
||||||
|
|
||||||
- name: Generate translations
|
- name: Generate translations
|
||||||
run: python ui/src/translations/generate_translations.py
|
run: python ui/src/translations/generate_translations.py
|
||||||
|
|
||||||
- name: Commit, push changes, and create PR
|
- name: Set up Node
|
||||||
env:
|
uses: actions/setup-node@v4
|
||||||
GH_TOKEN: ${{ github.token }}
|
with:
|
||||||
run: |
|
node-version: "20.x"
|
||||||
git config --global user.name "GitHub Action"
|
|
||||||
git config --global user.email "actions@github.com"
|
- name: Check keys matching
|
||||||
BRANCH_NAME="translations/update-translations-$(date +%s)"
|
run: node ui/src/translations/check.js
|
||||||
git checkout -b $BRANCH_NAME
|
|
||||||
git add ui/src/translations/*.json
|
- name: Set up Git
|
||||||
git commit -m "Auto-generate translations from en.json"
|
run: |
|
||||||
git push --set-upstream origin $BRANCH_NAME
|
git config --global user.name "GitHub Action"
|
||||||
gh pr create --title "Auto-generate translations from en.json" --body "This PR was created automatically by a GitHub Action." --base develop --head $BRANCH_NAME --assignee anna-geller --reviewer anna-geller
|
git config --global user.email "actions@github.com"
|
||||||
|
|
||||||
|
- name: Check for changes and commit
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
git add ui/src/translations/*.json
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit. Exiting with success."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "chore(translations): auto generate values for languages other than english"
|
||||||
|
git push origin ${{ github.head_ref }}
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
name: Open PR for a forked repository
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.pull_request.head.repo.fork == true }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 10
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: pip install gitpython openai
|
||||||
|
|
||||||
|
- name: Generate translations
|
||||||
|
run: python ui/src/translations/generate_translations.py
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20.x"
|
||||||
|
|
||||||
|
- name: Check keys matching
|
||||||
|
run: node ui/src/translations/check.js
|
||||||
|
|
||||||
|
- name: Set up Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "GitHub Action"
|
||||||
|
git config --global user.email "actions@github.com"
|
||||||
|
|
||||||
|
- name: Create and push a new branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
BRANCH_NAME="generated-translations-${{ github.event.pull_request.head.repo.name }}"
|
||||||
|
|
||||||
|
git checkout -b $BRANCH_NAME
|
||||||
|
git add ui/src/translations/*.json
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit. Exiting with success."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "chore(translations): auto generate values for languages other than english"
|
||||||
|
git push origin $BRANCH_NAME
|
||||||
|
|||||||
18
.github/workflows/main.yml
vendored
18
.github/workflows/main.yml
vendored
@@ -68,6 +68,7 @@ jobs:
|
|||||||
# Get Plugins List
|
# Get Plugins List
|
||||||
- name: Get Plugins List
|
- name: Get Plugins List
|
||||||
uses: ./.github/actions/plugins-list
|
uses: ./.github/actions/plugins-list
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
id: plugins-list
|
id: plugins-list
|
||||||
with:
|
with:
|
||||||
plugin-version: ${{ env.PLUGIN_VERSION }}
|
plugin-version: ${{ env.PLUGIN_VERSION }}
|
||||||
@@ -75,6 +76,7 @@ jobs:
|
|||||||
# Set Plugins List
|
# Set Plugins List
|
||||||
- name: Set Plugin List
|
- name: Set Plugin List
|
||||||
id: plugins
|
id: plugins
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
run: |
|
run: |
|
||||||
PLUGINS="${{ steps.plugins-list.outputs.plugins }}"
|
PLUGINS="${{ steps.plugins-list.outputs.plugins }}"
|
||||||
TAG=${GITHUB_REF#refs/*/}
|
TAG=${GITHUB_REF#refs/*/}
|
||||||
@@ -122,6 +124,7 @@ jobs:
|
|||||||
# Docker Build
|
# Docker Build
|
||||||
- name: Build & Export Docker Image
|
- name: Build & Export Docker Image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
@@ -149,6 +152,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker
|
- name: Upload Docker
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.vars.outputs.artifact }}
|
name: ${{ steps.vars.outputs.artifact }}
|
||||||
path: /tmp/${{ steps.vars.outputs.artifact }}.tar
|
path: /tmp/${{ steps.vars.outputs.artifact }}.tar
|
||||||
@@ -156,7 +160,7 @@ jobs:
|
|||||||
check-e2e:
|
check-e2e:
|
||||||
name: Check E2E Tests
|
name: Check E2E Tests
|
||||||
needs: build-artifacts
|
needs: build-artifacts
|
||||||
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
|
if: ${{ (github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '') && !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
uses: ./.github/workflows/e2e.yml
|
uses: ./.github/workflows/e2e.yml
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -276,7 +280,11 @@ jobs:
|
|||||||
name: Github Release
|
name: Github Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ check, check-e2e ]
|
needs: [ check, check-e2e ]
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: |
|
||||||
|
always() &&
|
||||||
|
startsWith(github.ref, 'refs/tags/v') &&
|
||||||
|
needs.check.result == 'success' &&
|
||||||
|
(needs.check-e2e.result == 'skipped' || needs.check-e2e.result == 'success')
|
||||||
steps:
|
steps:
|
||||||
# Download Exec
|
# Download Exec
|
||||||
- name: Download executable
|
- name: Download executable
|
||||||
@@ -368,7 +376,11 @@ jobs:
|
|||||||
name: Publish to Maven
|
name: Publish to Maven
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [check, check-e2e]
|
needs: [check, check-e2e]
|
||||||
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')
|
if: |
|
||||||
|
always() &&
|
||||||
|
github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v') &&
|
||||||
|
needs.check.result == 'success' &&
|
||||||
|
(needs.check-e2e.result == 'skipped' || needs.check-e2e.result == 'success')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ kestra:
|
|||||||
delay: 1s
|
delay: 1s
|
||||||
maxDelay: ""
|
maxDelay: ""
|
||||||
|
|
||||||
|
jdbc:
|
||||||
queues:
|
queues:
|
||||||
min-poll-interval: 25ms
|
min-poll-interval: 25ms
|
||||||
max-poll-interval: 1000ms
|
max-poll-interval: 1000ms
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package io.kestra.core.models.collectors;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.kestra.core.repositories.ServiceInstanceRepositoryInterface;
|
||||||
|
import io.kestra.core.server.Service;
|
||||||
|
import io.kestra.core.server.ServiceInstance;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.LongSummaryStatistics;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics about the number of running services over a given period.
|
||||||
|
*/
|
||||||
|
public record ServiceUsage(
|
||||||
|
List<DailyServiceStatistics> dailyStatistics
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily statistics for a specific service type.
|
||||||
|
*
|
||||||
|
* @param type The service type.
|
||||||
|
* @param values The statistic values.
|
||||||
|
*/
|
||||||
|
public record DailyServiceStatistics(
|
||||||
|
String type,
|
||||||
|
List<DailyStatistics> values
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics about the number of services running at any given time interval (e.g., 15 minutes) over a day.
|
||||||
|
*
|
||||||
|
* @param date The {@link LocalDate}.
|
||||||
|
* @param min The minimum number of services.
|
||||||
|
* @param max The maximum number of services.
|
||||||
|
* @param avg The average number of services.
|
||||||
|
*/
|
||||||
|
public record DailyStatistics(
|
||||||
|
LocalDate date,
|
||||||
|
long min,
|
||||||
|
long max,
|
||||||
|
long avg
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
public static ServiceUsage of(final Instant from,
|
||||||
|
final Instant to,
|
||||||
|
final ServiceInstanceRepositoryInterface repository,
|
||||||
|
final Duration interval) {
|
||||||
|
|
||||||
|
List<DailyServiceStatistics> statistics = Arrays
|
||||||
|
.stream(Service.ServiceType.values())
|
||||||
|
.map(type -> of(from, to, repository, type, interval))
|
||||||
|
.toList();
|
||||||
|
return new ServiceUsage(statistics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DailyServiceStatistics of(final Instant from,
|
||||||
|
final Instant to,
|
||||||
|
final ServiceInstanceRepositoryInterface repository,
|
||||||
|
final Service.ServiceType serviceType,
|
||||||
|
final Duration interval) {
|
||||||
|
return of(serviceType, interval, repository.findAllInstancesBetween(serviceType, from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static DailyServiceStatistics of(final Service.ServiceType serviceType,
|
||||||
|
final Duration interval,
|
||||||
|
final List<ServiceInstance> instances) {
|
||||||
|
// Compute the number of running service per time-interval.
|
||||||
|
final long timeIntervalInMillis = interval.toMillis();
|
||||||
|
|
||||||
|
final Map<Long, Long> aggregatePerTimeIntervals = instances
|
||||||
|
.stream()
|
||||||
|
.flatMap(instance -> {
|
||||||
|
List<ServiceInstance.TimestampedEvent> events = instance.events();
|
||||||
|
long start = 0;
|
||||||
|
long end = 0;
|
||||||
|
for (ServiceInstance.TimestampedEvent event : events) {
|
||||||
|
long epochMilli = event.ts().toEpochMilli();
|
||||||
|
if (event.state().equals(Service.ServiceState.RUNNING)) {
|
||||||
|
start = epochMilli;
|
||||||
|
}
|
||||||
|
else if (event.state().equals(Service.ServiceState.NOT_RUNNING) && end == 0) {
|
||||||
|
end = epochMilli;
|
||||||
|
}
|
||||||
|
else if (event.state().equals(Service.ServiceState.TERMINATED_GRACEFULLY)) {
|
||||||
|
end = epochMilli; // more precise than NOT_RUNNING
|
||||||
|
}
|
||||||
|
else if (event.state().equals(Service.ServiceState.TERMINATED_FORCED)) {
|
||||||
|
end = epochMilli; // more precise than NOT_RUNNING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.state().equals(Service.ServiceState.RUNNING)) {
|
||||||
|
end = Instant.now().toEpochMilli();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start != 0 && end != 0) {
|
||||||
|
// align to epoch-time by removing precision.
|
||||||
|
start = (start / timeIntervalInMillis) * timeIntervalInMillis;
|
||||||
|
|
||||||
|
// approximate the number of time interval for the current service
|
||||||
|
int intervals = (int) ((end - start) / timeIntervalInMillis);
|
||||||
|
|
||||||
|
// compute all time intervals
|
||||||
|
List<Long> keys = new ArrayList<>(intervals);
|
||||||
|
while (start < end) {
|
||||||
|
keys.add(start);
|
||||||
|
start = start + timeIntervalInMillis; // Next window
|
||||||
|
}
|
||||||
|
return keys.stream();
|
||||||
|
}
|
||||||
|
return Stream.empty(); // invalid service
|
||||||
|
})
|
||||||
|
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
|
||||||
|
|
||||||
|
// Aggregate per day
|
||||||
|
List<DailyStatistics> dailyStatistics = aggregatePerTimeIntervals.entrySet()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(entry -> {
|
||||||
|
Long epochTimeMilli = entry.getKey();
|
||||||
|
return Instant.ofEpochMilli(epochTimeMilli).atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
}, Collectors.toList()))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> {
|
||||||
|
LongSummaryStatistics statistics = entry.getValue().stream().collect(Collectors.summarizingLong(Map.Entry::getValue));
|
||||||
|
return new DailyStatistics(
|
||||||
|
entry.getKey(),
|
||||||
|
statistics.getMin(),
|
||||||
|
statistics.getMax(),
|
||||||
|
BigDecimal.valueOf(statistics.getAverage()).setScale(2, RoundingMode.HALF_EVEN).longValue()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
return new DailyServiceStatistics(serviceType.name(), dailyStatistics);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,4 +62,8 @@ public class Usage {
|
|||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
private final ExecutionUsage executions;
|
private final ExecutionUsage executions;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@Nullable
|
||||||
|
private ServiceUsage services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import io.kestra.core.services.KVStoreService;
|
|||||||
import io.kestra.core.storages.Storage;
|
import io.kestra.core.storages.Storage;
|
||||||
import io.kestra.core.storages.StorageInterface;
|
import io.kestra.core.storages.StorageInterface;
|
||||||
import io.kestra.core.storages.kv.KVStore;
|
import io.kestra.core.storages.kv.KVStore;
|
||||||
|
import io.kestra.core.utils.ListUtils;
|
||||||
import io.kestra.core.utils.VersionProvider;
|
import io.kestra.core.utils.VersionProvider;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import io.micronaut.context.annotation.Value;
|
import io.micronaut.context.annotation.Value;
|
||||||
@@ -30,7 +31,6 @@ import java.nio.file.Path;
|
|||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static io.kestra.core.utils.MapUtils.mergeWithNullableValues;
|
import static io.kestra.core.utils.MapUtils.mergeWithNullableValues;
|
||||||
@@ -67,6 +67,7 @@ public class DefaultRunContext extends RunContext {
|
|||||||
private String triggerExecutionId;
|
private String triggerExecutionId;
|
||||||
private Storage storage;
|
private Storage storage;
|
||||||
private Map<String, Object> pluginConfiguration;
|
private Map<String, Object> pluginConfiguration;
|
||||||
|
private List<String> secretInputs;
|
||||||
|
|
||||||
private final AtomicBoolean isInitialized = new AtomicBoolean(false);
|
private final AtomicBoolean isInitialized = new AtomicBoolean(false);
|
||||||
|
|
||||||
@@ -98,6 +99,15 @@ public class DefaultRunContext extends RunContext {
|
|||||||
return variables;
|
return variables;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@JsonInclude
|
||||||
|
public List<String> getSecretInputs() {
|
||||||
|
return secretInputs;
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public ApplicationContext getApplicationContext() {
|
public ApplicationContext getApplicationContext() {
|
||||||
return applicationContext;
|
return applicationContext;
|
||||||
@@ -123,6 +133,17 @@ public class DefaultRunContext extends RunContext {
|
|||||||
|
|
||||||
void setLogger(final RunContextLogger logger) {
|
void setLogger(final RunContextLogger logger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
|
||||||
|
// this is used when a run context is re-hydrated so we need to add again the secrets from the inputs
|
||||||
|
if (!ListUtils.isEmpty(secretInputs) && getVariables().containsKey("inputs")) {
|
||||||
|
Map<String, Object> inputs = (Map<String, Object>) getVariables().get("inputs");
|
||||||
|
for (String secretInput : secretInputs) {
|
||||||
|
String secret = (String) inputs.get(secretInput);
|
||||||
|
if (secret != null) {
|
||||||
|
logger.usedSecret(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPluginConfiguration(final Map<String, Object> pluginConfiguration) {
|
void setPluginConfiguration(final Map<String, Object> pluginConfiguration) {
|
||||||
@@ -488,6 +509,7 @@ public class DefaultRunContext extends RunContext {
|
|||||||
private String triggerExecutionId;
|
private String triggerExecutionId;
|
||||||
private RunContextLogger logger;
|
private RunContextLogger logger;
|
||||||
private KVStoreService kvStoreService;
|
private KVStoreService kvStoreService;
|
||||||
|
private List<String> secretInputs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the new {@link DefaultRunContext} object.
|
* Builds the new {@link DefaultRunContext} object.
|
||||||
@@ -507,6 +529,7 @@ public class DefaultRunContext extends RunContext {
|
|||||||
context.storage = storage;
|
context.storage = storage;
|
||||||
context.triggerExecutionId = triggerExecutionId;
|
context.triggerExecutionId = triggerExecutionId;
|
||||||
context.kvStoreService = kvStoreService;
|
context.kvStoreService = kvStoreService;
|
||||||
|
context.secretInputs = secretInputs;
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -748,7 +748,8 @@ public class ExecutorService {
|
|||||||
.map(WorkerGroup::getKey)
|
.map(WorkerGroup::getKey)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
// Check if the worker group exist
|
// Check if the worker group exist
|
||||||
if (workerGroupExecutorInterface.isWorkerGroupExistForKey(workerGroup)) {
|
String tenantId = executor.getFlow().getTenantId();
|
||||||
|
if (workerGroupExecutorInterface.isWorkerGroupExistForKey(workerGroup, tenantId)) {
|
||||||
// Check whether at-least one worker is available
|
// Check whether at-least one worker is available
|
||||||
if (workerGroupExecutorInterface.isWorkerGroupAvailableForKey(workerGroup)) {
|
if (workerGroupExecutorInterface.isWorkerGroupAvailableForKey(workerGroup)) {
|
||||||
return workerTask;
|
return workerTask;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.google.common.annotations.VisibleForTesting;
|
|||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import io.kestra.core.encryption.EncryptionService;
|
import io.kestra.core.encryption.EncryptionService;
|
||||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||||
|
import io.kestra.core.exceptions.KestraRuntimeException;
|
||||||
import io.kestra.core.models.executions.Execution;
|
import io.kestra.core.models.executions.Execution;
|
||||||
import io.kestra.core.models.flows.Data;
|
import io.kestra.core.models.flows.Data;
|
||||||
import io.kestra.core.models.flows.DependsOn;
|
import io.kestra.core.models.flows.DependsOn;
|
||||||
@@ -18,6 +19,7 @@ import io.kestra.core.models.flows.input.ItemTypeInterface;
|
|||||||
import io.kestra.core.models.tasks.common.EncryptedString;
|
import io.kestra.core.models.tasks.common.EncryptedString;
|
||||||
import io.kestra.core.models.validations.ManualConstraintViolation;
|
import io.kestra.core.models.validations.ManualConstraintViolation;
|
||||||
import io.kestra.core.serializers.JacksonMapper;
|
import io.kestra.core.serializers.JacksonMapper;
|
||||||
|
import io.kestra.core.storages.StorageContext;
|
||||||
import io.kestra.core.storages.StorageInterface;
|
import io.kestra.core.storages.StorageInterface;
|
||||||
import io.kestra.core.utils.ListUtils;
|
import io.kestra.core.utils.ListUtils;
|
||||||
import io.kestra.core.utils.MapUtils;
|
import io.kestra.core.utils.MapUtils;
|
||||||
@@ -33,7 +35,7 @@ import org.reactivestreams.Publisher;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@@ -90,31 +92,14 @@ public class FlowInputOutput {
|
|||||||
* @param inputs The Flow's inputs.
|
* @param inputs The Flow's inputs.
|
||||||
* @param execution The Execution.
|
* @param execution The Execution.
|
||||||
* @param data The Execution's inputs data.
|
* @param data The Execution's inputs data.
|
||||||
* @param deleteInputsFromStorage Specifies whether inputs stored on internal storage should be deleted before returning.
|
|
||||||
* @return The list of {@link InputAndValue}.
|
* @return The list of {@link InputAndValue}.
|
||||||
*/
|
*/
|
||||||
public List<InputAndValue> validateExecutionInputs(final List<Input<?>> inputs,
|
public Mono<List<InputAndValue>> validateExecutionInputs(final List<Input<?>> inputs,
|
||||||
final Execution execution,
|
final Execution execution,
|
||||||
final Publisher<CompletedPart> data,
|
final Publisher<CompletedPart> data) {
|
||||||
final boolean deleteInputsFromStorage) throws IOException {
|
if (ListUtils.isEmpty(inputs)) return Mono.just(Collections.emptyList());
|
||||||
if (ListUtils.isEmpty(inputs)) return Collections.emptyList();
|
|
||||||
|
|
||||||
Map<String, ?> dataByInputId = readData(inputs, execution, data);
|
return readData(inputs, execution, data, false).map(inputData -> resolveInputs(inputs, execution, inputData));
|
||||||
|
|
||||||
List<InputAndValue> values = this.resolveInputs(inputs, execution, dataByInputId);
|
|
||||||
if (deleteInputsFromStorage) {
|
|
||||||
values.stream()
|
|
||||||
.filter(it -> it.input() instanceof FileInput && Objects.nonNull(it.value()))
|
|
||||||
.forEach(it -> {
|
|
||||||
try {
|
|
||||||
URI uri = URI.create(it.value().toString());
|
|
||||||
storageInterface.delete(execution.getTenantId(), uri);
|
|
||||||
} catch (IllegalArgumentException | IOException e) {
|
|
||||||
log.debug("Failed to remove execution input after validation [{}]", it.value(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,9 +110,9 @@ public class FlowInputOutput {
|
|||||||
* @param data The Execution's inputs data.
|
* @param data The Execution's inputs data.
|
||||||
* @return The Map of typed inputs.
|
* @return The Map of typed inputs.
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> readExecutionInputs(final Flow flow,
|
public Mono<Map<String, Object>> readExecutionInputs(final Flow flow,
|
||||||
final Execution execution,
|
final Execution execution,
|
||||||
final Publisher<CompletedPart> data) throws IOException {
|
final Publisher<CompletedPart> data) {
|
||||||
return this.readExecutionInputs(flow.getInputs(), execution, data);
|
return this.readExecutionInputs(flow.getInputs(), execution, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,39 +124,51 @@ public class FlowInputOutput {
|
|||||||
* @param data The Execution's inputs data.
|
* @param data The Execution's inputs data.
|
||||||
* @return The Map of typed inputs.
|
* @return The Map of typed inputs.
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> readExecutionInputs(final List<Input<?>> inputs,
|
public Mono<Map<String, Object>> readExecutionInputs(final List<Input<?>> inputs,
|
||||||
final Execution execution,
|
final Execution execution,
|
||||||
final Publisher<CompletedPart> data) throws IOException {
|
final Publisher<CompletedPart> data) {
|
||||||
return this.readExecutionInputs(inputs, execution, readData(inputs, execution, data));
|
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, execution, inputData));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, ?> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data) throws IOException {
|
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
|
||||||
return Flux.from(data)
|
return Flux.from(data)
|
||||||
.subscribeOn(Schedulers.boundedElastic())
|
.<AbstractMap.SimpleEntry<String, String>>handle((input, sink) -> {
|
||||||
.map(throwFunction(input -> {
|
try {
|
||||||
if (input instanceof CompletedFileUpload fileUpload) {
|
if (input instanceof CompletedFileUpload fileUpload) {
|
||||||
final String fileExtension = FileInput.findFileInputExtension(inputs, fileUpload.getFilename());
|
if (!uploadFiles) {
|
||||||
File tempFile = File.createTempFile(fileUpload.getFilename() + "_", fileExtension);
|
final String fileExtension = FileInput.findFileInputExtension(inputs, fileUpload.getFilename());
|
||||||
try (var inputStream = fileUpload.getInputStream();
|
URI from = URI.create("kestra://" + StorageContext
|
||||||
var outputStream = new FileOutputStream(tempFile)) {
|
.forInput(execution, fileUpload.getFilename(), fileUpload.getFilename() + fileExtension)
|
||||||
long transferredBytes = inputStream.transferTo(outputStream);
|
.getContextStorageURI()
|
||||||
if (transferredBytes == 0) {
|
);
|
||||||
throw new RuntimeException("Can't upload file: " + fileUpload.getFilename());
|
sink.next(new AbstractMap.SimpleEntry<>(fileUpload.getFilename(), from.toString()));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
final String fileExtension = FileInput.findFileInputExtension(inputs, fileUpload.getFilename());
|
||||||
|
|
||||||
URI from = storageInterface.from(execution, fileUpload.getFilename(), tempFile);
|
File tempFile = File.createTempFile(fileUpload.getFilename() + "_", fileExtension);
|
||||||
return new AbstractMap.SimpleEntry<>(fileUpload.getFilename(), from.toString());
|
try (var inputStream = fileUpload.getInputStream();
|
||||||
} finally {
|
var outputStream = new FileOutputStream(tempFile)) {
|
||||||
if (!tempFile.delete()) {
|
long transferredBytes = inputStream.transferTo(outputStream);
|
||||||
tempFile.deleteOnExit();
|
if (transferredBytes == 0) {
|
||||||
}
|
sink.error(new KestraRuntimeException("Can't upload file: " + fileUpload.getFilename()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
URI from = storageInterface.from(execution, fileUpload.getFilename(), tempFile);
|
||||||
|
sink.next(new AbstractMap.SimpleEntry<>(fileUpload.getFilename(), from.toString()));
|
||||||
|
} finally {
|
||||||
|
if (!tempFile.delete()) {
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sink.next(new AbstractMap.SimpleEntry<>(input.getName(), new String(input.getBytes())));
|
||||||
}
|
}
|
||||||
} else {
|
} catch (IOException e) {
|
||||||
return new AbstractMap.SimpleEntry<>(input.getName(), new String(input.getBytes()));
|
sink.error(e);
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
.collectMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)
|
.collectMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue);
|
||||||
.block();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ public abstract class RunContext {
|
|||||||
@JsonInclude
|
@JsonInclude
|
||||||
public abstract Map<String, Object> getVariables();
|
public abstract Map<String, Object> getVariables();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of inputs of type SECRET.
|
||||||
|
*/
|
||||||
|
@JsonInclude
|
||||||
|
public abstract List<String> getSecretInputs();
|
||||||
|
|
||||||
public abstract String render(String inline) throws IllegalVariableEvaluationException;
|
public abstract String render(String inline) throws IllegalVariableEvaluationException;
|
||||||
|
|
||||||
public abstract Object renderTyped(String inline) throws IllegalVariableEvaluationException;
|
public abstract Object renderTyped(String inline) throws IllegalVariableEvaluationException;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import io.kestra.core.metrics.MetricRegistry;
|
|||||||
import io.kestra.core.models.executions.Execution;
|
import io.kestra.core.models.executions.Execution;
|
||||||
import io.kestra.core.models.executions.TaskRun;
|
import io.kestra.core.models.executions.TaskRun;
|
||||||
import io.kestra.core.models.flows.Flow;
|
import io.kestra.core.models.flows.Flow;
|
||||||
|
import io.kestra.core.models.flows.Type;
|
||||||
import io.kestra.core.models.tasks.Task;
|
import io.kestra.core.models.tasks.Task;
|
||||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||||
import io.kestra.core.plugins.PluginConfigurations;
|
import io.kestra.core.plugins.PluginConfigurations;
|
||||||
@@ -15,12 +16,12 @@ import io.kestra.core.storages.StorageContext;
|
|||||||
import io.kestra.core.storages.StorageInterface;
|
import io.kestra.core.storages.StorageInterface;
|
||||||
import io.micronaut.context.ApplicationContext;
|
import io.micronaut.context.ApplicationContext;
|
||||||
import io.micronaut.context.annotation.Value;
|
import io.micronaut.context.annotation.Value;
|
||||||
import jakarta.annotation.Nullable;
|
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -83,8 +84,10 @@ public class RunContextFactory {
|
|||||||
.withFlow(flow)
|
.withFlow(flow)
|
||||||
.withExecution(execution)
|
.withExecution(execution)
|
||||||
.withDecryptVariables(true)
|
.withDecryptVariables(true)
|
||||||
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
)
|
)
|
||||||
.build(runContextLogger))
|
.build(runContextLogger))
|
||||||
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +110,10 @@ public class RunContextFactory {
|
|||||||
.withExecution(execution)
|
.withExecution(execution)
|
||||||
.withTaskRun(taskRun)
|
.withTaskRun(taskRun)
|
||||||
.withDecryptVariables(decryptVariables)
|
.withDecryptVariables(decryptVariables)
|
||||||
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.build(runContextLogger))
|
.build(runContextLogger))
|
||||||
.withKvStoreService(kvStoreService)
|
.withKvStoreService(kvStoreService)
|
||||||
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +127,10 @@ public class RunContextFactory {
|
|||||||
.withVariables(newRunVariablesBuilder()
|
.withVariables(newRunVariablesBuilder()
|
||||||
.withFlow(flow)
|
.withFlow(flow)
|
||||||
.withTrigger(trigger)
|
.withTrigger(trigger)
|
||||||
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.build(runContextLogger)
|
.build(runContextLogger)
|
||||||
)
|
)
|
||||||
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +142,7 @@ public class RunContextFactory {
|
|||||||
.withLogger(runContextLogger)
|
.withLogger(runContextLogger)
|
||||||
.withStorage(new InternalStorage(runContextLogger.logger(), StorageContext.forFlow(flow), storageInterface, flowService))
|
.withStorage(new InternalStorage(runContextLogger.logger(), StorageContext.forFlow(flow), storageInterface, flowService))
|
||||||
.withVariables(variables)
|
.withVariables(variables)
|
||||||
|
.withSecretInputs(secretInputsFromFlow(flow))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +185,16 @@ public class RunContextFactory {
|
|||||||
return of(Map.of());
|
return of(Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> secretInputsFromFlow(Flow flow) {
|
||||||
|
if (flow == null || flow.getInputs() == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return flow.getInputs().stream()
|
||||||
|
.filter(input -> input.getType() == Type.SECRET)
|
||||||
|
.map(input -> input.getId()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
private DefaultRunContext.Builder newBuilder() {
|
private DefaultRunContext.Builder newBuilder() {
|
||||||
return new DefaultRunContext.Builder()
|
return new DefaultRunContext.Builder()
|
||||||
// inject mandatory services and config
|
// inject mandatory services and config
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import io.kestra.core.models.flows.State;
|
|||||||
import io.kestra.core.models.flows.input.SecretInput;
|
import io.kestra.core.models.flows.input.SecretInput;
|
||||||
import io.kestra.core.models.tasks.Task;
|
import io.kestra.core.models.tasks.Task;
|
||||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||||
|
import io.kestra.core.utils.ListUtils;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.With;
|
import lombok.With;
|
||||||
|
|
||||||
@@ -125,6 +126,8 @@ public final class RunVariables {
|
|||||||
|
|
||||||
Builder withGlobals(Map<?, ?> globals);
|
Builder withGlobals(Map<?, ?> globals);
|
||||||
|
|
||||||
|
Builder withSecretInputs(List<String> secretInputs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the immutable map of run variables.
|
* Builds the immutable map of run variables.
|
||||||
*
|
*
|
||||||
@@ -152,6 +155,7 @@ public final class RunVariables {
|
|||||||
protected Map<String, ?> envs;
|
protected Map<String, ?> envs;
|
||||||
protected Map<?, ?> globals;
|
protected Map<?, ?> globals;
|
||||||
private final Optional<String> secretKey;
|
private final Optional<String> secretKey;
|
||||||
|
private List<String> secretInputs;
|
||||||
|
|
||||||
public DefaultBuilder() {
|
public DefaultBuilder() {
|
||||||
this(Optional.empty());
|
this(Optional.empty());
|
||||||
@@ -252,6 +256,16 @@ public final class RunVariables {
|
|||||||
|
|
||||||
if (!inputs.isEmpty()) {
|
if (!inputs.isEmpty()) {
|
||||||
builder.put("inputs", inputs);
|
builder.put("inputs", inputs);
|
||||||
|
|
||||||
|
// if a secret input is used, add it to the list of secrets to mask on the logger
|
||||||
|
if (logger != null && !ListUtils.isEmpty(secretInputs)) {
|
||||||
|
for (String secretInput : secretInputs) {
|
||||||
|
String secret = (String) inputs.get(secretInput);
|
||||||
|
if (secret != null) {
|
||||||
|
logger.usedSecret(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (execution.getTrigger() != null && execution.getTrigger().getVariables() != null) {
|
if (execution.getTrigger() != null && execution.getTrigger().getVariables() != null) {
|
||||||
|
|||||||
@@ -530,10 +530,6 @@ public class Worker implements Service, Runnable, AutoCloseable {
|
|||||||
.increment();
|
.increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ZonedDateTime now() {
|
|
||||||
return ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private WorkerTask cleanUpTransient(WorkerTask workerTask) {
|
private WorkerTask cleanUpTransient(WorkerTask workerTask) {
|
||||||
try {
|
try {
|
||||||
return MAPPER.readValue(MAPPER.writeValueAsString(workerTask), WorkerTask.class);
|
return MAPPER.readValue(MAPPER.writeValueAsString(workerTask), WorkerTask.class);
|
||||||
@@ -553,7 +549,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
|
|||||||
metricRegistry
|
metricRegistry
|
||||||
.timer(MetricRegistry.METRIC_WORKER_QUEUED_DURATION, metricRegistry.tags(workerTask, workerGroup))
|
.timer(MetricRegistry.METRIC_WORKER_QUEUED_DURATION, metricRegistry.tags(workerTask, workerGroup))
|
||||||
.record(Duration.between(
|
.record(Duration.between(
|
||||||
workerTask.getTaskRun().getState().getStartDate(), now()
|
workerTask.getTaskRun().getState().getStartDate(), Instant.now()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,8 +700,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private WorkerTask runAttempt(WorkerTask workerTask) throws QueueException {
|
private WorkerTask runAttempt(WorkerTask workerTask) throws QueueException {
|
||||||
DefaultRunContext runContext = (DefaultRunContext) workerTask.getRunContext();
|
DefaultRunContext runContext = runContextInitializer.forWorker((DefaultRunContext) workerTask.getRunContext(), workerTask);;
|
||||||
runContextInitializer.forWorker(runContext, workerTask);
|
|
||||||
|
|
||||||
Logger logger = runContext.logger();
|
Logger logger = runContext.logger();
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ import java.util.Set;
|
|||||||
public interface WorkerGroupExecutorInterface {
|
public interface WorkerGroupExecutorInterface {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a Worker Group exists for the given key.
|
* Checks whether a Worker Group exists for the given key and tenant.
|
||||||
*
|
*
|
||||||
* @param key The Worker Group's key - can be {@code null}.
|
* @param key The Worker Group's key - can be {@code null}.
|
||||||
|
* @param tenant The tenant's ID - can be {@code null}.
|
||||||
* @return {@code true} if the worker group exists, or is {@code null}, {@code false} otherwise.
|
* @return {@code true} if the worker group exists, or is {@code null}, {@code false} otherwise.
|
||||||
*/
|
*/
|
||||||
boolean isWorkerGroupExistForKey(String key);
|
boolean isWorkerGroupExistForKey(String key, String tenant);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the Worker Group is available.
|
* Checks whether the Worker Group is available.
|
||||||
@@ -46,7 +47,7 @@ public interface WorkerGroupExecutorInterface {
|
|||||||
class DefaultWorkerGroupExecutorInterface implements WorkerGroupExecutorInterface {
|
class DefaultWorkerGroupExecutorInterface implements WorkerGroupExecutorInterface {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isWorkerGroupExistForKey(String key) {
|
public boolean isWorkerGroupExistForKey(String key, String tenant) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import io.kestra.core.models.collectors.*;
|
|||||||
import io.kestra.core.plugins.PluginRegistry;
|
import io.kestra.core.plugins.PluginRegistry;
|
||||||
import io.kestra.core.repositories.ExecutionRepositoryInterface;
|
import io.kestra.core.repositories.ExecutionRepositoryInterface;
|
||||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||||
|
import io.kestra.core.repositories.ServiceInstanceRepositoryInterface;
|
||||||
import io.kestra.core.serializers.JacksonMapper;
|
import io.kestra.core.serializers.JacksonMapper;
|
||||||
import io.kestra.core.utils.IdUtils;
|
import io.kestra.core.utils.IdUtils;
|
||||||
import io.kestra.core.utils.VersionProvider;
|
import io.kestra.core.utils.VersionProvider;
|
||||||
@@ -24,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@@ -66,6 +68,9 @@ public class CollectorService {
|
|||||||
@Value("${kestra.anonymous-usage-report.uri}")
|
@Value("${kestra.anonymous-usage-report.uri}")
|
||||||
protected URI url;
|
protected URI url;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ServiceInstanceRepositoryInterface serviceRepository;
|
||||||
|
|
||||||
private transient Usage defaultUsage;
|
private transient Usage defaultUsage;
|
||||||
|
|
||||||
protected synchronized Usage defaultUsage() {
|
protected synchronized Usage defaultUsage() {
|
||||||
@@ -109,7 +114,8 @@ public class CollectorService {
|
|||||||
if (details) {
|
if (details) {
|
||||||
builder = builder
|
builder = builder
|
||||||
.flows(FlowUsage.of(flowRepository))
|
.flows(FlowUsage.of(flowRepository))
|
||||||
.executions(ExecutionUsage.of(executionRepository, from, to));
|
.executions(ExecutionUsage.of(executionRepository, from, to))
|
||||||
|
.services(ServiceUsage.of(from.toInstant(), to.toInstant(), serviceRepository, Duration.ofMinutes(5)));
|
||||||
}
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ package io.kestra.core.services;
|
|||||||
import io.kestra.core.events.CrudEvent;
|
import io.kestra.core.events.CrudEvent;
|
||||||
import io.kestra.core.events.CrudEventType;
|
import io.kestra.core.events.CrudEventType;
|
||||||
import io.kestra.core.exceptions.InternalException;
|
import io.kestra.core.exceptions.InternalException;
|
||||||
import io.kestra.core.models.executions.*;
|
import io.kestra.core.models.executions.Execution;
|
||||||
|
import io.kestra.core.models.executions.ExecutionKilled;
|
||||||
|
import io.kestra.core.models.executions.ExecutionKilledExecution;
|
||||||
|
import io.kestra.core.models.executions.TaskRun;
|
||||||
|
import io.kestra.core.models.executions.TaskRunAttempt;
|
||||||
import io.kestra.core.models.flows.Flow;
|
import io.kestra.core.models.flows.Flow;
|
||||||
import io.kestra.core.models.flows.State;
|
import io.kestra.core.models.flows.State;
|
||||||
import io.kestra.core.models.flows.input.InputAndValue;
|
import io.kestra.core.models.flows.input.InputAndValue;
|
||||||
@@ -26,7 +30,6 @@ import io.kestra.plugin.core.flow.Pause;
|
|||||||
import io.kestra.plugin.core.flow.WorkingDirectory;
|
import io.kestra.plugin.core.flow.WorkingDirectory;
|
||||||
import io.micronaut.context.event.ApplicationEventPublisher;
|
import io.micronaut.context.event.ApplicationEventPublisher;
|
||||||
import io.micronaut.core.annotation.Nullable;
|
import io.micronaut.core.annotation.Nullable;
|
||||||
import io.micronaut.http.HttpResponse;
|
|
||||||
import io.micronaut.http.multipart.CompletedPart;
|
import io.micronaut.http.multipart.CompletedPart;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
import jakarta.inject.Named;
|
||||||
@@ -38,12 +41,21 @@ import lombok.experimental.SuperBuilder;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.*;
|
import java.util.AbstractMap;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -449,17 +461,17 @@ public class ExecutionService {
|
|||||||
* @return the execution in the new state.
|
* @return the execution in the new state.
|
||||||
* @throws Exception if the state of the execution cannot be updated
|
* @throws Exception if the state of the execution cannot be updated
|
||||||
*/
|
*/
|
||||||
public List<InputAndValue> validateForResume(final Execution execution, Flow flow, @Nullable Publisher<CompletedPart> inputs) throws Exception {
|
public Mono<List<InputAndValue>> validateForResume(final Execution execution, Flow flow, @Nullable Publisher<CompletedPart> inputs) {
|
||||||
Task task = getFirstPausedTaskOrThrow(execution, flow);
|
return getFirstPausedTaskOrThrow(execution, flow).handle((task, sink) -> {
|
||||||
if (task instanceof Pause pauseTask) {
|
if (task instanceof Pause pauseTask) {
|
||||||
return flowInputOutput.validateExecutionInputs(
|
flowInputOutput.validateExecutionInputs(
|
||||||
pauseTask.getOnResume(),
|
pauseTask.getOnResume(),
|
||||||
execution,
|
execution,
|
||||||
inputs,
|
inputs
|
||||||
true
|
).subscribe(sink::next, sink::error);
|
||||||
);
|
}
|
||||||
}
|
sink.next(Collections.emptyList());
|
||||||
return Collections.emptyList();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -473,25 +485,37 @@ public class ExecutionService {
|
|||||||
* @return the execution in the new state.
|
* @return the execution in the new state.
|
||||||
* @throws Exception if the state of the execution cannot be updated
|
* @throws Exception if the state of the execution cannot be updated
|
||||||
*/
|
*/
|
||||||
public Execution resume(final Execution execution, Flow flow, State.Type newState, @Nullable Publisher<CompletedPart> inputs) throws Exception {
|
public Mono<Execution> resume(final Execution execution, Flow flow, State.Type newState, @Nullable Publisher<CompletedPart> inputs) {
|
||||||
var task = getFirstPausedTaskOrThrow(execution, flow);
|
return getFirstPausedTaskOrThrow(execution, flow).handle((task, sink) -> {
|
||||||
Map<String, Object> pauseOutputs = Collections.emptyMap();
|
Mono<Map<String, Object>> monoOutputs;
|
||||||
if (task instanceof Pause pauseTask) {
|
|
||||||
pauseOutputs = flowInputOutput.readExecutionInputs(
|
|
||||||
pauseTask.getOnResume(),
|
|
||||||
execution,
|
|
||||||
inputs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resume(execution, flow, newState, pauseOutputs);
|
if (task instanceof Pause pauseTask) {
|
||||||
|
monoOutputs = flowInputOutput.readExecutionInputs(pauseTask.getOnResume(), execution, inputs);
|
||||||
|
} else {
|
||||||
|
monoOutputs = Mono.just(Collections.emptyMap());
|
||||||
|
}
|
||||||
|
Mono<Execution> monoExecution = monoOutputs.handle((outputs, monoSink) -> {
|
||||||
|
try {
|
||||||
|
sink.next(resume(execution, flow, newState, outputs));
|
||||||
|
} catch (Exception e) {
|
||||||
|
sink.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
monoExecution.subscribe(sink::next, sink::error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task getFirstPausedTaskOrThrow(Execution execution, Flow flow) throws InternalException {
|
private static Mono<Task> getFirstPausedTaskOrThrow(Execution execution, Flow flow){
|
||||||
var runningTaskRun = execution
|
return Mono.create(sink -> {
|
||||||
.findFirstByState(State.Type.PAUSED)
|
try {
|
||||||
.orElseThrow(() -> new IllegalArgumentException("No paused task found on execution " + execution.getId()));
|
var runningTaskRun = execution
|
||||||
return flow.findTaskByTaskId(runningTaskRun.getTaskId());
|
.findFirstByState(State.Type.PAUSED)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("No paused task found on execution " + execution.getId()));
|
||||||
|
sink.success(flow.findTaskByTaskId(runningTaskRun.getTaskId()));
|
||||||
|
} catch (InternalException e) {
|
||||||
|
sink.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public final class PathMatcherPredicate implements Predicate<Path> {
|
|||||||
} else {
|
} else {
|
||||||
pattern = mayAddRecursiveMatch(p);
|
pattern = mayAddRecursiveMatch(p);
|
||||||
}
|
}
|
||||||
syntaxAndPattern = SYNTAX_GLOB + pattern;
|
syntaxAndPattern = SYNTAX_GLOB + pattern.replace("\\", "/");
|
||||||
}
|
}
|
||||||
return syntaxAndPattern;
|
return syntaxAndPattern;
|
||||||
})
|
})
|
||||||
@@ -125,7 +125,7 @@ public final class PathMatcherPredicate implements Predicate<Path> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String mayAddLeadingSlash(final String path) {
|
private static String mayAddLeadingSlash(final String path) {
|
||||||
return path.startsWith("/") ? path : "/" + path;
|
return (path.startsWith("/") || path.startsWith("\\")) ? path : "/" + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isPrefixWithSyntax(final String pattern) {
|
public static boolean isPrefixWithSyntax(final String pattern) {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import io.kestra.core.models.tasks.*;
|
|||||||
import io.kestra.core.runners.*;
|
import io.kestra.core.runners.*;
|
||||||
import io.kestra.core.serializers.FileSerde;
|
import io.kestra.core.serializers.FileSerde;
|
||||||
import io.kestra.core.services.StorageService;
|
import io.kestra.core.services.StorageService;
|
||||||
|
import io.kestra.core.storages.FileAttributes;
|
||||||
|
import io.kestra.core.storages.StorageContext;
|
||||||
|
import io.kestra.core.storages.StorageInterface;
|
||||||
import io.kestra.core.storages.StorageSplitInterface;
|
import io.kestra.core.storages.StorageSplitInterface;
|
||||||
import io.kestra.core.utils.GraphUtils;
|
import io.kestra.core.utils.GraphUtils;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -580,23 +583,25 @@ public class ForEachItem extends Task implements FlowableTask<VoidOutput>, Child
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer iterations = (Integer) taskOutput.get(ExecutableUtils.TASK_VARIABLE_NUMBER_OF_BATCHES);
|
String subflowOutputsBase = (String) taskOutput.get(ExecutableUtils.TASK_VARIABLE_SUBFLOW_OUTPUTS_BASE_URI);
|
||||||
String subflowOutputsBaseUri = (String) taskOutput.get(ExecutableUtils.TASK_VARIABLE_SUBFLOW_OUTPUTS_BASE_URI);
|
URI subflowOutputsBaseUri = URI.create(StorageContext.KESTRA_PROTOCOL + subflowOutputsBase + "/");
|
||||||
|
|
||||||
List<URI> outputsURIs = IntStream.rangeClosed(1, iterations)
|
StorageInterface storage = ((DefaultRunContext) runContext).getApplicationContext().getBean(StorageInterface.class);
|
||||||
.mapToObj(it -> "kestra://" + subflowOutputsBaseUri + "/" + it + "/outputs.ion")
|
if (storage.exists(runContext.tenantId(), subflowOutputsBaseUri)) {
|
||||||
.map(throwFunction(URI::create))
|
List<FileAttributes> list = storage.list(runContext.tenantId(), subflowOutputsBaseUri);
|
||||||
.filter(runContext.storage()::isFileExist)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (!outputsURIs.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
// Merge outputs from each sub-flow into a single stored in the internal storage.
|
// Merge outputs from each sub-flow into a single stored in the internal storage.
|
||||||
List<InputStream> streams = outputsURIs.stream()
|
List<InputStream> streams = list.stream()
|
||||||
.map(throwFunction(runContext.storage()::getFile))
|
.map(throwFunction(attr -> {
|
||||||
.toList();
|
URI file = subflowOutputsBaseUri.resolve(attr.getFileName() + "/outputs.ion");
|
||||||
try (InputStream is = new SequenceInputStream(Collections.enumeration(streams))) {
|
return runContext.storage().getFile(file);
|
||||||
URI uri = runContext.storage().putFile(is, "outputs.ion");
|
}))
|
||||||
return ForEachItemMergeOutputs.Output.builder().subflowOutputs(uri).build();
|
.toList();
|
||||||
|
try (InputStream is = new SequenceInputStream(Collections.enumeration(streams))) {
|
||||||
|
URI uri = runContext.storage().putFile(is, "outputs.ion");
|
||||||
|
return ForEachItemMergeOutputs.Output.builder().subflowOutputs(uri).build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.util.Map;
|
|||||||
You can use this task to return some outputs and pass them to downstream tasks.
|
You can use this task to return some outputs and pass them to downstream tasks.
|
||||||
It's helpful for parsing and returning values from a task. You can then access these outputs in your downstream tasks
|
It's helpful for parsing and returning values from a task. You can then access these outputs in your downstream tasks
|
||||||
using the expression `{{ outputs.mytask_id.values.my_output_name }}` and you can see them in the Outputs tab.
|
using the expression `{{ outputs.mytask_id.values.my_output_name }}` and you can see them in the Outputs tab.
|
||||||
|
The values can be strings, numbers, arrays, or any valid JSON object.
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@Plugin(
|
@Plugin(
|
||||||
@@ -39,6 +40,11 @@ tasks:
|
|||||||
values:
|
values:
|
||||||
taskrun_data: "{{ task.id }} > {{ taskrun.startDate }}"
|
taskrun_data: "{{ task.id }} > {{ taskrun.startDate }}"
|
||||||
execution_data: "{{ flow.id }} > {{ execution.startDate }}"
|
execution_data: "{{ flow.id }} > {{ execution.startDate }}"
|
||||||
|
number_value: 42
|
||||||
|
array_value: ["{{ task.id }}", "{{ flow.id }}", "static value"]
|
||||||
|
nested_object:
|
||||||
|
key1: "value1"
|
||||||
|
key2: "{{ execution.id }}"
|
||||||
|
|
||||||
- id: log_values
|
- id: log_values
|
||||||
type: io.kestra.plugin.core.log.Log
|
type: io.kestra.plugin.core.log.Log
|
||||||
@@ -51,15 +57,16 @@ tasks:
|
|||||||
)
|
)
|
||||||
public class OutputValues extends Task implements RunnableTask<OutputValues.Output> {
|
public class OutputValues extends Task implements RunnableTask<OutputValues.Output> {
|
||||||
@Schema(
|
@Schema(
|
||||||
title = "The templated strings to render."
|
title = "The templated strings to render.",
|
||||||
|
description = "These values can be strings, numbers, arrays, or objects. Templated strings (enclosed in {{ }}) will be rendered using the current context."
|
||||||
)
|
)
|
||||||
private HashMap<String, String> values;
|
private HashMap<String, Object> values;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OutputValues.Output run(RunContext runContext) throws Exception {
|
public OutputValues.Output run(RunContext runContext) throws Exception {
|
||||||
return OutputValues.Output.builder()
|
return OutputValues.Output.builder()
|
||||||
.values(runContext.renderMap(values))
|
.values(runContext.render(values))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +76,6 @@ public class OutputValues extends Task implements RunnableTask<OutputValues.Outp
|
|||||||
@Schema(
|
@Schema(
|
||||||
title = "The generated values."
|
title = "The generated values."
|
||||||
)
|
)
|
||||||
private Map<String, String> values;
|
private Map<String, Object> values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package io.kestra.core.models.collectors;
|
||||||
|
|
||||||
|
import io.kestra.core.server.Service;
|
||||||
|
import io.kestra.core.server.ServiceInstance;
|
||||||
|
import io.kestra.core.utils.IdUtils;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
class ServiceUsageTest {
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetDailyUsage() {
|
||||||
|
// Given
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
LocalDate start = now.withDayOfMonth(1);
|
||||||
|
LocalDate end = start.withDayOfMonth(start.getMonth().length(start.isLeapYear()));
|
||||||
|
|
||||||
|
List<ServiceInstance> instances = new ArrayList<>();
|
||||||
|
while (start.toEpochDay() < end.toEpochDay()) {
|
||||||
|
Instant createAt = start.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
|
Instant updatedAt = start.atStartOfDay(ZoneId.systemDefault()).plus(Duration.ofHours(10)).toInstant();
|
||||||
|
ServiceInstance instance = new ServiceInstance(
|
||||||
|
IdUtils.create(),
|
||||||
|
Service.ServiceType.WORKER,
|
||||||
|
Service.ServiceState.EMPTY,
|
||||||
|
null,
|
||||||
|
createAt,
|
||||||
|
updatedAt,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
Map.of(),
|
||||||
|
Set.of()
|
||||||
|
);
|
||||||
|
instance = instance
|
||||||
|
.state(Service.ServiceState.RUNNING, createAt)
|
||||||
|
.state(Service.ServiceState.NOT_RUNNING, updatedAt);
|
||||||
|
instances.add(instance);
|
||||||
|
start = start.plusDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When
|
||||||
|
ServiceUsage.DailyServiceStatistics statistics = ServiceUsage.of(
|
||||||
|
Service.ServiceType.WORKER,
|
||||||
|
Duration.ofMinutes(15),
|
||||||
|
instances
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
Assertions.assertEquals(instances.size(), statistics.values().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import io.kestra.core.junit.annotations.KestraTest;
|
|||||||
import io.kestra.core.models.executions.Execution;
|
import io.kestra.core.models.executions.Execution;
|
||||||
import io.kestra.core.models.flows.DependsOn;
|
import io.kestra.core.models.flows.DependsOn;
|
||||||
import io.kestra.core.models.flows.Input;
|
import io.kestra.core.models.flows.Input;
|
||||||
|
import io.kestra.core.models.flows.Type;
|
||||||
import io.kestra.core.models.flows.input.FileInput;
|
import io.kestra.core.models.flows.input.FileInput;
|
||||||
import io.kestra.core.models.flows.input.InputAndValue;
|
import io.kestra.core.models.flows.input.InputAndValue;
|
||||||
import io.kestra.core.models.flows.input.StringInput;
|
import io.kestra.core.models.flows.input.StringInput;
|
||||||
@@ -198,35 +199,22 @@ class FlowInputOutputTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteFileInputAfterValidationGivenDeleteTrue() throws IOException {
|
void shouldNotUploadFileInputAfterValidation() throws IOException {
|
||||||
// Given
|
// Given
|
||||||
FileInput input = FileInput.builder()
|
FileInput input = FileInput
|
||||||
|
.builder()
|
||||||
.id("input")
|
.id("input")
|
||||||
|
.type(Type.FILE)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Publisher<CompletedPart> data = Mono.just(new MemoryCompletedFileUpload("input", "input", "???".getBytes(StandardCharsets.UTF_8)));
|
Publisher<CompletedPart> data = Mono.just(new MemoryCompletedFileUpload("input", "input", "???".getBytes(StandardCharsets.UTF_8)));
|
||||||
|
|
||||||
// When
|
// When
|
||||||
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), DEFAULT_TEST_EXECUTION, data, true);
|
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), DEFAULT_TEST_EXECUTION, data).block();
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assertions.assertFalse(storageInterface.exists(null, URI.create(values.get(0).value().toString())));
|
Assertions.assertNull(values.getFirst().exception());
|
||||||
}
|
Assertions.assertFalse(storageInterface.exists(null, URI.create(values.getFirst().value().toString())));
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotDeleteFileInputAfterValidationGivenDeleteFalse() throws IOException {
|
|
||||||
// Given
|
|
||||||
FileInput input = FileInput.builder()
|
|
||||||
.id("input")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
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, false);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
Assertions.assertTrue(storageInterface.exists(null, URI.create(values.get(0).value().toString())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class MemoryCompletedFileUpload implements CompletedFileUpload {
|
private static final class MemoryCompletedFileUpload implements CompletedFileUpload {
|
||||||
|
|||||||
@@ -3,16 +3,23 @@ package io.kestra.core.runners;
|
|||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.io.CharStreams;
|
import com.google.common.io.CharStreams;
|
||||||
import io.kestra.core.models.executions.Execution;
|
import io.kestra.core.models.executions.Execution;
|
||||||
|
import io.kestra.core.models.executions.LogEntry;
|
||||||
import io.kestra.core.models.flows.Flow;
|
import io.kestra.core.models.flows.Flow;
|
||||||
import io.kestra.core.models.flows.State;
|
import io.kestra.core.models.flows.State;
|
||||||
import io.kestra.core.queues.QueueException;
|
import io.kestra.core.queues.QueueException;
|
||||||
|
import io.kestra.core.queues.QueueFactoryInterface;
|
||||||
|
import io.kestra.core.queues.QueueInterface;
|
||||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||||
import io.kestra.core.storages.StorageInterface;
|
import io.kestra.core.storages.StorageInterface;
|
||||||
|
import io.kestra.core.utils.TestsUtils;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
import org.jcodings.util.Hash;
|
import org.jcodings.util.Hash;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import jakarta.validation.ConstraintViolationException;
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -22,17 +29,19 @@ import java.time.Duration;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
public class InputsTest extends AbstractMemoryRunnerTest {
|
public class InputsTest extends AbstractMemoryRunnerTest {
|
||||||
|
@Inject
|
||||||
|
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
|
||||||
|
private QueueInterface<LogEntry> logQueue;
|
||||||
|
|
||||||
public static Map<String, Object> inputs = ImmutableMap.<String, Object>builder()
|
public static Map<String, Object> inputs = ImmutableMap.<String, Object>builder()
|
||||||
.put("string", "myString")
|
.put("string", "myString")
|
||||||
.put("enum", "ENUM_VALUE")
|
.put("enum", "ENUM_VALUE")
|
||||||
@@ -351,4 +360,22 @@ public class InputsTest extends AbstractMemoryRunnerTest {
|
|||||||
assertThat(((Map<?, ?>) execution.getInputs().get("json")).size(), is(0));
|
assertThat(((Map<?, ?>) execution.getInputs().get("json")).size(), is(0));
|
||||||
assertThat((String) execution.findTaskRunsByTaskId("jsonOutput").getFirst().getOutputs().get("value"), is("{}"));
|
assertThat((String) execution.findTaskRunsByTaskId("jsonOutput").getFirst().getOutputs().get("value"), is("{}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotLogSecretInput() throws TimeoutException, QueueException {
|
||||||
|
Flux<LogEntry> receive = TestsUtils.receive(logQueue, l -> {});
|
||||||
|
|
||||||
|
Execution execution = runnerUtils.runOne(
|
||||||
|
null,
|
||||||
|
"io.kestra.tests",
|
||||||
|
"input-log-secret"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(execution.getTaskRunList(), hasSize(1));
|
||||||
|
assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
|
||||||
|
|
||||||
|
var logEntry = receive.blockLast();
|
||||||
|
assertThat(logEntry, notNullValue());
|
||||||
|
assertThat(logEntry.getMessage(), is("This is my secret: ********"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class OutputValuesTest extends AbstractMemoryRunnerTest {
|
|||||||
assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
|
assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
|
||||||
assertThat(execution.getTaskRunList(), hasSize(1));
|
assertThat(execution.getTaskRunList(), hasSize(1));
|
||||||
TaskRun outputValues = execution.getTaskRunList().getFirst();
|
TaskRun outputValues = execution.getTaskRunList().getFirst();
|
||||||
Map<String, String> values = (Map<String, String>) outputValues.getOutputs().get("values");
|
Map<String, Object> values = (Map<String, Object>) outputValues.getOutputs().get("values");
|
||||||
assertThat(values.get("output1"), is("xyz"));
|
assertThat(values.get("output1"), is("xyz"));
|
||||||
assertThat(values.get("output2"), is("abc"));
|
assertThat(values.get("output2"), is("abc"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ public class PauseTest extends AbstractMemoryRunnerTest {
|
|||||||
flow,
|
flow,
|
||||||
State.Type.RUNNING,
|
State.Type.RUNNING,
|
||||||
Flux.just(part1, part2)
|
Flux.just(part1, part2)
|
||||||
);
|
).block();
|
||||||
|
|
||||||
execution = runnerUtils.awaitExecution(
|
execution = runnerUtils.awaitExecution(
|
||||||
e -> e.getId().equals(executionId) && e.getState().getCurrent() == State.Type.SUCCESS,
|
e -> e.getId().equals(executionId) && e.getState().getCurrent() == State.Type.SUCCESS,
|
||||||
@@ -243,7 +243,7 @@ public class PauseTest extends AbstractMemoryRunnerTest {
|
|||||||
|
|
||||||
ConstraintViolationException e = assertThrows(
|
ConstraintViolationException e = assertThrows(
|
||||||
ConstraintViolationException.class,
|
ConstraintViolationException.class,
|
||||||
() -> executionService.resume(execution, flow, State.Type.RUNNING, Mono.empty())
|
() -> executionService.resume(execution, flow, State.Type.RUNNING, Mono.empty()).block()
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(e.getMessage(), containsString("Invalid input for `asked`, missing required input, but received `null`"));
|
assertThat(e.getMessage(), containsString("Invalid input for `asked`, missing required input, but received `null`"));
|
||||||
|
|||||||
12
core/src/test/resources/flows/valids/input-log-secret.yaml
Normal file
12
core/src/test/resources/flows/valids/input-log-secret.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
id: input-log-secret
|
||||||
|
namespace: io.kestra.tests
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
- id: secret
|
||||||
|
type: SECRET
|
||||||
|
defaults: password
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- id: log-secret
|
||||||
|
type: io.kestra.plugin.core.log.Log
|
||||||
|
message: "This is my secret: {{inputs.secret}}"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
version=0.19.0-SNAPSHOT
|
version=0.19.3
|
||||||
|
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.priority=low
|
org.gradle.priority=low
|
||||||
|
|||||||
@@ -350,7 +350,10 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
|||||||
DSLContext context = DSL.using(configuration);
|
DSLContext context = DSL.using(configuration);
|
||||||
|
|
||||||
return context.delete(this.jdbcRepository.getTable())
|
return context.delete(this.jdbcRepository.getTable())
|
||||||
.where(field("execution_id", String.class).eq(execution.getId()))
|
// The deleted field is not used, so ti will always be false.
|
||||||
|
// We add it here to be sure to use the correct index.
|
||||||
|
.where(field("deleted", Boolean.class).eq(false))
|
||||||
|
.and(field("execution_id", String.class).eq(execution.getId()))
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
|
|||||||
DSLContext context = DSL.using(configuration);
|
DSLContext context = DSL.using(configuration);
|
||||||
|
|
||||||
return context.delete(this.jdbcRepository.getTable())
|
return context.delete(this.jdbcRepository.getTable())
|
||||||
.where(field("execution_id", String.class).eq(execution.getId()))
|
// The deleted field is not used, so ti will always be false.
|
||||||
|
// We add it here to be sure to use the correct index.
|
||||||
|
.where(field("deleted", Boolean.class).eq(false))
|
||||||
|
.and(field("execution_id", String.class).eq(execution.getId()))
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -168,8 +171,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
|
|||||||
.getDslContextWrapper()
|
.getDslContextWrapper()
|
||||||
.transactionResult(configuration -> {
|
.transactionResult(configuration -> {
|
||||||
DSLContext context = DSL.using(configuration);
|
DSLContext context = DSL.using(configuration);
|
||||||
SelectConditionStep<Record1<Object>> select = DSL
|
SelectConditionStep<Record1<Object>> select = context
|
||||||
.using(configuration)
|
|
||||||
.selectDistinct(field(field))
|
.selectDistinct(field(field))
|
||||||
.from(this.jdbcRepository.getTable())
|
.from(this.jdbcRepository.getTable())
|
||||||
.where(this.defaultFilter(tenantId));
|
.where(this.defaultFilter(tenantId));
|
||||||
@@ -185,8 +187,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
|
|||||||
.getDslContextWrapper()
|
.getDslContextWrapper()
|
||||||
.transactionResult(configuration -> {
|
.transactionResult(configuration -> {
|
||||||
DSLContext context = DSL.using(configuration);
|
DSLContext context = DSL.using(configuration);
|
||||||
SelectConditionStep<Record1<Object>> select = DSL
|
SelectConditionStep<Record1<Object>> select = context
|
||||||
.using(configuration)
|
|
||||||
.select(field("value"))
|
.select(field("value"))
|
||||||
.from(this.jdbcRepository.getTable())
|
.from(this.jdbcRepository.getTable())
|
||||||
.where(this.defaultFilter(tenantId));
|
.where(this.defaultFilter(tenantId));
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ public class LocalStorage implements StorageInterface {
|
|||||||
Path prefix = (tenantId == null) ?
|
Path prefix = (tenantId == null) ?
|
||||||
basePath.toAbsolutePath() :
|
basePath.toAbsolutePath() :
|
||||||
Path.of(basePath.toAbsolutePath().toString(), tenantId);
|
Path.of(basePath.toAbsolutePath().toString(), tenantId);
|
||||||
return URI.create("kestra:///" + prefix.relativize(path));
|
return URI.create("kestra:///" + prefix.relativize(path).toString().replace("\\", "/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parentTraversalGuard(URI uri) {
|
private void parentTraversalGuard(URI uri) {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "vitest run",
|
"test:unit": "vitest run",
|
||||||
"test:lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs",
|
"test:lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix",
|
||||||
|
"translations:check": "node ./src/translations/check.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@js-joda/core": "^5.6.3",
|
"@js-joda/core": "^5.6.3",
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<svg width="169" height="146" viewBox="0 0 169 146" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.4" d="M129.725 83.5475C123.348 107.696 98.6012 122.103 74.4526 115.725C50.3039 109.348 35.8975 84.6014 42.2749 60.4528C48.6524 36.3041 73.3987 21.8977 97.5473 28.2752C121.696 34.6526 136.102 59.3989 129.725 83.5475Z" fill="#1C1E27" stroke="#E93ED1" stroke-linejoin="round"/>
|
||||||
|
<g filter="url(#filter0_d_3247_30504)">
|
||||||
|
<path d="M127.096 42.8848C130.859 48.1869 133.626 54.2556 135.113 60.8241L134.684 60.9214C135.393 64.0538 135.809 67.3012 135.9 70.6344C135.991 73.9675 135.754 77.2329 135.217 80.3994L135.651 80.473C134.525 87.1131 132.095 93.324 128.627 98.8241L128.254 98.5893C124.76 104.131 120.203 108.944 114.861 112.737L115.116 113.096C109.814 116.859 103.745 119.626 97.1762 121.113L97.079 120.684C93.9466 121.393 90.6991 121.809 87.366 121.9C84.0328 121.991 80.7675 121.754 77.601 121.217L77.5274 121.651C70.8873 120.525 64.6763 118.095 59.1763 114.627L59.4111 114.254C53.8696 110.76 49.056 106.203 45.2638 100.861L44.9048 101.116C41.1411 95.8135 38.3747 89.7448 36.8874 83.1762L37.3167 83.079C36.6075 79.9466 36.1916 76.6991 36.1004 73.366C36.0091 70.0328 36.2468 66.7675 36.7836 63.6009L36.3496 63.5273C37.4753 56.8873 39.9057 50.6763 43.3737 45.1763L43.7461 45.4111C47.2404 39.8695 51.7975 35.0559 57.1396 31.2638L56.8848 30.9048C62.1869 27.141 68.2556 24.3746 74.8242 22.8873L74.9214 23.3167C78.0538 22.6074 81.3013 22.1916 84.6344 22.1003C87.9676 22.0091 91.2329 22.2467 94.3994 22.7836L94.473 22.3495C101.113 23.4753 107.324 25.9056 112.824 29.3737L112.589 29.7461C118.131 33.2404 122.944 37.7975 126.737 43.1396L127.096 42.8848Z" stroke="#9470FF" stroke-width="0.880475" stroke-linejoin="round" stroke-dasharray="21.13 21.13" shape-rendering="crispEdges"/>
|
||||||
|
</g>
|
||||||
|
<line x1="165.701" y1="72.5" x2="141.883" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="42.6736" y1="36.2307" x2="26.1508" y2="19.0765" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(0.73486 -0.678218 -0.678218 -0.73486 130.917 35.3833)" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="132.256" y1="118.383" x2="148.779" y2="135.537" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(-0.73486 0.678218 0.678218 0.73486 44.0134 119.23)" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<g filter="url(#filter1_dii_3247_30504)">
|
||||||
|
<path d="M74.9999 70.625C80.0599 70.625 84.1666 66.425 84.1666 61.25C84.1666 56.075 80.0599 51.875 74.9999 51.875C69.9399 51.875 65.8333 56.075 65.8333 61.25C65.8333 66.425 69.9399 70.625 74.9999 70.625Z" fill="#ED3ED5"/>
|
||||||
|
<path d="M102.5 76.25H93.3333C91.3166 76.25 89.6666 77.9375 89.6666 80V89.375C89.6666 91.4375 91.3166 93.125 93.3333 93.125H102.5C104.517 93.125 106.167 91.4375 106.167 89.375V80C106.167 77.9375 104.517 76.25 102.5 76.25Z" fill="#ED3ED5"/>
|
||||||
|
<path d="M96.4683 64.4375C97.2016 64.7937 97.9899 65 98.8333 65C101.858 65 104.333 62.4687 104.333 59.375C104.333 56.2813 101.858 53.75 98.8333 53.75C95.8083 53.75 93.3333 56.2813 93.3333 59.375C93.3333 60.2375 93.5349 61.0437 93.8833 61.7937L75.5316 80.5625C74.7983 80.2062 74.0099 80 73.1666 80C70.1416 80 67.6666 82.5313 67.6666 85.625C67.6666 88.7187 70.1416 91.25 73.1666 91.25C76.1916 91.25 78.6666 88.7187 78.6666 85.625C78.6666 84.7625 78.4649 83.9562 78.1166 83.2062L96.4683 64.4375Z" fill="#ED3ED5"/>
|
||||||
|
</g>
|
||||||
|
<line x1="86.5" y1="126.021" x2="86.5" y2="134.802" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="86.5" y1="7.27393" x2="86.5" y2="16.0542" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="30.1165" y1="72.5" x2="6.29907" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_3247_30504" x="32.9997" y="21.6411" width="106.001" height="106.001" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="2.64143"/>
|
||||||
|
<feGaussianBlur stdDeviation="1.32071"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.432266 0 0 0 0 0.00354165 0 0 0 0 0.846458 0 0 0 1 0"/>
|
||||||
|
<feBlend mode="screen" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30504"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30504" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter1_dii_3247_30504" x="50.8333" y="36.875" width="70.3333" height="71.25" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="7.5"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.950882 0 0 0 0 0.165557 0 0 0 0 0.859261 0 0 0 0.62 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30504"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30504" result="shape"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="4"/>
|
||||||
|
<feGaussianBlur stdDeviation="4"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0.35 0"/>
|
||||||
|
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_3247_30504"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="2"/>
|
||||||
|
<feGaussianBlur stdDeviation="3"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.45 0"/>
|
||||||
|
<feBlend mode="plus-lighter" in2="effect2_innerShadow_3247_30504" result="effect3_innerShadow_3247_30504"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,53 @@
|
|||||||
|
<svg width="169" height="146" viewBox="0 0 169 146" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.4" d="M129.725 83.5475C123.348 107.696 98.6012 122.103 74.4526 115.725C50.3039 109.348 35.8975 84.6014 42.2749 60.4528C48.6524 36.3041 73.3987 21.8977 97.5473 28.2752C121.696 34.6526 136.102 59.3989 129.725 83.5475Z" fill="#D1CFE9" stroke="#E93ED1" stroke-linejoin="round"/>
|
||||||
|
<g filter="url(#filter0_d_3247_30783)">
|
||||||
|
<path d="M127.096 42.8848C130.859 48.1869 133.626 54.2556 135.113 60.8241L134.684 60.9214C135.393 64.0538 135.809 67.3012 135.9 70.6344C135.991 73.9675 135.754 77.2329 135.217 80.3994L135.651 80.473C134.525 87.1131 132.095 93.324 128.627 98.8241L128.254 98.5893C124.76 104.131 120.203 108.944 114.861 112.737L115.116 113.096C109.814 116.859 103.745 119.626 97.1762 121.113L97.079 120.684C93.9466 121.393 90.6991 121.809 87.366 121.9C84.0328 121.991 80.7675 121.754 77.601 121.217L77.5274 121.651C70.8873 120.525 64.6763 118.095 59.1763 114.627L59.4111 114.254C53.8696 110.76 49.056 106.203 45.2638 100.861L44.9048 101.116C41.1411 95.8135 38.3747 89.7448 36.8874 83.1762L37.3167 83.079C36.6075 79.9466 36.1916 76.6991 36.1004 73.366C36.0091 70.0328 36.2468 66.7675 36.7836 63.6009L36.3496 63.5273C37.4753 56.8873 39.9057 50.6763 43.3737 45.1763L43.7461 45.4111C47.2404 39.8695 51.7975 35.0559 57.1396 31.2638L56.8848 30.9048C62.1869 27.141 68.2556 24.3746 74.8242 22.8873L74.9214 23.3167C78.0538 22.6074 81.3013 22.1916 84.6344 22.1003C87.9676 22.0091 91.2329 22.2467 94.3994 22.7836L94.473 22.3495C101.113 23.4753 107.324 25.9056 112.824 29.3737L112.589 29.7461C118.131 33.2404 122.944 37.7975 126.737 43.1396L127.096 42.8848Z" stroke="#9470FF" stroke-width="0.880475" stroke-linejoin="round" stroke-dasharray="21.13 21.13" shape-rendering="crispEdges"/>
|
||||||
|
</g>
|
||||||
|
<line x1="165.701" y1="72.5" x2="141.883" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="42.6736" y1="36.2307" x2="26.1508" y2="19.0765" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(0.73486 -0.678218 -0.678218 -0.73486 130.917 35.3833)" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="132.256" y1="118.383" x2="148.779" y2="135.537" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(-0.73486 0.678218 0.678218 0.73486 44.0134 119.23)" stroke="#3991FF" stroke-dasharray="2 2"/>
|
||||||
|
<g filter="url(#filter1_dii_3247_30783)">
|
||||||
|
<path d="M74.9999 70.625C80.0599 70.625 84.1666 66.425 84.1666 61.25C84.1666 56.075 80.0599 51.875 74.9999 51.875C69.9399 51.875 65.8333 56.075 65.8333 61.25C65.8333 66.425 69.9399 70.625 74.9999 70.625Z" fill="#ED3ED5"/>
|
||||||
|
<path d="M102.5 76.25H93.3333C91.3166 76.25 89.6666 77.9375 89.6666 80V89.375C89.6666 91.4375 91.3166 93.125 93.3333 93.125H102.5C104.517 93.125 106.167 91.4375 106.167 89.375V80C106.167 77.9375 104.517 76.25 102.5 76.25Z" fill="#ED3ED5"/>
|
||||||
|
<path d="M96.4683 64.4375C97.2016 64.7937 97.9899 65 98.8333 65C101.858 65 104.333 62.4687 104.333 59.375C104.333 56.2813 101.858 53.75 98.8333 53.75C95.8083 53.75 93.3333 56.2813 93.3333 59.375C93.3333 60.2375 93.5349 61.0437 93.8833 61.7937L75.5316 80.5625C74.7983 80.2062 74.0099 80 73.1666 80C70.1416 80 67.6666 82.5313 67.6666 85.625C67.6666 88.7187 70.1416 91.25 73.1666 91.25C76.1916 91.25 78.6666 88.7187 78.6666 85.625C78.6666 84.7625 78.4649 83.9562 78.1166 83.2062L96.4683 64.4375Z" fill="#ED3ED5"/>
|
||||||
|
</g>
|
||||||
|
<line x1="86.5" y1="126.021" x2="86.5" y2="134.802" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="86.5" y1="7.27393" x2="86.5" y2="16.0542" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<line x1="30.1165" y1="72.5" x2="6.29907" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_3247_30783" x="32.9997" y="21.6411" width="106.001" height="106.001" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="2.64143"/>
|
||||||
|
<feGaussianBlur stdDeviation="1.32071"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.432266 0 0 0 0 0.00354165 0 0 0 0 0.846458 0 0 0 1 0"/>
|
||||||
|
<feBlend mode="screen" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30783"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30783" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter1_dii_3247_30783" x="50.8333" y="36.875" width="70.3333" height="71.25" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="7.5"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.950882 0 0 0 0 0.165557 0 0 0 0 0.859261 0 0 0 0.05 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30783"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30783" result="shape"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="4"/>
|
||||||
|
<feGaussianBlur stdDeviation="4"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0.35 0"/>
|
||||||
|
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_3247_30783"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="2"/>
|
||||||
|
<feGaussianBlur stdDeviation="3"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.45 0"/>
|
||||||
|
<feBlend mode="plus-lighter" in2="effect2_innerShadow_3247_30783" result="effect3_innerShadow_3247_30783"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -10,10 +10,15 @@
|
|||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<component :is="embedActiveTab || tab.disabled || tab.locked ? 'a' : 'router-link'" @click="embeddedTabChange(tab)" :to="embedActiveTab ? undefined : to(tab)" :data-test-id="tab.name">
|
<component :is="embedActiveTab || tab.disabled || tab.locked ? 'a' : 'router-link'" @click="embeddedTabChange(tab)" :to="embedActiveTab ? undefined : to(tab)" :data-test-id="tab.name">
|
||||||
<enterprise-tooltip :disabled="tab.locked" :term="tab.name" content="tabs">
|
<el-tooltip v-if="tab.disabled && tab.props && tab.props.showTooltip" :content="$t('add-trigger-in-editor')" placement="top">
|
||||||
{{ tab.title }}
|
<span><strong>{{ tab.title }}</strong></span>
|
||||||
<el-badge :type="tab.count > 0 ? 'danger' : 'primary'" :value="tab.count" v-if="tab.count !== undefined" />
|
</el-tooltip>
|
||||||
</enterprise-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>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -133,6 +138,7 @@
|
|||||||
},
|
},
|
||||||
to(tab) {
|
to(tab) {
|
||||||
if (this.activeTab === tab) {
|
if (this.activeTab === tab) {
|
||||||
|
this.setActiveName()
|
||||||
return this.$route;
|
return this.$route;
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@@ -224,4 +230,3 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<el-col :xs="24" :sm="8" :lg="4">
|
<el-col :xs="24" :sm="8" :lg="4">
|
||||||
<refresh-button
|
<refresh-button
|
||||||
class="float-right"
|
class="float-right"
|
||||||
@refresh="fetchAll()"
|
@refresh="refresh()"
|
||||||
:can-auto-refresh="canAutoRefresh"
|
:can-auto-refresh="canAutoRefresh"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
<Card
|
<Card
|
||||||
:icon="CheckBold"
|
:icon="CheckBold"
|
||||||
:label="t('dashboard.success_ratio')"
|
:label="t('dashboard.success_ratio')"
|
||||||
|
:tooltip="t('dashboard.success_ratio_tooltip')"
|
||||||
:value="stats.success"
|
:value="stats.success"
|
||||||
:redirect="{
|
:redirect="{
|
||||||
name: 'executions/list',
|
name: 'executions/list',
|
||||||
@@ -77,6 +78,7 @@
|
|||||||
<Card
|
<Card
|
||||||
:icon="Alert"
|
:icon="Alert"
|
||||||
:label="t('dashboard.failure_ratio')"
|
:label="t('dashboard.failure_ratio')"
|
||||||
|
:tooltip="t('dashboard.failure_ratio_tooltip')"
|
||||||
:value="stats.failed"
|
:value="stats.failed"
|
||||||
:redirect="{
|
:redirect="{
|
||||||
name: 'executions/list',
|
name: 'executions/list',
|
||||||
@@ -140,7 +142,10 @@
|
|||||||
v-model="descriptionDialog"
|
v-model="descriptionDialog"
|
||||||
:title="$t('description')"
|
:title="$t('description')"
|
||||||
>
|
>
|
||||||
<Markdown :source="description" class="p-4 description" />
|
<Markdown
|
||||||
|
:source="description"
|
||||||
|
class="p-4 description"
|
||||||
|
/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -197,7 +202,6 @@
|
|||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import _cloneDeep from "lodash/cloneDeep";
|
|
||||||
|
|
||||||
import {apiUrl} from "override/utils/route";
|
import {apiUrl} from "override/utils/route";
|
||||||
import State from "../../utils/state";
|
import State from "../../utils/state";
|
||||||
@@ -228,6 +232,7 @@
|
|||||||
import BookOpenOutline from "vue-material-design-icons/BookOpenOutline.vue";
|
import BookOpenOutline from "vue-material-design-icons/BookOpenOutline.vue";
|
||||||
import permission from "../../models/permission.js";
|
import permission from "../../models/permission.js";
|
||||||
import action from "../../models/action.js";
|
import action from "../../models/action.js";
|
||||||
|
import {storageKeys} from "../../utils/constants";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -235,6 +240,7 @@
|
|||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
const user = store.getters["auth/user"];
|
const user = store.getters["auth/user"];
|
||||||
|
|
||||||
|
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE) || null;
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
embed: {
|
embed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -254,6 +260,10 @@
|
|||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
restoreURL:{
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const descriptionDialog = ref(false);
|
const descriptionDialog = ref(false);
|
||||||
@@ -271,6 +281,13 @@
|
|||||||
scope: ["USER"],
|
scope: ["USER"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
await updateParams({
|
||||||
|
startDate: filters.value.startDate,
|
||||||
|
endDate: moment().toISOString(true),
|
||||||
|
});
|
||||||
|
fetchAll();
|
||||||
|
};
|
||||||
const canAutoRefresh = ref(false);
|
const canAutoRefresh = ref(false);
|
||||||
const toggleAutoRefresh = (event) => {
|
const toggleAutoRefresh = (event) => {
|
||||||
canAutoRefresh.value = event;
|
canAutoRefresh.value = event;
|
||||||
@@ -290,29 +307,45 @@
|
|||||||
const executions = ref({raw: {}, all: {}, yesterday: {}, today: {}});
|
const executions = ref({raw: {}, all: {}, yesterday: {}, today: {}});
|
||||||
const stats = computed(() => {
|
const stats = computed(() => {
|
||||||
const counts = executions?.value?.all?.executionCounts || {};
|
const counts = executions?.value?.all?.executionCounts || {};
|
||||||
const total = Object.values(counts).reduce((sum, count) => sum + count, 0);
|
const terminatedStates = State.getTerminatedStates();
|
||||||
|
const statesToCount = Object.fromEntries(
|
||||||
|
Object.entries(counts).filter(([key]) =>
|
||||||
|
terminatedStates.includes(key),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
function percentage(count, total) {
|
const total = Object.values(statesToCount).reduce(
|
||||||
return total ? ((count / total) * 100).toFixed(2) : "0.00";
|
(sum, count) => sum + count,
|
||||||
}
|
0,
|
||||||
|
);
|
||||||
|
const successStates = ["SUCCESS", "CANCELLED", "WARNING"];
|
||||||
|
const failedStates = ["FAILED", "KILLED", "RETRIED"];
|
||||||
|
const sumStates = (states) =>
|
||||||
|
states.reduce((sum, state) => sum + (statesToCount[state] || 0), 0);
|
||||||
|
|
||||||
|
const successRatio =
|
||||||
|
total > 0 ? (sumStates(successStates) / total) * 100 : 0;
|
||||||
|
const failedRatio = total > 0 ? (sumStates(failedStates) / total) * 100 : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total,
|
total,
|
||||||
success: `${percentage(counts[State.SUCCESS] || 0, total)}%`,
|
success: `${successRatio.toFixed(2)}%`,
|
||||||
failed: `${percentage(counts[State.FAILED] || 0, total)}%`,
|
failed: `${failedRatio.toFixed(2)}%`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const transformer = (data) => {
|
const transformer = (data) => {
|
||||||
return data.reduce((accumulator, value) => {
|
return data.reduce((accumulator, value) => {
|
||||||
if (!accumulator) accumulator = _cloneDeep(value);
|
accumulator = accumulator || {executionCounts: {}, duration: {}};
|
||||||
else {
|
|
||||||
for (const key in value.executionCounts) {
|
|
||||||
accumulator.executionCounts[key] += value.executionCounts[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in value.duration) {
|
for (const key in value.executionCounts) {
|
||||||
accumulator.duration[key] += value.duration[key];
|
accumulator.executionCounts[key] =
|
||||||
}
|
(accumulator.executionCounts[key] || 0) +
|
||||||
|
value.executionCounts[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in value.duration) {
|
||||||
|
accumulator.duration[key] =
|
||||||
|
(accumulator.duration[key] || 0) + value.duration[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
return accumulator;
|
return accumulator;
|
||||||
@@ -427,7 +460,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
filters.value.namespace = route.query.namespace ?? null;
|
if (!route.query.namespace && props.restoreURL) {
|
||||||
|
router.replace({query: {...route.query, namespace: defaultNamespace}});
|
||||||
|
filters.value.namespace = route.query.namespace || defaultNamespace;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
filters.value.namespace = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
updateParams();
|
updateParams();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,14 @@
|
|||||||
<div class="p-4 card">
|
<div class="p-4 card">
|
||||||
<div class="d-flex pb-2 justify-content-between">
|
<div class="d-flex pb-2 justify-content-between">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<component :is="icon" class="me-2 fs-4 icons" />
|
<el-tooltip
|
||||||
|
v-if="tooltip"
|
||||||
|
:content="tooltip"
|
||||||
|
popper-class="dashboard-card-tooltip"
|
||||||
|
>
|
||||||
|
<component :is="icon" class="me-2 fs-4 icons" />
|
||||||
|
</el-tooltip>
|
||||||
|
<component v-else :is="icon" class="me-2 fs-4 icons" />
|
||||||
|
|
||||||
<p class="m-0 fs-6 label">
|
<p class="m-0 fs-6 label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -31,6 +38,10 @@
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
tooltip: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
required: true,
|
required: true,
|
||||||
@@ -63,3 +74,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.dashboard-card-tooltip {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div class="p-4 responsive-container">
|
||||||
<div class="d-flex flex justify-content-between pb-4">
|
<div class="d-flex flex-wrap justify-content-between pb-4 info-container">
|
||||||
<div>
|
<div class="info-block">
|
||||||
<p class="m-0 fs-6">
|
<p class="m-0 fs-6">
|
||||||
<span class="fw-bold">{{ t("executions") }}</span>
|
<span class="fw-bold">{{ t("executions") }}</span>
|
||||||
<span class="fw-light small">
|
<span class="fw-light small">
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="switch-container">
|
||||||
<div class="d-flex justify-content-end align-items-center">
|
<div class="d-flex justify-content-end align-items-center switch-content">
|
||||||
<span class="pe-2 fw-light small">{{ t("duration") }}</span>
|
<span class="pe-2 fw-light small">{{ t("duration") }}</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="duration"
|
v-model="duration"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref, onMounted, onUnmounted} from "vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
import Check from "vue-material-design-icons/Check.vue";
|
import Check from "vue-material-design-icons/Check.vue";
|
||||||
|
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
|
const isSmallScreen = ref(window.innerWidth < 610);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -106,9 +107,20 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
isSmallScreen.value = window.innerWidth < 610;
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const options = computed(() =>
|
const options = computed(() =>
|
||||||
defaultConfig({
|
defaultConfig({
|
||||||
barThickness: 12,
|
barThickness: isSmallScreen.value ? 8 : 12,
|
||||||
skipNull: true,
|
skipNull: true,
|
||||||
borderSkipped: false,
|
borderSkipped: false,
|
||||||
borderColor: "transparent",
|
borderColor: "transparent",
|
||||||
@@ -141,7 +153,7 @@
|
|||||||
display: true,
|
display: true,
|
||||||
stacked: true,
|
stacked: true,
|
||||||
ticks: {
|
ticks: {
|
||||||
maxTicksLimit: 8,
|
maxTicksLimit: isSmallScreen.value ? 5 : 8,
|
||||||
callback: function (value) {
|
callback: function (value) {
|
||||||
const label = this.getLabelForValue(value);
|
const label = this.getLabelForValue(value);
|
||||||
const date = moment(new Date(label));
|
const date = moment(new Date(label));
|
||||||
@@ -156,7 +168,7 @@
|
|||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: !isSmallScreen.value,
|
||||||
text: t("executions"),
|
text: t("executions"),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@@ -166,12 +178,12 @@
|
|||||||
position: "left",
|
position: "left",
|
||||||
stacked: true,
|
stacked: true,
|
||||||
ticks: {
|
ticks: {
|
||||||
maxTicksLimit: 8,
|
maxTicksLimit: isSmallScreen.value ? 5 : 8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yB: {
|
yB: {
|
||||||
title: {
|
title: {
|
||||||
display: duration.value,
|
display: duration.value && !isSmallScreen.value,
|
||||||
text: t("duration"),
|
text: t("duration"),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@@ -180,7 +192,7 @@
|
|||||||
display: duration.value,
|
display: duration.value,
|
||||||
position: "right",
|
position: "right",
|
||||||
ticks: {
|
ticks: {
|
||||||
maxTicksLimit: 8,
|
maxTicksLimit: isSmallScreen.value ? 5 : 8,
|
||||||
callback: function (value) {
|
callback: function (value) {
|
||||||
return `${this.getLabelForValue(value)}s`;
|
return `${this.getLabelForValue(value)}s`;
|
||||||
},
|
},
|
||||||
@@ -193,22 +205,65 @@
|
|||||||
const duration = ref(true);
|
const duration = ref(true);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
Copy code
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||||
|
|
||||||
$height: 200px;
|
$height: 200px;
|
||||||
|
|
||||||
.tall {
|
.tall {
|
||||||
height: $height;
|
height: $height;
|
||||||
max-height: $height;
|
max-height: $height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
font-size: $font-size-xs;
|
font-size: $font-size-xs;
|
||||||
color: $gray-700;
|
color: $gray-700;
|
||||||
|
|
||||||
html.dark & {
|
html.dark & {
|
||||||
color: $gray-300;
|
color: $gray-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
@media (max-width: 610px) {
|
||||||
|
.responsive-container {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-block {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-content {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-6 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pe-2 {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
y: {
|
y: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: t("executions"),
|
text: t("logs"),
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
|
|||||||
@@ -28,7 +28,10 @@
|
|||||||
v-else
|
v-else
|
||||||
:model-value="!scope.row.disabled"
|
:model-value="!scope.row.disabled"
|
||||||
@change="
|
@change="
|
||||||
toggleState(scope.row.triggerContext);
|
toggleState(
|
||||||
|
scope.row.triggerContext,
|
||||||
|
!scope.row.disabled,
|
||||||
|
);
|
||||||
scope.row.disabled = !scope.row.disabled;
|
scope.row.disabled = !scope.row.disabled;
|
||||||
"
|
"
|
||||||
:active-icon="Check"
|
:active-icon="Check"
|
||||||
@@ -194,11 +197,8 @@
|
|||||||
() => loadExecutions(),
|
() => loadExecutions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleState = (trigger) => {
|
const toggleState = (trigger, disabled) => {
|
||||||
store.dispatch("trigger/update", {
|
store.dispatch("trigger/update", {...trigger, disabled});
|
||||||
...trigger,
|
|
||||||
disabled: !trigger.disabled,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
:persistent="false"
|
:persistent="false"
|
||||||
transition=""
|
transition=""
|
||||||
:hide-after="0"
|
:hide-after="0"
|
||||||
:content="$t('change status tooltip')"
|
:content="$t('change state tooltip')"
|
||||||
raw-content
|
raw-content
|
||||||
:placement="tooltipPosition"
|
:placement="tooltipPosition"
|
||||||
>
|
>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
:disabled="!enabled"
|
:disabled="!enabled"
|
||||||
class="ms-0 me-1"
|
class="ms-0 me-1"
|
||||||
>
|
>
|
||||||
{{ $t('change status') }}
|
{{ $t('change state') }}
|
||||||
</component>
|
</component>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<p v-html="$t('change execution status confirm', {id: execution.id})" />
|
<p v-html="$t('change execution state confirm', {id: execution.id})" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Current status is : <status size="small" class="me-1" :status="execution.state.current" />
|
Current status is : <status size="small" class="me-1" :status="execution.state.current" />
|
||||||
@@ -186,4 +186,4 @@
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
@click="visible = !visible"
|
@click="visible = !visible"
|
||||||
:disabled="!enabled"
|
:disabled="!enabled"
|
||||||
>
|
>
|
||||||
<span v-if="component !== 'el-button'">{{ $t('change status') }}</span>
|
<span v-if="component !== 'el-button'">{{ $t('change_status') }}</span>
|
||||||
|
|
||||||
<el-dialog v-if="enabled && visible" v-model="visible" :id="uuid" destroy-on-close :append-to-body="true">
|
<el-dialog v-if="enabled && visible" v-model="visible" :id="uuid" destroy-on-close :append-to-body="true">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<p v-html="$t('change status confirm', {id: execution.id, task: taskRun.taskId})" />
|
<p v-html="$t('change state confirm', {id: execution.id, task: taskRun.taskId})" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Current status is : <status size="small" class="me-1" :status="taskRun.state.current" />
|
Current status is : <status size="small" class="me-1" :status="taskRun.state.current" />
|
||||||
|
|||||||
@@ -44,8 +44,9 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="$route.name !== 'flows/update'">
|
<el-form-item v-if="$route.name !== 'flows/update'">
|
||||||
<namespace-select
|
<namespace-select
|
||||||
|
:value="selectedNamespace"
|
||||||
data-type="flow"
|
data-type="flow"
|
||||||
:value="$route.query.namespace"
|
:disabled="!!namespace"
|
||||||
@update:model-value="onDataTableValue('namespace', $event)"
|
@update:model-value="onDataTableValue('namespace', $event)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -133,17 +134,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #top v-if="showStatChart()">
|
<template #top>
|
||||||
<state-global-chart
|
<el-card v-if="showStatChart()" shadow="never" class="mb-4">
|
||||||
v-if="daily"
|
<ExecutionsBar v-if="daily" :data="daily" :total="executionsCount" />
|
||||||
class="mb-4"
|
</el-card>
|
||||||
:ready="dailyReady"
|
|
||||||
:data="daily"
|
|
||||||
:start-date="startDate"
|
|
||||||
:end-date="endDate"
|
|
||||||
:namespace="namespace"
|
|
||||||
:flow-id="flowId"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #table>
|
<template #table>
|
||||||
@@ -456,7 +450,6 @@
|
|||||||
import Filters from "../saved-filters/Filters.vue";
|
import Filters from "../saved-filters/Filters.vue";
|
||||||
import StatusFilterButtons from "../layout/StatusFilterButtons.vue"
|
import StatusFilterButtons from "../layout/StatusFilterButtons.vue"
|
||||||
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue"
|
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue"
|
||||||
import StateGlobalChart from "../../components/stats/StateGlobalChart.vue";
|
|
||||||
import Kicon from "../Kicon.vue"
|
import Kicon from "../Kicon.vue"
|
||||||
import Labels from "../layout/Labels.vue"
|
import Labels from "../layout/Labels.vue"
|
||||||
import RestoreUrl from "../../mixins/restoreUrl";
|
import RestoreUrl from "../../mixins/restoreUrl";
|
||||||
@@ -471,6 +464,7 @@
|
|||||||
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
|
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
|
||||||
import DateAgo from "../layout/DateAgo.vue";
|
import DateAgo from "../layout/DateAgo.vue";
|
||||||
import {h, ref} from "vue";
|
import {h, ref} from "vue";
|
||||||
|
import ExecutionsBar from "../../components/dashboard/components/charts/executions/Bar.vue"
|
||||||
|
|
||||||
import {filterLabels} from "./utils"
|
import {filterLabels} from "./utils"
|
||||||
|
|
||||||
@@ -488,13 +482,13 @@
|
|||||||
Filters,
|
Filters,
|
||||||
StatusFilterButtons,
|
StatusFilterButtons,
|
||||||
ScopeFilterButtons,
|
ScopeFilterButtons,
|
||||||
StateGlobalChart,
|
|
||||||
Kicon,
|
Kicon,
|
||||||
Labels,
|
Labels,
|
||||||
Id,
|
Id,
|
||||||
TriggerFlow,
|
TriggerFlow,
|
||||||
TopNavBar,
|
TopNavBar,
|
||||||
LabelInput
|
LabelInput,
|
||||||
|
ExecutionsBar
|
||||||
},
|
},
|
||||||
emits: ["state-count"],
|
emits: ["state-count"],
|
||||||
props: {
|
props: {
|
||||||
@@ -614,11 +608,6 @@
|
|||||||
selectedStatus: undefined
|
selectedStatus: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeCreate(){
|
|
||||||
if(!this.$route.query.scope) {
|
|
||||||
this.$route.query.scope = this.namespace === "system" ? ["SYSTEM"] : ["USER"];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
// allow to have different storage key for flow executions list
|
// allow to have different storage key for flow executions list
|
||||||
if (this.$route.name === "flows/update") {
|
if (this.$route.name === "flows/update") {
|
||||||
@@ -694,6 +683,26 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
executionsCount() {
|
||||||
|
return [...this.daily].reduce((a, b) => {
|
||||||
|
return a + Object.values(b.executionCounts).reduce((a, b) => a + b, 0);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
selectedNamespace(){
|
||||||
|
return this.namespace !== null && this.namespace !== undefined ? this.namespace : this.$route.query?.namespace;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeRouteEnter(to, from, next) {
|
||||||
|
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
|
||||||
|
const query = {...to.query};
|
||||||
|
if (defaultNamespace) {
|
||||||
|
query.namespace = defaultNamespace;
|
||||||
|
} if (!query.scope) {
|
||||||
|
query.scope = defaultNamespace === "system" ? ["SYSTEM"] : ["USER"];
|
||||||
|
}
|
||||||
|
next(vm => {
|
||||||
|
vm.$router?.replace({query});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
executionParams(row) {
|
executionParams(row) {
|
||||||
@@ -857,7 +866,7 @@
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
changeStatusToast() {
|
changeStatusToast() {
|
||||||
return this.$t("bulk change execution status", {"executionCount": this.queryBulkAction ? this.total : this.selection.length});
|
return this.$t("bulk change state", {"executionCount": this.queryBulkAction ? this.total : this.selection.length});
|
||||||
},
|
},
|
||||||
deleteExecutions() {
|
deleteExecutions() {
|
||||||
const includeNonTerminated = ref(false);
|
const includeNonTerminated = ref(false);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
:target-execution="execution"
|
:target-execution="execution"
|
||||||
:target-flow="flow"
|
:target-flow="flow"
|
||||||
:show-logs="taskTypeByTaskRunId[item.id] !== 'io.kestra.plugin.core.flow.ForEachItem' && taskTypeByTaskRunId[item.id] !== 'io.kestra.core.tasks.flows.ForEachItem'"
|
:show-logs="taskTypeByTaskRunId[item.id] !== 'io.kestra.plugin.core.flow.ForEachItem' && taskTypeByTaskRunId[item.id] !== 'io.kestra.core.tasks.flows.ForEachItem'"
|
||||||
|
class="mh-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
:total-count="countByLogLevel[logLevel]"
|
:total-count="countByLogLevel[logLevel]"
|
||||||
@previous="previousLogForLevel(logLevel)"
|
@previous="previousLogForLevel(logLevel)"
|
||||||
@next="nextLogForLevel(logLevel)"
|
@next="nextLogForLevel(logLevel)"
|
||||||
|
@close="clearLogLevel(logLevel)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -217,7 +218,17 @@
|
|||||||
|
|
||||||
const sortedIndices = [...logIndicesForLevel, this.logCursor].filter(Utils.distinctFilter).sort(this.sortLogsByViewOrder);
|
const sortedIndices = [...logIndicesForLevel, this.logCursor].filter(Utils.distinctFilter).sort(this.sortLogsByViewOrder);
|
||||||
this.logCursor = sortedIndices?.[sortedIndices.indexOf(this.logCursor) + 1] ?? sortedIndices[0];
|
this.logCursor = sortedIndices?.[sortedIndices.indexOf(this.logCursor) + 1] ?? sortedIndices[0];
|
||||||
}
|
},
|
||||||
|
clearLogLevel(level) {
|
||||||
|
if (this.logCursor !== undefined && this.cursorLogLevel === level) {
|
||||||
|
this.logCursor = undefined;
|
||||||
|
}
|
||||||
|
if (this.level === level) {
|
||||||
|
this.level = undefined;
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<p v-html="$t(replayOrRestart + ' confirm', {id: execution.id})" />
|
<p v-html="$t(replayOrRestart + ' confirm', {id: execution.id})" />
|
||||||
|
|
||||||
<el-form v-if="revisionsOptions && revisionsOptions.length > 1">
|
<el-form v-if="revisionsOptions && revisionsOptions.length > 1">
|
||||||
<p class="text-muted">
|
<p class="execution-description">
|
||||||
{{ $t("restart change revision") }}
|
{{ $t("restart change revision") }}
|
||||||
</p>
|
</p>
|
||||||
<el-form-item :label="$t('revisions')">
|
<el-form-item :label="$t('revisions')">
|
||||||
@@ -227,3 +227,8 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.execution-description {
|
||||||
|
color: var(--bs-gray-700);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -373,4 +373,9 @@
|
|||||||
.bordered {
|
.bordered {
|
||||||
border: 1px solid var(--bs-border-color)
|
border: 1px solid var(--bs-border-color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bordered > .el-collapse-item{
|
||||||
|
margin-bottom :0px !important
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -62,8 +62,9 @@
|
|||||||
} else if (this.$route.query.blueprintId && this.$route.query.blueprintSource) {
|
} else if (this.$route.query.blueprintId && this.$route.query.blueprintSource) {
|
||||||
this.source = await this.queryBlueprint(this.$route.query.blueprintId)
|
this.source = await this.queryBlueprint(this.$route.query.blueprintId)
|
||||||
} else {
|
} else {
|
||||||
|
const selectedNamespace = this.$route.query.namespace || "company.team";
|
||||||
this.source = `id: myflow
|
this.source = `id: myflow
|
||||||
namespace: company.team
|
namespace: ${selectedNamespace}
|
||||||
tasks:
|
tasks:
|
||||||
- id: hello
|
- id: hello
|
||||||
type: io.kestra.plugin.core.log.Log
|
type: io.kestra.plugin.core.log.Log
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<logs-wrapper :restore-url="false" embed />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import LogsWrapper from "../logs/LogsWrapper.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
LogsWrapper,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
75
ui/src/components/flows/FlowNoDependencies.vue
Normal file
75
ui/src/components/flows/FlowNoDependencies.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="no-dependencies-container">
|
||||||
|
<div>
|
||||||
|
<img :src="flowImage" alt="No dependencies">
|
||||||
|
</div>
|
||||||
|
<div class="no-dependencies-message">
|
||||||
|
<p>{{ $t("flow-no-dependencies") }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="no-dependencies-doc-message" :class="themeClass">
|
||||||
|
<p>
|
||||||
|
{{ $t("read-more") }}
|
||||||
|
<a
|
||||||
|
:href="dependenciesDocsUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="no-dependencies-doc-message doc-link"
|
||||||
|
>
|
||||||
|
{{ $t("flow-dependencies") }}
|
||||||
|
</a>
|
||||||
|
{{ $t("in-our-documentation") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import flowImageDark from "../../assets/onboarding/onboarding-flow-no-dependency-dark.svg"
|
||||||
|
import flowImageLight from "../../assets/onboarding/onboarding-flow-no-dependency-light.svg"
|
||||||
|
export default {
|
||||||
|
name: "NoDependencies",
|
||||||
|
computed: {
|
||||||
|
flowImage() {
|
||||||
|
return (localStorage.getItem("theme") || "light") === "light" ? flowImageLight : flowImageDark;
|
||||||
|
},
|
||||||
|
themeClass() {
|
||||||
|
return (localStorage.getItem("theme") || "light") === "light" ? "theme-light" : "theme-dark";
|
||||||
|
},
|
||||||
|
dependenciesDocsUrl() {
|
||||||
|
return "https://kestra.io/docs/ui/flows#dependencies";
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.no-dependencies-container {
|
||||||
|
padding: 180px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.no-dependencies-message {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: var(--el-font-size-small);
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
margin-bottom: -10px;
|
||||||
|
}
|
||||||
|
.no-dependencies-doc-message {
|
||||||
|
font-weight: 200;
|
||||||
|
font-size: var(--el-font-size-extra-small);
|
||||||
|
}
|
||||||
|
.theme-light {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.theme-dark {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.doc-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
.doc-link:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Topology from "./Topology.vue";
|
import Topology from "./Topology.vue";
|
||||||
import FlowRevisions from "./FlowRevisions.vue";
|
import FlowRevisions from "./FlowRevisions.vue";
|
||||||
import FlowLogs from "./FlowLogs.vue";
|
import LogsWrapper from "../logs/LogsWrapper.vue"
|
||||||
import FlowExecutions from "./FlowExecutions.vue";
|
import FlowExecutions from "./FlowExecutions.vue";
|
||||||
import RouteContext from "../../mixins/routeContext";
|
import RouteContext from "../../mixins/routeContext";
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
import Tabs from "../Tabs.vue";
|
import Tabs from "../Tabs.vue";
|
||||||
import Overview from "./Overview.vue";
|
import Overview from "./Overview.vue";
|
||||||
import FlowDependencies from "./FlowDependencies.vue";
|
import FlowDependencies from "./FlowDependencies.vue";
|
||||||
|
import FlowNoDependencies from "./FlowNoDependencies.vue";
|
||||||
import FlowMetrics from "./FlowMetrics.vue";
|
import FlowMetrics from "./FlowMetrics.vue";
|
||||||
import FlowEditor from "./FlowEditor.vue";
|
import FlowEditor from "./FlowEditor.vue";
|
||||||
import FlowTriggers from "./FlowTriggers.vue";
|
import FlowTriggers from "./FlowTriggers.vue";
|
||||||
@@ -201,7 +202,11 @@
|
|||||||
name: "triggers",
|
name: "triggers",
|
||||||
component: FlowTriggers,
|
component: FlowTriggers,
|
||||||
title: this.$t("triggers"),
|
title: this.$t("triggers"),
|
||||||
|
props: {
|
||||||
|
showTooltip: !this.flow.triggers || this.flow.triggers.length === 0
|
||||||
|
},
|
||||||
disabled: !this.flow.triggers,
|
disabled: !this.flow.triggers,
|
||||||
|
hideTitle: !this.flow.triggers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,8 +221,13 @@
|
|||||||
) {
|
) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
name: "logs",
|
name: "logs",
|
||||||
component: FlowLogs,
|
component: LogsWrapper,
|
||||||
title: this.$t("logs"),
|
title: this.$t("logs"),
|
||||||
|
props: {
|
||||||
|
showFilters: true,
|
||||||
|
restoreurl: false,
|
||||||
|
},
|
||||||
|
containerClass: "full-container p-4"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +257,7 @@
|
|||||||
) {
|
) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
name: "dependencies",
|
name: "dependencies",
|
||||||
component: FlowDependencies,
|
component: this.routeFlowDependencies,
|
||||||
title: this.$t("dependencies"),
|
title: this.$t("dependencies"),
|
||||||
count: this.dependenciesCount,
|
count: this.dependenciesCount,
|
||||||
});
|
});
|
||||||
@@ -317,6 +327,9 @@
|
|||||||
this.flow.namespace,
|
this.flow.namespace,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
routeFlowDependencies() {
|
||||||
|
return this.dependenciesCount > 0 ? FlowDependencies : FlowNoDependencies;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
this.$store.commit("flow/setFlow", undefined);
|
this.$store.commit("flow/setFlow", undefined);
|
||||||
@@ -331,4 +344,4 @@
|
|||||||
.body-color {
|
.body-color {
|
||||||
color: var(--bs-body-color);
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<el-form label-position="top" :model="inputs" ref="form" @submit.prevent="false">
|
<el-form label-position="top" :model="inputs" ref="form" @submit.prevent="false">
|
||||||
<inputs-form :initial-inputs="flow.inputs" :flow="flow" v-model="inputs" :execute-clicked="executeClicked" />
|
<inputs-form :initial-inputs="flow.inputs" :flow="flow" v-model="inputs" :execute-clicked="executeClicked" @confirm="onSubmit($refs.form)" />
|
||||||
|
|
||||||
<el-collapse class="mt-4" v-model="collapseName">
|
<el-collapse class="mt-4" v-model="collapseName">
|
||||||
<el-collapse-item :title="$t('advanced configuration')" name="advanced">
|
<el-collapse-item :title="$t('advanced configuration')" name="advanced">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
@click="onSubmit($refs.form); executeClicked = true;"
|
@click="onSubmit($refs.form); executeClicked = true;"
|
||||||
type="primary"
|
type="primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:disabled="flow.disabled || haveBadLabels"
|
:disabled="!flowCanBeExecuted"
|
||||||
>
|
>
|
||||||
{{ $t('launch execution') }}
|
{{ $t('launch execution') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -111,6 +111,9 @@
|
|||||||
haveBadLabels() {
|
haveBadLabels() {
|
||||||
return this.executionLabels.some(label => (label.key && !label.value) || (!label.key && label.value));
|
return this.executionLabels.some(label => (label.key && !label.value) || (!label.key && label.value));
|
||||||
},
|
},
|
||||||
|
flowCanBeExecuted() {
|
||||||
|
return this.flow && !this.flow.disabled && !this.haveBadLabels;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getExecutionLabels() {
|
getExecutionLabels() {
|
||||||
@@ -152,7 +155,7 @@
|
|||||||
return inputs;
|
return inputs;
|
||||||
},
|
},
|
||||||
onSubmit(formRef) {
|
onSubmit(formRef) {
|
||||||
if (formRef) {
|
if (formRef && this.flowCanBeExecuted) {
|
||||||
formRef.validate((valid) => {
|
formRef.validate((valid) => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return false;
|
return false;
|
||||||
@@ -165,9 +168,11 @@
|
|||||||
newTab: this.newTab,
|
newTab: this.newTab,
|
||||||
id: this.flow.id,
|
id: this.flow.id,
|
||||||
namespace: this.flow.namespace,
|
namespace: this.flow.namespace,
|
||||||
labels: this.executionLabels
|
labels: [...new Set(
|
||||||
.filter(label => label.key && label.value)
|
this.executionLabels
|
||||||
.map(label => `${label.key}:${label.value}`),
|
.filter(label => label.key && label.value)
|
||||||
|
.map(label => `${label.key}:${label.value}`)
|
||||||
|
)],
|
||||||
scheduleDate: this.$moment(this.scheduleDate).tz(localStorage.getItem(TIMEZONE_STORAGE_KEY) ?? moment.tz.guess()).toISOString(true),
|
scheduleDate: this.$moment(this.scheduleDate).tz(localStorage.getItem(TIMEZONE_STORAGE_KEY) ?? moment.tz.guess()).toISOString(true),
|
||||||
nextStep: true
|
nextStep: true
|
||||||
})
|
})
|
||||||
@@ -175,7 +180,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
state(input) {
|
state(input) {
|
||||||
const required = input.required === undefined ? true : input.required;
|
const required = input.required === undefined ? true : input.required;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<top-nav-bar :title="routeInfo.title">
|
<top-nav-bar v-if="topbar" :title="routeInfo.title">
|
||||||
<template #additional-right>
|
<template #additional-right>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{name: 'flows/create'}" v-if="canCreate">
|
<router-link :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" v-if="canCreate">
|
||||||
<el-button :icon="Plus" type="primary">
|
<el-button :icon="Plus" type="primary">
|
||||||
{{ $t('create') }}
|
{{ $t('create') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</top-nav-bar>
|
</top-nav-bar>
|
||||||
<section data-component="FILENAME_PLACEHOLDER" class="container" v-if="ready">
|
<section data-component="FILENAME_PLACEHOLDER" :class="{'container': topbar}" v-if="ready">
|
||||||
<div>
|
<div>
|
||||||
<data-table
|
<data-table
|
||||||
@page-changed="onPageChanged"
|
@page-changed="onPageChanged"
|
||||||
@@ -49,8 +49,9 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<namespace-select
|
<namespace-select
|
||||||
|
:value="selectedNamespace"
|
||||||
data-type="flow"
|
data-type="flow"
|
||||||
:value="$route.query.namespace"
|
:disabled="!!namespace"
|
||||||
@update:model-value="onDataTableValue('namespace', $event)"
|
@update:model-value="onDataTableValue('namespace', $event)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -67,18 +68,22 @@
|
|||||||
@update:model-value="onDataTableValue('labels', $event)"
|
@update:model-value="onDataTableValue('labels', $event)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-switch
|
||||||
|
:model-value="showChart"
|
||||||
|
@update:model-value="onShowChartChange"
|
||||||
|
:active-text="$t('show chart')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<filters :storage-key="storageKeys.FLOWS_FILTERS" />
|
<filters :storage-key="storageKeys.FLOWS_FILTERS" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #top>
|
<template #top>
|
||||||
<state-global-chart
|
<el-card v-if="showStatChart()" shadow="never" class="mb-4">
|
||||||
class="mb-4"
|
<ExecutionsBar :data="daily" :total="executionsCount" />
|
||||||
v-if="daily"
|
</el-card>
|
||||||
:ready="dailyReady"
|
|
||||||
:data="daily"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #table>
|
<template #table>
|
||||||
@@ -109,10 +114,10 @@
|
|||||||
<el-button v-if="canDelete" @click="deleteFlows" :icon="TrashCan">
|
<el-button v-if="canDelete" @click="deleteFlows" :icon="TrashCan">
|
||||||
{{ $t('delete') }}
|
{{ $t('delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="canUpdate" @click="enableFlows" :icon="FileDocumentCheckOutline">
|
<el-button v-if="canUpdate && anyFlowDisabled()" @click="enableFlows" :icon="FileDocumentCheckOutline">
|
||||||
{{ $t('enable') }}
|
{{ $t('enable') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="canUpdate" @click="disableFlows" :icon="FileDocumentRemoveOutline">
|
<el-button v-if="canUpdate && anyFlowEnabled()" @click="disableFlows" :icon="FileDocumentRemoveOutline">
|
||||||
{{ $t('disable') }}
|
{{ $t('disable') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</bulk-select>
|
</bulk-select>
|
||||||
@@ -245,7 +250,6 @@
|
|||||||
import DataTable from "../layout/DataTable.vue";
|
import DataTable from "../layout/DataTable.vue";
|
||||||
import SearchField from "../layout/SearchField.vue";
|
import SearchField from "../layout/SearchField.vue";
|
||||||
import StateChart from "../stats/StateChart.vue";
|
import StateChart from "../stats/StateChart.vue";
|
||||||
import StateGlobalChart from "../stats/StateGlobalChart.vue";
|
|
||||||
import Status from "../Status.vue";
|
import Status from "../Status.vue";
|
||||||
import TriggerAvatar from "./TriggerAvatar.vue";
|
import TriggerAvatar from "./TriggerAvatar.vue";
|
||||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue"
|
import MarkdownTooltip from "../layout/MarkdownTooltip.vue"
|
||||||
@@ -254,6 +258,7 @@
|
|||||||
import Upload from "vue-material-design-icons/Upload.vue";
|
import Upload from "vue-material-design-icons/Upload.vue";
|
||||||
import LabelFilter from "../labels/LabelFilter.vue";
|
import LabelFilter from "../labels/LabelFilter.vue";
|
||||||
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue"
|
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue"
|
||||||
|
import ExecutionsBar from "../../components/dashboard/components/charts/executions/Bar.vue"
|
||||||
import {storageKeys} from "../../utils/constants";
|
import {storageKeys} from "../../utils/constants";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -265,7 +270,6 @@
|
|||||||
DateAgo,
|
DateAgo,
|
||||||
SearchField,
|
SearchField,
|
||||||
StateChart,
|
StateChart,
|
||||||
StateGlobalChart,
|
|
||||||
Status,
|
Status,
|
||||||
TriggerAvatar,
|
TriggerAvatar,
|
||||||
MarkdownTooltip,
|
MarkdownTooltip,
|
||||||
@@ -274,7 +278,19 @@
|
|||||||
Upload,
|
Upload,
|
||||||
LabelFilter,
|
LabelFilter,
|
||||||
ScopeFilterButtons,
|
ScopeFilterButtons,
|
||||||
TopNavBar
|
TopNavBar,
|
||||||
|
ExecutionsBar
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
topbar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
namespace: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -285,6 +301,7 @@
|
|||||||
lastExecutionByFlowReady: false,
|
lastExecutionByFlowReady: false,
|
||||||
dailyReady: false,
|
dailyReady: false,
|
||||||
file: undefined,
|
file: undefined,
|
||||||
|
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_FLOWS_CHART)),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -318,20 +335,45 @@
|
|||||||
},
|
},
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this.user && this.user.isAllowed(permission.FLOW, action.UPDATE, this.$route.query.namespace);
|
return this.user && this.user.isAllowed(permission.FLOW, action.UPDATE, this.$route.query.namespace);
|
||||||
|
},
|
||||||
|
executionsCount() {
|
||||||
|
return [...this.daily].reduce((a, b) => {
|
||||||
|
return a + Object.values(b.executionCounts).reduce((a, b) => a + b, 0);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
selectedNamespace(){
|
||||||
|
return this.namespace !== null && this.namespace !== undefined ? this.namespace : this.$route.query?.namespace;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeCreate(){
|
beforeRouteEnter(to, from, next) {
|
||||||
if(!this.$route.query.scope) {
|
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
|
||||||
this.$route.query.scope = ["USER"]
|
const query = {...to.query};
|
||||||
|
if (defaultNamespace) {
|
||||||
|
query.namespace = defaultNamespace;
|
||||||
|
} if (!query.scope) {
|
||||||
|
query.scope = ["USER"];
|
||||||
}
|
}
|
||||||
|
next(vm => {
|
||||||
|
vm.$router?.replace({query});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectionMapper(element) {
|
selectionMapper(element) {
|
||||||
return {
|
return {
|
||||||
id: element.id,
|
id: element.id,
|
||||||
namespace: element.namespace
|
namespace: element.namespace,
|
||||||
|
enabled: !element.disabled
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showStatChart() {
|
||||||
|
return this.daily && this.showChart;
|
||||||
|
},
|
||||||
|
onShowChartChange(value) {
|
||||||
|
this.showChart = value;
|
||||||
|
localStorage.setItem(storageKeys.SHOW_FLOWS_CHART, value);
|
||||||
|
if(this.showStatChart())
|
||||||
|
this.loadStats();
|
||||||
|
},
|
||||||
exportFlows() {
|
exportFlows() {
|
||||||
this.$toast().confirm(
|
this.$toast().confirm(
|
||||||
this.$t("flow export", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
|
this.$t("flow export", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
|
||||||
@@ -386,6 +428,12 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
anyFlowDisabled() {
|
||||||
|
return this.selection.some(flow => !flow.enabled);
|
||||||
|
},
|
||||||
|
anyFlowEnabled() {
|
||||||
|
return this.selection.some(flow => flow.enabled);
|
||||||
|
},
|
||||||
enableFlows() {
|
enableFlows() {
|
||||||
this.$toast().confirm(
|
this.$toast().confirm(
|
||||||
this.$t("flow enable", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
|
this.$t("flow enable", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
|
||||||
@@ -483,10 +531,10 @@
|
|||||||
|
|
||||||
return _merge(base, queryFilter)
|
return _merge(base, queryFilter)
|
||||||
},
|
},
|
||||||
loadData(callback) {
|
loadStats() {
|
||||||
this.dailyReady = false;
|
this.dailyReady = false;
|
||||||
|
|
||||||
if (this.user.hasAny(permission.EXECUTION)) {
|
if (this.user.hasAny(permission.EXECUTION) && this.showStatChart) {
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch("stat/daily", this.loadQuery({
|
.dispatch("stat/daily", this.loadQuery({
|
||||||
startDate: this.$moment(this.startDate).add(-1, "day").startOf("day").toISOString(true),
|
startDate: this.$moment(this.startDate).add(-1, "day").startOf("day").toISOString(true),
|
||||||
@@ -496,6 +544,9 @@
|
|||||||
this.dailyReady = true;
|
this.dailyReady = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
loadData(callback) {
|
||||||
|
this.loadStats();
|
||||||
|
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch("flow/findFlows", this.loadQuery({
|
.dispatch("flow/findFlows", this.loadQuery({
|
||||||
@@ -540,7 +591,7 @@
|
|||||||
rowClasses(row) {
|
rowClasses(row) {
|
||||||
return row && row.row && row.row.disabled ? "disabled" : "";
|
return row && row.row && row.row.disabled ? "disabled" : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
<code>disabled</code>
|
<code>disabled</code>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<el-switch active-color="green" v-model="newMetadata.disabled" />
|
<el-switch active-color="green" v-model="newMetadata.disabled" @update:model-value="(value) => newMetadata.disabled = value" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
:icon="Minus"
|
:icon="Minus"
|
||||||
@click="deleteInput(index)"
|
@click="deleteInput(index)"
|
||||||
|
:disabled="index === 0 && newInputs.length === 1"
|
||||||
/>
|
/>
|
||||||
</el-button-group>
|
</el-button-group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
:icon="Minus"
|
:icon="Minus"
|
||||||
@click="deleteInput(index)"
|
@click="deleteInput(index)"
|
||||||
|
:disabled="index === 0 && newVariables.length === 1"
|
||||||
/>
|
/>
|
||||||
</el-button-group>
|
</el-button-group>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,8 +118,8 @@
|
|||||||
this.newVariables[index][1] = event;
|
this.newVariables[index][1] = event;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteInput(key) {
|
deleteInput(index) {
|
||||||
delete this.newVariables[key];
|
this.newVariables.splice(index, 1);
|
||||||
},
|
},
|
||||||
addVariable() {
|
addVariable() {
|
||||||
this.newVariables.push(["", undefined]);
|
this.newVariables.push(["", undefined]);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dashboard flow :flow-i-d="flow ? flow.id : undefined" embed />
|
<Dashboard :restore-u-r-l="false" flow :flow-i-d="flow ? flow.id : undefined" embed />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -284,9 +284,12 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (this.input) {
|
if (this.input) {
|
||||||
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyF, () => {});
|
|
||||||
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyH, () => {});
|
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyH, () => {});
|
||||||
this.editor.addCommand(KeyCode.F1, () => {});
|
this.editor.addCommand(KeyCode.F1, () => {});
|
||||||
|
|
||||||
|
if (!this.readOnly) {
|
||||||
|
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyF, () => { });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.original === undefined && this.navbar && this.fullHeight) {
|
if (this.original === undefined && this.navbar && this.fullHeight) {
|
||||||
@@ -332,7 +335,7 @@
|
|||||||
|
|
||||||
if (!this.fullHeight) {
|
if (!this.fullHeight) {
|
||||||
editor.onDidContentSizeChange(e => {
|
editor.onDidContentSizeChange(e => {
|
||||||
if(!this.$refs.container) return;
|
if(!this.$refs.container) return;
|
||||||
this.$refs.container.style.height = (e.contentHeight + this.customHeight) + "px";
|
this.$refs.container.style.height = (e.contentHeight + this.customHeight) + "px";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
import TaskEditor from "../flows/TaskEditor.vue";
|
import TaskEditor from "../flows/TaskEditor.vue";
|
||||||
import MetadataEditor from "../flows/MetadataEditor.vue";
|
import MetadataEditor from "../flows/MetadataEditor.vue";
|
||||||
import Editor from "./Editor.vue";
|
import Editor from "./Editor.vue";
|
||||||
import yamlUtils from "../../utils/yamlUtils";
|
|
||||||
import {SECTIONS} from "../../utils/constants.js";
|
import {SECTIONS} from "../../utils/constants.js";
|
||||||
import LowCodeEditor from "../inputs/LowCodeEditor.vue";
|
import LowCodeEditor from "../inputs/LowCodeEditor.vue";
|
||||||
import {editorViewTypes} from "../../utils/constants";
|
import {editorViewTypes} from "../../utils/constants";
|
||||||
@@ -406,7 +405,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updatePluginDocumentation = (event) => {
|
const updatePluginDocumentation = (event) => {
|
||||||
const taskType = yamlUtils.getTaskType(
|
const taskType = YamlUtils.getTaskType(
|
||||||
event.model.getValue(),
|
event.model.getValue(),
|
||||||
event.position
|
event.position
|
||||||
);
|
);
|
||||||
@@ -706,7 +705,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const save = async (e) => {
|
const save = async (e) => {
|
||||||
if (!currentTab?.value?.dirty && !props.isCreating) {
|
if (!haveChange.value && !props.isCreating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e) {
|
if (e) {
|
||||||
@@ -934,7 +933,8 @@
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<el-button @click="toggleExplorerVisibility()" class="toggle-button">
|
<el-button @click="toggleExplorerVisibility()">
|
||||||
|
<span class="pe-2 toggle-button">{{ t("files") }}</span>
|
||||||
<component :is="explorerVisible ? MenuOpen : MenuClose" />
|
<component :is="explorerVisible ? MenuOpen : MenuClose" />
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -1309,7 +1309,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle-button {
|
.toggle-button {
|
||||||
color: $secondary;
|
font-size: var(--el-font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
v-if="input.type === 'STRING' || input.type === 'URI'"
|
v-if="input.type === 'STRING' || input.type === 'URI'"
|
||||||
v-model="inputs[input.id]"
|
v-model="inputs[input.id]"
|
||||||
@update:model-value="onChange"
|
@update:model-value="onChange"
|
||||||
|
@confirm="onSubmit"
|
||||||
/>
|
/>
|
||||||
<el-select
|
<el-select
|
||||||
:full-height="false"
|
:full-height="false"
|
||||||
@@ -209,7 +210,7 @@
|
|||||||
multiSelectInputs: {},
|
multiSelectInputs: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue", "confirm"],
|
||||||
created() {
|
created() {
|
||||||
this.inputsList.push(...(this.initialInputs ?? []));
|
this.inputsList.push(...(this.initialInputs ?? []));
|
||||||
this.validateInputs();
|
this.validateInputs();
|
||||||
@@ -223,9 +224,10 @@
|
|||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
this._keyListener = function(e) {
|
this._keyListener = function(e) {
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
// Ctrl/Control + Enter
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onSubmit(this.$refs.form);
|
this.onSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,6 +250,9 @@
|
|||||||
onChange() {
|
onChange() {
|
||||||
this.$emit("update:modelValue", this.inputs);
|
this.$emit("update:modelValue", this.inputs);
|
||||||
},
|
},
|
||||||
|
onSubmit() {
|
||||||
|
this.$emit("confirm");
|
||||||
|
},
|
||||||
onMultiSelectChange(input, e) {
|
onMultiSelectChange(input, e) {
|
||||||
this.inputs[input] = JSON.stringify(e).toString();
|
this.inputs[input] = JSON.stringify(e).toString();
|
||||||
this.onChange();
|
this.onChange();
|
||||||
|
|||||||
@@ -302,6 +302,16 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose paste function globally for testing
|
||||||
|
window.pasteToEditor = (textToPaste) => {
|
||||||
|
this.editor.executeEdits("", [
|
||||||
|
{
|
||||||
|
range: this.editor.getSelection(),
|
||||||
|
text: textToPaste,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
beforeUnmount: function () {
|
beforeUnmount: function () {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
|||||||
@@ -185,10 +185,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-tooltip {
|
.markdown-tooltip {
|
||||||
*:last-child {
|
*:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
line-height: 15px;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,10 +14,16 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex side gap-2 flex-shrink-0">
|
<div class="d-flex side gap-2 flex-shrink-0 align-items-center">
|
||||||
<div class="d-none d-lg-flex align-items-center">
|
<div class="d-none d-lg-flex align-items-center">
|
||||||
<global-search class="trigger-flow-guided-step" />
|
<global-search class="trigger-flow-guided-step" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex side gap-2 flex-shrink-0 align-items-center">
|
||||||
|
<el-button v-if="shouldDisplayDeleteButton && logs !== undefined && logs.length > 0" @click="deleteLogs()">
|
||||||
|
<TrashCan class="me-2" />
|
||||||
|
<span>{{ $t("delete logs") }}</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
<slot name="additional-right" />
|
<slot name="additional-right" />
|
||||||
<div class="d-flex fixed-buttons">
|
<div class="d-flex fixed-buttons">
|
||||||
<el-dropdown popper-class="">
|
<el-dropdown popper-class="">
|
||||||
@@ -100,6 +106,7 @@
|
|||||||
import Update from "vue-material-design-icons/Update.vue";
|
import Update from "vue-material-design-icons/Update.vue";
|
||||||
import ProgressQuestion from "vue-material-design-icons/ProgressQuestion.vue";
|
import ProgressQuestion from "vue-material-design-icons/ProgressQuestion.vue";
|
||||||
import GlobalSearch from "./GlobalSearch.vue";
|
import GlobalSearch from "./GlobalSearch.vue";
|
||||||
|
import TrashCan from "vue-material-design-icons/TrashCan.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -113,6 +120,7 @@
|
|||||||
Update,
|
Update,
|
||||||
ProgressQuestion,
|
ProgressQuestion,
|
||||||
GlobalSearch,
|
GlobalSearch,
|
||||||
|
TrashCan,
|
||||||
Impersonating
|
Impersonating
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -128,6 +136,7 @@
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState("api", ["version"]),
|
...mapState("api", ["version"]),
|
||||||
...mapState("core", ["tutorialFlows"]),
|
...mapState("core", ["tutorialFlows"]),
|
||||||
|
...mapState("log", ["logs"]),
|
||||||
...mapGetters("core", ["guidedProperties"]),
|
...mapGetters("core", ["guidedProperties"]),
|
||||||
...mapGetters("auth", ["user"]),
|
...mapGetters("auth", ["user"]),
|
||||||
displayNavBar() {
|
displayNavBar() {
|
||||||
@@ -136,7 +145,10 @@
|
|||||||
tourEnabled(){
|
tourEnabled(){
|
||||||
// Temporary solution to not showing the tour menu item for EE
|
// Temporary solution to not showing the tour menu item for EE
|
||||||
return this.tutorialFlows?.length && !Object.keys(this.user).length
|
return this.tutorialFlows?.length && !Object.keys(this.user).length
|
||||||
}
|
},
|
||||||
|
shouldDisplayDeleteButton() {
|
||||||
|
return this.$route.name === "flows/update" && this.$route.params?.tab === "logs"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
restartGuidedTour() {
|
restartGuidedTour() {
|
||||||
@@ -144,6 +156,13 @@
|
|||||||
this.$store.commit("core/setGuidedProperties", {tourStarted: false});
|
this.$store.commit("core/setGuidedProperties", {tourStarted: false});
|
||||||
|
|
||||||
this.$tours["guidedTour"]?.start();
|
this.$tours["guidedTour"]?.start();
|
||||||
|
},
|
||||||
|
deleteLogs() {
|
||||||
|
this.$toast().confirm(
|
||||||
|
this.$t("delete_all_logs"),
|
||||||
|
() => this.$store.dispatch("log/deleteLogs", {namespace: this.namespace, flowId: this.flowId}),
|
||||||
|
() => {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<chevron-up class="medium-icon nav-button" @click="forwardEvent('previous')" />
|
<chevron-up class="medium-icon nav-button" @click="forwardEvent('previous')" />
|
||||||
<chevron-down class="medium-icon nav-button" @click="forwardEvent('next')" />
|
<chevron-down class="medium-icon nav-button" @click="forwardEvent('next')" />
|
||||||
|
<close class="medium-icon nav-button close-button" @click="forwardEvent('close')" v-if="isSelected" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
||||||
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
||||||
|
import Close from "vue-material-design-icons/Close.vue";
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
@@ -66,5 +68,12 @@
|
|||||||
.medium-icon {
|
.medium-icon {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<section v-bind="$attrs" :class="{'container': !embed}" class="log-panel">
|
<section v-bind="$attrs" :class="{'container': !embed}" class="log-panel">
|
||||||
<div class="log-content">
|
<div class="log-content">
|
||||||
<data-table @page-changed="onPageChanged" ref="dataTable" :total="total" :size="pageSize" :page="pageNumber" :embed="embed">
|
<data-table @page-changed="onPageChanged" ref="dataTable" :total="total" :size="pageSize" :page="pageNumber" :embed="embed">
|
||||||
<template #navbar v-if="!embed">
|
<template #navbar v-if="!embed || showFilters">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<search-field />
|
<search-field />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -29,6 +29,13 @@
|
|||||||
@update:filter-value="onDataTableValue"
|
@update:filter-value="onDataTableValue"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-switch
|
||||||
|
:model-value="showChart"
|
||||||
|
@update:model-value="onShowChartChange"
|
||||||
|
:active-text="$t('show chart')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<filters :storage-key="storageKeys.LOGS_FILTERS" />
|
<filters :storage-key="storageKeys.LOGS_FILTERS" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -37,16 +44,11 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="charts" #top>
|
<template v-if="showStatChart()" #top>
|
||||||
<el-card shadow="never" class="mb-3" v-loading="!statsReady">
|
<el-card shadow="never" class="mb-3" v-loading="!statsReady">
|
||||||
<div class="state-global-charts">
|
<div>
|
||||||
<template v-if="hasStatsData">
|
<template v-if="hasStatsData">
|
||||||
<log-chart
|
<Logs :data="logDaily" />
|
||||||
v-if="statsReady"
|
|
||||||
:data="logDaily"
|
|
||||||
:namespace="namespace"
|
|
||||||
:flow-id="flowId"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-alert type="info" :closable="false" class="m-0">
|
<el-alert type="info" :closable="false" class="m-0">
|
||||||
@@ -55,11 +57,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-button v-if="shouldDisplayDeleteButton && logs !== undefined && logs.length > 0" @click="deleteLogs()" class="mb-3 delete-logs-btn">
|
|
||||||
<TrashCan class="me-2" />
|
|
||||||
<span>{{ $t("delete logs") }}</span>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #table>
|
<template #table>
|
||||||
@@ -100,16 +97,16 @@
|
|||||||
import DataTable from "../../components/layout/DataTable.vue";
|
import DataTable from "../../components/layout/DataTable.vue";
|
||||||
import RefreshButton from "../../components/layout/RefreshButton.vue";
|
import RefreshButton from "../../components/layout/RefreshButton.vue";
|
||||||
import _merge from "lodash/merge";
|
import _merge from "lodash/merge";
|
||||||
import LogChart from "../stats/LogChart.vue";
|
import Logs from "../dashboard/components/charts/logs/Bar.vue";
|
||||||
import Filters from "../saved-filters/Filters.vue";
|
import Filters from "../saved-filters/Filters.vue";
|
||||||
import {storageKeys} from "../../utils/constants";
|
import {storageKeys} from "../../utils/constants";
|
||||||
import TrashCan from "vue-material-design-icons/TrashCan.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [RouteContext, RestoreUrl, DataTableActions],
|
mixins: [RouteContext, RestoreUrl, DataTableActions],
|
||||||
components: {
|
components: {
|
||||||
Filters,
|
Filters,
|
||||||
DataTable, LogLine, NamespaceSelect, DateFilter, SearchField, LogLevelSelector, RefreshButton, TopNavBar, LogChart, TrashCan},
|
DataTable, LogLine, NamespaceSelect, DateFilter, SearchField, LogLevelSelector, RefreshButton, TopNavBar, Logs},
|
||||||
props: {
|
props: {
|
||||||
logLevel: {
|
logLevel: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -123,6 +120,10 @@
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
showFilters: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
filters: {
|
filters: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null
|
||||||
@@ -140,7 +141,8 @@
|
|||||||
refreshDates: false,
|
refreshDates: false,
|
||||||
statsReady: false,
|
statsReady: false,
|
||||||
statsData: [],
|
statsData: [],
|
||||||
canAutoRefresh: false
|
canAutoRefresh: false,
|
||||||
|
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_LOGS_CHART)),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -157,9 +159,6 @@
|
|||||||
isFlowEdit() {
|
isFlowEdit() {
|
||||||
return this.$route.name === "flows/update"
|
return this.$route.name === "flows/update"
|
||||||
},
|
},
|
||||||
shouldDisplayDeleteButton() {
|
|
||||||
return this.$route.name === "flows/update"
|
|
||||||
},
|
|
||||||
isNamespaceEdit() {
|
isNamespaceEdit() {
|
||||||
return this.$route.name === "namespaces/update"
|
return this.$route.name === "namespaces/update"
|
||||||
},
|
},
|
||||||
@@ -199,10 +198,30 @@
|
|||||||
return this.countStats > 0;
|
return this.countStats > 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
beforeRouteEnter(to, from, next) {
|
||||||
|
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
|
||||||
|
const query = {...to.query};
|
||||||
|
if (defaultNamespace) {
|
||||||
|
query.namespace = defaultNamespace;
|
||||||
|
}
|
||||||
|
next(vm => {
|
||||||
|
vm.$router?.replace({query});
|
||||||
|
});
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDateFilterTypeChange(event) {
|
onDateFilterTypeChange(event) {
|
||||||
this.canAutoRefresh = event;
|
this.canAutoRefresh = event;
|
||||||
},
|
},
|
||||||
|
showStatChart() {
|
||||||
|
return this.charts && this.showChart;
|
||||||
|
},
|
||||||
|
onShowChartChange(value) {
|
||||||
|
this.showChart = value;
|
||||||
|
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value);
|
||||||
|
if (this.showStatChart()) {
|
||||||
|
this.loadStats();
|
||||||
|
}
|
||||||
|
},
|
||||||
refresh() {
|
refresh() {
|
||||||
this.refreshDates = !this.refreshDates;
|
this.refreshDates = !this.refreshDates;
|
||||||
this.load();
|
this.load();
|
||||||
@@ -262,13 +281,6 @@
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this.statsReady = true;
|
this.statsReady = true;
|
||||||
});
|
});
|
||||||
},
|
|
||||||
deleteLogs() {
|
|
||||||
this.$toast().confirm(
|
|
||||||
this.$t("delete_all_logs"),
|
|
||||||
() => this.$store.dispatch("log/deleteLogs", {namespace: this.namespace, flowId: this.flowId}),
|
|
||||||
() => {}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -302,8 +314,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.delete-logs-btn {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
15
ui/src/components/namespace/Executions.vue
Normal file
15
ui/src/components/namespace/Executions.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<Executions :restore-url="false" :namespace="$route.params.id || $route.query.id" :topbar="false" :hidden="['selection','inputs','flowRevision','taskRunList.taskId']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Executions from "../executions/Executions.vue"
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {Executions},
|
||||||
|
computed: {
|
||||||
|
...mapState("namespace", ["namespace"]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
11
ui/src/components/namespace/Flows.vue
Normal file
11
ui/src/components/namespace/Flows.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<Flows :restore-url="false" :namespace="$route.params.id || $route.query.id" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Flows from "../flows/Flows.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {Flows},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -48,8 +48,9 @@
|
|||||||
import permission from "../../models/permission";
|
import permission from "../../models/permission";
|
||||||
import action from "../../models/action";
|
import action from "../../models/action";
|
||||||
import Overview from "./Overview.vue";
|
import Overview from "./Overview.vue";
|
||||||
|
import Executions from "./Executions.vue";
|
||||||
import NamespaceKV from "./NamespaceKV.vue";
|
import NamespaceKV from "./NamespaceKV.vue";
|
||||||
import NamespaceFlows from "./NamespaceFlows.vue";
|
import Flows from "./Flows.vue";
|
||||||
import EditorView from "../inputs/EditorView.vue";
|
import EditorView from "../inputs/EditorView.vue";
|
||||||
import BlueprintsBrowser from "../../override/components/flows/blueprints/BlueprintsBrowser.vue";
|
import BlueprintsBrowser from "../../override/components/flows/blueprints/BlueprintsBrowser.vue";
|
||||||
import {apiUrl} from "override/utils/route";
|
import {apiUrl} from "override/utils/route";
|
||||||
@@ -135,15 +136,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "flows",
|
name: "flows",
|
||||||
component: NamespaceFlows,
|
component: Flows,
|
||||||
title: this.$t("flows"),
|
title: this.$t("flows"),
|
||||||
props: {
|
props: {
|
||||||
tab: "flows",
|
tab: "flows",
|
||||||
|
topbar:false,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
id: this.$route.query.id
|
id: this.$route.query.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "executions",
|
||||||
|
component: Executions,
|
||||||
|
props: {
|
||||||
|
embed: false,
|
||||||
|
},
|
||||||
|
title: this.$t("executions"),
|
||||||
|
query: {
|
||||||
|
id: this.$route.query.id
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "dependencies",
|
name: "dependencies",
|
||||||
component: NamespaceDependenciesWrapper,
|
component: NamespaceDependenciesWrapper,
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-table
|
<select-table
|
||||||
:data="kvs"
|
:data="kvs"
|
||||||
ref="table"
|
ref="selectTable"
|
||||||
:default-sort="{prop: 'id', order: 'ascending'}"
|
:default-sort="{prop: 'id', order: 'ascending'}"
|
||||||
stripe
|
stripe
|
||||||
table-layout="auto"
|
table-layout="auto"
|
||||||
fixed
|
fixed
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
>
|
>
|
||||||
|
<template #select-actions>
|
||||||
|
<bulk-select
|
||||||
|
:select-all="queryBulkAction"
|
||||||
|
:selections="selection"
|
||||||
|
@update:select-all="toggleAllSelection"
|
||||||
|
@unselect="toggleAllUnselected"
|
||||||
|
>
|
||||||
|
<el-button :icon="Delete" type="default" @click="removeKvs()">
|
||||||
|
{{ $t("delete") }}
|
||||||
|
</el-button>
|
||||||
|
</bulk-select>
|
||||||
|
</template>
|
||||||
<el-table-column prop="key" sortable="custom" :sort-orders="['ascending', 'descending']" :label="$t('key')">
|
<el-table-column prop="key" sortable="custom" :sort-orders="['ascending', 'descending']" :label="$t('key')">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<id :value="scope.row.key" :shrink="false" />
|
<id :value="scope.row.key" :shrink="false" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -27,7 +40,7 @@
|
|||||||
<el-button :icon="Delete" link @click="removeKv(scope.row.key)" />
|
<el-button :icon="Delete" link @click="removeKv(scope.row.key)" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</select-table>
|
||||||
|
|
||||||
<drawer
|
<drawer
|
||||||
v-if="addKvDrawerVisible"
|
v-if="addKvDrawerVisible"
|
||||||
@@ -115,6 +128,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import BulkSelect from "../layout/BulkSelect.vue";
|
||||||
|
import SelectTable from "../layout/SelectTable.vue";
|
||||||
import Editor from "../inputs/Editor.vue";
|
import Editor from "../inputs/Editor.vue";
|
||||||
import FileDocumentEdit from "vue-material-design-icons/FileDocumentEdit.vue";
|
import FileDocumentEdit from "vue-material-design-icons/FileDocumentEdit.vue";
|
||||||
import Delete from "vue-material-design-icons/Delete.vue";
|
import Delete from "vue-material-design-icons/Delete.vue";
|
||||||
@@ -127,8 +142,10 @@
|
|||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
import Drawer from "../Drawer.vue";
|
import Drawer from "../Drawer.vue";
|
||||||
import Id from "../Id.vue";
|
import Id from "../Id.vue";
|
||||||
|
import SelectTableActions from "../../mixins/selectTableActions";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [SelectTableActions],
|
||||||
components: {
|
components: {
|
||||||
Id,
|
Id,
|
||||||
Drawer
|
Drawer
|
||||||
@@ -195,6 +212,11 @@
|
|||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
this.resetKv();
|
this.resetKv();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"kv.type"() {
|
||||||
|
if (this.$refs.form) {
|
||||||
|
this.$refs.form.clearValidate("value");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -264,6 +286,19 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
removeKvs() {
|
||||||
|
let request = {"keys":[]}
|
||||||
|
this.selection.forEach((obj)=>{
|
||||||
|
request.keys.push(obj.key)
|
||||||
|
})
|
||||||
|
this.$toast().confirm(this.$t("delete confirm multiple",{name: request.keys.length}), () => {
|
||||||
|
return this.$store
|
||||||
|
.dispatch("namespace/deleteKvs", {namespace: this.$route.params.id, request: request})
|
||||||
|
.then(() => {
|
||||||
|
this.$toast().deleted(request.keys.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
saveKv(formRef) {
|
saveKv(formRef) {
|
||||||
formRef.validate((valid) => {
|
formRef.validate((valid) => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
this.$store
|
this.$store
|
||||||
.dispatch("namespace/loadNamespacesForDatatype", {dataType: this.dataType})
|
.dispatch("namespace/loadNamespacesForDatatype", {dataType: this.dataType})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.groupedNamespaces = this.groupNamespaces(this.datatypeNamespaces);
|
this.groupedNamespaces = this.groupNamespaces(this.datatypeNamespaces).filter(namespace => namespace.code !== "system");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dashboard :namespace="$route.params.id || $route.query.id" embed />
|
<Dashboard :restore-u-r-l="false" :namespace="$route.params.id || $route.query.id" embed />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -305,6 +305,12 @@
|
|||||||
this.expandParentIfNeeded();
|
this.expandParentIfNeeded();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
"$i18n.locale": {
|
||||||
|
deep: true,
|
||||||
|
handler(){
|
||||||
|
this.localMenu = this.disabledCurrentRoute(this.generateMenu());
|
||||||
|
}
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
handler(newVal, oldVal) {
|
handler(newVal, oldVal) {
|
||||||
// Check if the active menu item has changed, if yes then update the menu
|
// Check if the active menu item has changed, if yes then update the menu
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ export default {
|
|||||||
return tab.name === name;
|
return tab.name === name;
|
||||||
});
|
});
|
||||||
|
|
||||||
state.tabs[tabIdxToDirty].dirty = dirty;
|
if(state.tabs[tabIdxToDirty]) state.tabs[tabIdxToDirty].dirty = dirty;
|
||||||
state.current.dirty = dirty;
|
if(state.current) state.current.dirty = dirty;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeTabs(state) {
|
closeTabs(state) {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export default {
|
|||||||
metrics: [],
|
metrics: [],
|
||||||
aggregatedMetrics: undefined,
|
aggregatedMetrics: undefined,
|
||||||
tasksWithMetrics: [],
|
tasksWithMetrics: [],
|
||||||
executeFlow: false
|
executeFlow: false,
|
||||||
|
lastSaveFlow: undefined
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -313,6 +314,7 @@ export default {
|
|||||||
},
|
},
|
||||||
setFlow(state, flow) {
|
setFlow(state, flow) {
|
||||||
state.flow = flow;
|
state.flow = flow;
|
||||||
|
state.lastSaveFlow = flow;
|
||||||
// if (state.flowGraph !== undefined && state.flowGraphParam && flow) {
|
// if (state.flowGraph !== undefined && state.flowGraphParam && flow) {
|
||||||
// if (state.flowGraphParam.namespace !== flow.namespace || state.flowGraphParam.id !== flow.id) {
|
// if (state.flowGraphParam.namespace !== flow.namespace || state.flowGraphParam.id !== flow.id) {
|
||||||
// state.flowGraph = undefined
|
// state.flowGraph = undefined
|
||||||
@@ -389,6 +391,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
lastSaveFlow(state){
|
||||||
|
if(state.lastSavedFlow){
|
||||||
|
return state.lastSavedFlow;
|
||||||
|
}
|
||||||
|
},
|
||||||
flow(state) {
|
flow(state) {
|
||||||
if (state.flow) {
|
if (state.flow) {
|
||||||
return state.flow;
|
return state.flow;
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ export default {
|
|||||||
return dispatch("kvsList", {id: payload.namespace})
|
return dispatch("kvsList", {id: payload.namespace})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
deleteKvs({dispatch}, payload) {
|
||||||
|
return this.$http
|
||||||
|
.delete(`${apiUrl(this)}/namespaces/${payload.namespace}/kv`,{
|
||||||
|
data: payload.request
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return dispatch("kvsList", {id: payload.namespace})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Create a directory
|
// Create a directory
|
||||||
async createDirectory(_, payload) {
|
async createDirectory(_, payload) {
|
||||||
|
|||||||
@@ -1030,6 +1030,10 @@ form.ks-horizontal {
|
|||||||
border-radius: var(--el-border-radius-base);
|
border-radius: var(--el-border-radius-base);
|
||||||
height: var(--el-component-size);
|
height: var(--el-component-size);
|
||||||
|
|
||||||
|
.el-radio-button {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.el-radio-button__inner {
|
.el-radio-button__inner {
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
padding: 4px 15px;
|
padding: 4px 15px;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ html.dark {
|
|||||||
#{--bs-link-color-rgb}: to-rgb($secondary);
|
#{--bs-link-color-rgb}: to-rgb($secondary);
|
||||||
#{--bs-tertiary-color}: #C3BBE3;
|
#{--bs-tertiary-color}: #C3BBE3;
|
||||||
|
|
||||||
$levels: info, running, danger, warning, success;
|
$levels: info, running, danger, warning;
|
||||||
@each $level in $levels {
|
@each $level in $levels {
|
||||||
.bg-#{$level} {
|
.bg-#{$level} {
|
||||||
#{--bs-bg-opacity}: 0.2;
|
#{--bs-bg-opacity}: 0.2;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
$content-running: #7400df;
|
$content-running: #7400df;
|
||||||
$content-alert: #ab0009;
|
$content-alert: #ab0009;
|
||||||
$content-warning: #c15300;
|
$content-warning: #c15300;
|
||||||
$content-success: #017f5c;
|
$content-success: #03DABA;
|
||||||
#{--background-failed}: #fed6d9;
|
#{--background-failed}: #fed6d9;
|
||||||
#{--background-success}: #e4f9f3;
|
#{--background-success}: #e4f9f3;
|
||||||
#{--content-information}: $content-information;
|
#{--content-information}: $content-information;
|
||||||
@@ -178,4 +178,4 @@ $logLevels: "trace", "debug", "info", "warn", "error";
|
|||||||
|
|
||||||
.text-tertiary {
|
.text-tertiary {
|
||||||
color: var(--bs-tertiary-color);
|
color: var(--bs-tertiary-color);
|
||||||
}
|
}
|
||||||
|
|||||||
38
ui/src/translations/check.js
Normal file
38
ui/src/translations/check.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import {fileURLToPath} from "url";
|
||||||
|
|
||||||
|
const getPath = (lang) => path.resolve(path.dirname(fileURLToPath(import.meta.url)), `./${lang}.json`);
|
||||||
|
const readJSON = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||||
|
|
||||||
|
const getNestedKeys = (obj, prefix = "") =>
|
||||||
|
Object.keys(obj).reduce((keys, key) => {
|
||||||
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
keys.push(fullKey);
|
||||||
|
if (
|
||||||
|
typeof obj[key] === "object" &&
|
||||||
|
obj[key] &&
|
||||||
|
!Array.isArray(obj[key])
|
||||||
|
) {
|
||||||
|
keys.push(...getNestedKeys(obj[key], fullKey));
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Use English as a base language
|
||||||
|
const content = getNestedKeys(readJSON(getPath("en"))["en"]);
|
||||||
|
|
||||||
|
const languages = ["de", "es", "fr", "hi", "it", "ja", "ko", "pl", "pt", "ru", "zh_CN"];
|
||||||
|
const paths = languages.map((lang) => getPath(lang));
|
||||||
|
|
||||||
|
languages.forEach((lang, i) => {
|
||||||
|
const current = getNestedKeys(readJSON(paths[i])[lang]);
|
||||||
|
|
||||||
|
const missing = content.filter((key) => !current.includes(key));
|
||||||
|
const extra = current.filter((key) => !content.includes(key));
|
||||||
|
|
||||||
|
console.log(`---\n\x1b[34mComparison with ${lang.toUpperCase()}\x1b[0m \n`);
|
||||||
|
console.log(missing.length ? `Missing keys: \x1b[31m${missing.join(", ")}\x1b[0m` : "No missing keys.");
|
||||||
|
console.log(extra.length ? `Extra keys: \x1b[32m${extra.join(", ")}\x1b[0m` : "No extra keys.");
|
||||||
|
console.log("---\n");
|
||||||
|
});
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger ist entsperrt"
|
"success": "Trigger ist entsperrt"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Trigger neu starten"
|
"button": "Trigger neu starten",
|
||||||
|
"tooltip": "Den Trigger neu starten"
|
||||||
},
|
},
|
||||||
"date format": "Datumsformat",
|
"date format": "Datumsformat",
|
||||||
"timezone": "Zeitzone",
|
"timezone": "Zeitzone",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "Dieser Trigger kann nur durch Code aktiviert werden.",
|
"trigger_disabled": "Dieser Trigger kann nur durch Code aktiviert werden.",
|
||||||
"no_flow_description": "Dieser Flow hat keine Beschreibung.",
|
"no_flow_description": "Dieser Flow hat keine Beschreibung.",
|
||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"not_auth": "Sie haben nicht die erforderlichen Berechtigungen, um dieses Widget anzuzeigen."
|
"not_auth": "Sie haben nicht die erforderlichen Berechtigungen, um dieses Widget anzuzeigen.",
|
||||||
|
"see_all": "Alle anzeigen",
|
||||||
|
"success_ratio_tooltip": "Erfolgsquote ist die Summe der SUCCESS, CANCELLED und WARNING Ausführungen, geteilt durch die Gesamtanzahl der Ausführungen in einem beendeten Zustand.",
|
||||||
|
"failure_ratio_tooltip": "Der Fehlerrate ist die Summe der FAILED, KILLED und RETRIED Ausführungen, geteilt durch die Gesamtanzahl der Ausführungen in einem beendeten Zustand."
|
||||||
},
|
},
|
||||||
"delete_log": "Sind Sie sicher, dass Sie das Log löschen möchten?",
|
"delete_log": "Sind Sie sicher, dass Sie das Log löschen möchten?",
|
||||||
"docs": "Dokumentation",
|
"docs": "Dokumentation",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "Aktive Slots",
|
"active-slots": "Aktive Slots",
|
||||||
"concurrency": "Nebenläufigkeit",
|
"concurrency": "Nebenläufigkeit",
|
||||||
"open sidebar": "Seitenleiste öffnen",
|
"open sidebar": "Seitenleiste öffnen",
|
||||||
"close sidebar": "Seitenleiste schließen"
|
"close sidebar": "Seitenleiste schließen",
|
||||||
|
"change_status": "Status ändern",
|
||||||
|
"in-our-documentation": "in unserer Dokumentation.",
|
||||||
|
"read-more": "Mehr erfahren über",
|
||||||
|
"flow-dependencies": "Abhängigkeiten",
|
||||||
|
"flow-no-dependencies": "Ihr Flow hat keine Abhängigkeiten.",
|
||||||
|
"files": "Dateien",
|
||||||
|
"add-trigger-in-editor": "Fügen Sie zuerst einen Trigger zu Ihrem Flow hinzu",
|
||||||
|
"delete confirm multiple": "Sind Sie sicher, dass Sie <code>{name}</code> KV(s) löschen möchten?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"Default page": "Default page",
|
"Default page": "Default page",
|
||||||
"confirmation": "Confirmation",
|
"confirmation": "Confirmation",
|
||||||
"delete confirm": "Are you sure to delete <code>{name}</code>?",
|
"delete confirm": "Are you sure to delete <code>{name}</code>?",
|
||||||
|
"delete confirm multiple": "Are you sure to delete <code>{name}</code> KV(s)?",
|
||||||
"outdated revision save confirmation": {
|
"outdated revision save confirmation": {
|
||||||
"confirm": "Do you want to overwrite it?",
|
"confirm": "Do you want to overwrite it?",
|
||||||
"update": {
|
"update": {
|
||||||
@@ -230,6 +231,7 @@
|
|||||||
"automatic refresh": "Automatic refresh",
|
"automatic refresh": "Automatic refresh",
|
||||||
"toggle periodic refresh each 10 seconds": "Toggle periodic refresh every 10 seconds",
|
"toggle periodic refresh each 10 seconds": "Toggle periodic refresh every 10 seconds",
|
||||||
"trigger refresh": "Trigger refresh",
|
"trigger refresh": "Trigger refresh",
|
||||||
|
"add-trigger-in-editor": "Add a Trigger to your Flow first",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"topology-graph": {
|
"topology-graph": {
|
||||||
"graph-orientation": "Graph orientation",
|
"graph-orientation": "Graph orientation",
|
||||||
@@ -428,6 +430,10 @@
|
|||||||
"flow enable": "Are you sure you want to enable <code>{flowCount}</code> flow(s)?",
|
"flow enable": "Are you sure you want to enable <code>{flowCount}</code> flow(s)?",
|
||||||
"flows disabled": "<code>{count}</code> Flow(s) disabled",
|
"flows disabled": "<code>{count}</code> Flow(s) disabled",
|
||||||
"flows enabled": "<code>{count}</code> Flow(s) enabled",
|
"flows enabled": "<code>{count}</code> Flow(s) enabled",
|
||||||
|
"flow-no-dependencies": "Your flow does not have any dependencies",
|
||||||
|
"flow-dependencies": "dependencies",
|
||||||
|
"read-more": "Read more about",
|
||||||
|
"in-our-documentation": "in our documentation.",
|
||||||
"dependencies": "Dependencies",
|
"dependencies": "Dependencies",
|
||||||
"see dependencies": "See dependencies",
|
"see dependencies": "See dependencies",
|
||||||
"dependencies missing acls": "No permissions on this flow",
|
"dependencies missing acls": "No permissions on this flow",
|
||||||
@@ -855,7 +861,9 @@
|
|||||||
"trigger_check_warning": "Warning: Usage of the `trigger` variable detected, executing your flow manually won't fullfill the trigger variable.",
|
"trigger_check_warning": "Warning: Usage of the `trigger` variable detected, executing your flow manually won't fullfill the trigger variable.",
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"success_ratio": "Success Ratio",
|
"success_ratio": "Success Ratio",
|
||||||
|
"success_ratio_tooltip": "Success Ratio is the sum of SUCCESS, CANCELLED and WARNING executions divided by the total number of executions in a terminated state.",
|
||||||
"failure_ratio": "Failure Ratio",
|
"failure_ratio": "Failure Ratio",
|
||||||
|
"failure_ratio_tooltip": "Failed ratio is the sum of FAILED, KILLED and RETRIED executions divided by the total number of executions in a terminated state.",
|
||||||
"per_day": " (per day)",
|
"per_day": " (per day)",
|
||||||
"per_namespace": " (per namespace)",
|
"per_namespace": " (per namespace)",
|
||||||
"total_executions": "Total Executions",
|
"total_executions": "Total Executions",
|
||||||
@@ -866,7 +874,8 @@
|
|||||||
"trigger_disabled": "This trigger can only be enabled through code.",
|
"trigger_disabled": "This trigger can only be enabled through code.",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"no_flow_description": "This flow has no description.",
|
"no_flow_description": "This flow has no description.",
|
||||||
"not_auth": "You don't have the necessary permissions to view this widget."
|
"not_auth": "You don't have the necessary permissions to view this widget.",
|
||||||
|
"see_all": "See all"
|
||||||
},
|
},
|
||||||
"docs": "Docs",
|
"docs": "Docs",
|
||||||
"active-slots": "Active slots",
|
"active-slots": "Active slots",
|
||||||
@@ -877,6 +886,8 @@
|
|||||||
"desc_no_limit": "Read more about <a href=\"https://kestra.io/docs/workflow-components/concurrency\" target=\"_blank\">Concurrency Limits</a> in our documentation."
|
"desc_no_limit": "Read more about <a href=\"https://kestra.io/docs/workflow-components/concurrency\" target=\"_blank\">Concurrency Limits</a> in our documentation."
|
||||||
},
|
},
|
||||||
"open sidebar": "open sidebar",
|
"open sidebar": "open sidebar",
|
||||||
"close sidebar": "close sidebar"
|
"close sidebar": "close sidebar",
|
||||||
|
"change_status": "Change status",
|
||||||
|
"files": "Files"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger desbloqueado"
|
"success": "Trigger desbloqueado"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Reiniciar trigger"
|
"button": "Reiniciar trigger",
|
||||||
|
"tooltip": "Reiniciar el trigger"
|
||||||
},
|
},
|
||||||
"date format": "Formato de fecha",
|
"date format": "Formato de fecha",
|
||||||
"timezone": "Zona horaria",
|
"timezone": "Zona horaria",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "Este trigger solo puede ser habilitado a través de código.",
|
"trigger_disabled": "Este trigger solo puede ser habilitado a través de código.",
|
||||||
"no_flow_description": "Este flow no tiene descripción.",
|
"no_flow_description": "Este flow no tiene descripción.",
|
||||||
"description": "Descripción",
|
"description": "Descripción",
|
||||||
"not_auth": "No tienes los permisos necesarios para ver este widget."
|
"not_auth": "No tienes los permisos necesarios para ver este widget.",
|
||||||
|
"see_all": "Ver todo",
|
||||||
|
"success_ratio_tooltip": "La Tasa de Éxito es la suma de ejecuciones en estado SUCCESS, CANCELLED y WARNING dividida por el número total de ejecuciones en un estado terminado.",
|
||||||
|
"failure_ratio_tooltip": "La proporción de Failed es la suma de ejecuciones FAILED, KILLED y RETRIED dividida por el número total de ejecuciones en un estado terminado."
|
||||||
},
|
},
|
||||||
"delete_log": "¿Está seguro de que desea eliminar el log?",
|
"delete_log": "¿Está seguro de que desea eliminar el log?",
|
||||||
"docs": "Documentos",
|
"docs": "Documentos",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "Ranuras activas",
|
"active-slots": "Ranuras activas",
|
||||||
"concurrency": "Concurrente",
|
"concurrency": "Concurrente",
|
||||||
"open sidebar": "abrir barra lateral",
|
"open sidebar": "abrir barra lateral",
|
||||||
"close sidebar": "cerrar barra lateral"
|
"close sidebar": "cerrar barra lateral",
|
||||||
|
"change_status": "Cambiar estado",
|
||||||
|
"in-our-documentation": "en nuestra documentación.",
|
||||||
|
"read-more": "Leer más sobre",
|
||||||
|
"flow-dependencies": "dependencias",
|
||||||
|
"flow-no-dependencies": "Tu flow no tiene dependencias.",
|
||||||
|
"files": "Archivos",
|
||||||
|
"add-trigger-in-editor": "Agrega un Trigger a tu Flow primero",
|
||||||
|
"delete confirm multiple": "¿Está seguro de que desea eliminar el(los) <code>{name}</code> KV(s)?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"details": "Regardez l'onglet Révisions pour plus de détails sur la dernière version."
|
"details": "Regardez l'onglet Révisions pour plus de détails sur la dernière version."
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
|
"title": "Le flux existe déjà",
|
||||||
"description": "Un Flow avec le même id / namespace existe déjà.",
|
"description": "Un Flow avec le même id / namespace existe déjà.",
|
||||||
"details": "Sauvegarder pour l'écraser et forcer la création d'une révision."
|
"details": "Sauvegarder pour l'écraser et forcer la création d'une révision."
|
||||||
}
|
}
|
||||||
@@ -214,14 +215,14 @@
|
|||||||
"topology-graph": {
|
"topology-graph": {
|
||||||
"graph-orientation": "Orientation du graph",
|
"graph-orientation": "Orientation du graph",
|
||||||
"zoom-in": "Zoomer",
|
"zoom-in": "Zoomer",
|
||||||
"zoom-out": "Dézommer",
|
"zoom-out": "Dézoomer",
|
||||||
"zoom-reset": "Zoom par défaut",
|
"zoom-reset": "Zoom par défaut",
|
||||||
"zoom-fit": "Voir tout"
|
"zoom-fit": "Voir tout"
|
||||||
},
|
},
|
||||||
"show task logs": "Afficher les journaux de la tâche",
|
"show task logs": "Afficher les journaux de la tâche",
|
||||||
"show task outputs": "Afficher les outputs de la tâche",
|
"show task outputs": "Afficher les outputs de la tâche",
|
||||||
"show task source": "Afficher le code source de la tâche",
|
"show task source": "Afficher le code source de la tâche",
|
||||||
"specific task": "Tâche specifique",
|
"specific task": "Tâche spécifique",
|
||||||
"display output for specific task": "Afficher les sorties pour cette tâche",
|
"display output for specific task": "Afficher les sorties pour cette tâche",
|
||||||
"display metric for specific task": "Afficher les mesures pour cette tâche",
|
"display metric for specific task": "Afficher les mesures pour cette tâche",
|
||||||
"display direct sub tasks count": "Display les sous tâches directes",
|
"display direct sub tasks count": "Display les sous tâches directes",
|
||||||
@@ -509,7 +510,8 @@
|
|||||||
"success": "Le déclencheur est débloqué"
|
"success": "Le déclencheur est débloqué"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Redémarrer trigger"
|
"button": "Redémarrer trigger",
|
||||||
|
"tooltip": "Redémarrer le trigger"
|
||||||
},
|
},
|
||||||
"date format": "Format de date",
|
"date format": "Format de date",
|
||||||
"timezone": "Fuseau horaire",
|
"timezone": "Fuseau horaire",
|
||||||
@@ -624,12 +626,12 @@
|
|||||||
"Set labels": "Ajouter des labels",
|
"Set labels": "Ajouter des labels",
|
||||||
"Set labels to execution": "Ajouter ou mettre à jour des labels à l'exécution <code>{id}</code>",
|
"Set labels to execution": "Ajouter ou mettre à jour des labels à l'exécution <code>{id}</code>",
|
||||||
"Set labels done": "Labels ajoutés avec succès à l'exécution",
|
"Set labels done": "Labels ajoutés avec succès à l'exécution",
|
||||||
"bulk set labels": "Etes-vous sûr de vouloir ajouter des labels à <code>{executionCount}</code> exécutions(s)?",
|
"bulk set labels": "Êtes-vous sûr de vouloir ajouter des labels à <code>{executionCount}</code> exécutions(s)?",
|
||||||
"dependencies loaded": "Dépendances chargées",
|
"dependencies loaded": "Dépendances chargées",
|
||||||
"loaded x dependencies": "{count} dépendances chargées",
|
"loaded x dependencies": "{count} dépendances chargées",
|
||||||
"security_advice": {
|
"security_advice": {
|
||||||
"title": "Vos données ne sont pas protégées !",
|
"title": "Vos données ne sont pas protégées !",
|
||||||
"content": "Activer l'authentication basique pour protéger votre instance.",
|
"content": "Activer l'authentification basique pour protéger votre instance.",
|
||||||
"switch_text": "Ne plus montrer",
|
"switch_text": "Ne plus montrer",
|
||||||
"enable": "Activer l'authentification"
|
"enable": "Activer l'authentification"
|
||||||
},
|
},
|
||||||
@@ -848,7 +850,10 @@
|
|||||||
"trigger_disabled": "Ce trigger ne peut être activé que par le code.",
|
"trigger_disabled": "Ce trigger ne peut être activé que par le code.",
|
||||||
"no_flow_description": "Ce flow n'a pas de description.",
|
"no_flow_description": "Ce flow n'a pas de description.",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"not_auth": "Vous n'avez pas les autorisations nécessaires pour afficher ce widget."
|
"not_auth": "Vous n'avez pas les autorisations nécessaires pour afficher ce widget.",
|
||||||
|
"see_all": "Voir tout",
|
||||||
|
"success_ratio_tooltip": "Le ratio de succès est la somme des exécutions en état SUCCESS, CANCELLED et WARNING divisée par le nombre total d'exécutions dans un état terminé.",
|
||||||
|
"failure_ratio_tooltip": "Le ratio d'échec est la somme des exécutions en état FAILED, KILLED et RETRIED divisée par le nombre total d'exécutions dans un état terminé."
|
||||||
},
|
},
|
||||||
"delete_log": "Êtes-vous sûr de vouloir supprimer le log ?",
|
"delete_log": "Êtes-vous sûr de vouloir supprimer le log ?",
|
||||||
"docs": "Documentation",
|
"docs": "Documentation",
|
||||||
@@ -860,6 +865,14 @@
|
|||||||
"active-slots": "Slots actifs",
|
"active-slots": "Slots actifs",
|
||||||
"concurrency": "Concurrence",
|
"concurrency": "Concurrence",
|
||||||
"open sidebar": "ouvrir la barre latérale",
|
"open sidebar": "ouvrir la barre latérale",
|
||||||
"close sidebar": "fermer la barre latérale"
|
"close sidebar": "fermer la barre latérale",
|
||||||
|
"change_status": "Changer le statut",
|
||||||
|
"in-our-documentation": "dans notre documentation.",
|
||||||
|
"read-more": "En savoir plus sur",
|
||||||
|
"flow-dependencies": "dépendances",
|
||||||
|
"flow-no-dependencies": "Votre flow n'a pas de dépendances.",
|
||||||
|
"files": "Fichiers",
|
||||||
|
"add-trigger-in-editor": "Ajoutez d'abord un Trigger à votre Flow",
|
||||||
|
"delete confirm multiple": "Êtes-vous sûr de vouloir supprimer le(s) KV <code>{name}</code> ?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +93,7 @@ def translate_dict(en_dict, target_language):
|
|||||||
translated_value = translate_dict(value, target_language)
|
translated_value = translate_dict(value, target_language)
|
||||||
else:
|
else:
|
||||||
translated_value = translate_text(value, target_language)
|
translated_value = translate_text(value, target_language)
|
||||||
|
print(f"Translating key '{key}' with value '{value}' from English, to value '{translated_value}' in {target_language}.")
|
||||||
translated_dict[key] = translated_value
|
translated_dict[key] = translated_value
|
||||||
return translated_dict
|
return translated_dict
|
||||||
|
|
||||||
@@ -160,7 +161,6 @@ def get_keys_to_translate(file_path="ui/src/translations/en.json"):
|
|||||||
keys_to_translate = detect_changes(current_en_dict, previous_en_dict)
|
keys_to_translate = detect_changes(current_en_dict, previous_en_dict)
|
||||||
en_flat = flatten_dict(current_en_dict)
|
en_flat = flatten_dict(current_en_dict)
|
||||||
to_translate = {k: en_flat[k] for k in keys_to_translate}
|
to_translate = {k: en_flat[k] for k in keys_to_translate}
|
||||||
print("Changed data requiring translatation:", to_translate)
|
|
||||||
return to_translate
|
return to_translate
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger अनलॉक किया गया"
|
"success": "Trigger अनलॉक किया गया"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Trigger पुनः प्रारंभ करें"
|
"button": "Trigger पुनः प्रारंभ करें",
|
||||||
|
"tooltip": "ट्रिगर को पुनः प्रारंभ करें"
|
||||||
},
|
},
|
||||||
"date format": "दिनांक प्रारूप",
|
"date format": "दिनांक प्रारूप",
|
||||||
"timezone": "समय क्षेत्र",
|
"timezone": "समय क्षेत्र",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "इस trigger को केवल कोड के माध्यम से सक्षम किया जा सकता है।",
|
"trigger_disabled": "इस trigger को केवल कोड के माध्यम से सक्षम किया जा सकता है।",
|
||||||
"no_flow_description": "इस flow का कोई विवरण नहीं है।",
|
"no_flow_description": "इस flow का कोई विवरण नहीं है।",
|
||||||
"description": "विवरण",
|
"description": "विवरण",
|
||||||
"not_auth": "आपके पास इस विजेट को देखने की आवश्यक अनुमतियाँ नहीं हैं।"
|
"not_auth": "आपके पास इस विजेट को देखने की आवश्यक अनुमतियाँ नहीं हैं।",
|
||||||
|
"see_all": "सभी देखें",
|
||||||
|
"success_ratio_tooltip": "सफलता अनुपात SUCCESS, CANCELLED और WARNING निष्पादन का योग है, जिसे समाप्त स्थिति में कुल निष्पादन की संख्या से विभाजित किया जाता है।",
|
||||||
|
"failure_ratio_tooltip": "असफल अनुपात, FAILED, KILLED और RETRIED निष्पादन का योग है, जिसे समाप्त स्थिति में कुल निष्पादन की संख्या से विभाजित किया जाता है।"
|
||||||
},
|
},
|
||||||
"delete_log": "क्या आप वाकई log को हटाना चाहते हैं?",
|
"delete_log": "क्या आप वाकई log को हटाना चाहते हैं?",
|
||||||
"docs": "डॉक्स",
|
"docs": "डॉक्स",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "सक्रिय स्लॉट्स",
|
"active-slots": "सक्रिय स्लॉट्स",
|
||||||
"concurrency": "समानांतरता",
|
"concurrency": "समानांतरता",
|
||||||
"open sidebar": "साइडबार खोलें",
|
"open sidebar": "साइडबार खोलें",
|
||||||
"close sidebar": "साइडबार बंद करें"
|
"close sidebar": "साइडबार बंद करें",
|
||||||
|
"change_status": "स्थिति बदलें",
|
||||||
|
"in-our-documentation": "हमारे दस्तावेज़ में।",
|
||||||
|
"read-more": "और अधिक पढ़ें",
|
||||||
|
"flow-dependencies": "निर्भरता",
|
||||||
|
"flow-no-dependencies": "आपके flow की कोई dependencies नहीं हैं।",
|
||||||
|
"files": "फ़ाइलें",
|
||||||
|
"add-trigger-in-editor": "अपने Flow में पहले एक Trigger जोड़ें",
|
||||||
|
"delete confirm multiple": "क्या आप वाकई <code>{name}</code> KV(s) को हटाना चाहते हैं?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger sbloccato"
|
"success": "Trigger sbloccato"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Riavvia trigger"
|
"button": "Riavvia trigger",
|
||||||
|
"tooltip": "Riavvia il trigger"
|
||||||
},
|
},
|
||||||
"date format": "Formato data",
|
"date format": "Formato data",
|
||||||
"timezone": "Fuso orario",
|
"timezone": "Fuso orario",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "Questo trigger può essere abilitato solo tramite codice.",
|
"trigger_disabled": "Questo trigger può essere abilitato solo tramite codice.",
|
||||||
"no_flow_description": "Questo flow non ha descrizione.",
|
"no_flow_description": "Questo flow non ha descrizione.",
|
||||||
"description": "Descrizione",
|
"description": "Descrizione",
|
||||||
"not_auth": "Non hai le autorizzazioni necessarie per visualizzare questo widget."
|
"not_auth": "Non hai le autorizzazioni necessarie per visualizzare questo widget.",
|
||||||
|
"see_all": "Vedi tutto",
|
||||||
|
"success_ratio_tooltip": "Il Rapporto di Successo è la somma delle esecuzioni in stato SUCCESS, CANCELLED e WARNING divisa per il numero totale di esecuzioni in uno stato terminato.",
|
||||||
|
"failure_ratio_tooltip": "Il rapporto di errore è la somma delle esecuzioni FAILED, KILLED e RETRIED divisa per il numero totale di esecuzioni in uno stato terminato."
|
||||||
},
|
},
|
||||||
"delete_log": "Sei sicuro di voler eliminare il log?",
|
"delete_log": "Sei sicuro di voler eliminare il log?",
|
||||||
"docs": "Documenti",
|
"docs": "Documenti",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "Slot attivi",
|
"active-slots": "Slot attivi",
|
||||||
"concurrency": "Concurrency",
|
"concurrency": "Concurrency",
|
||||||
"open sidebar": "apri barra laterale",
|
"open sidebar": "apri barra laterale",
|
||||||
"close sidebar": "chiudi barra laterale"
|
"close sidebar": "chiudi barra laterale",
|
||||||
|
"change_status": "Cambia stato",
|
||||||
|
"in-our-documentation": "nella nostra documentazione.",
|
||||||
|
"read-more": "Leggi di più su",
|
||||||
|
"flow-dependencies": "dipendenze",
|
||||||
|
"flow-no-dependencies": "Il tuo flow non ha dipendenze",
|
||||||
|
"files": "File",
|
||||||
|
"add-trigger-in-editor": "Aggiungi un Trigger al tuo Flow prima",
|
||||||
|
"delete confirm multiple": "Sei sicuro di voler eliminare il/i <code>{name}</code> KV?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "triggerのロックが解除されました"
|
"success": "triggerのロックが解除されました"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "triggerを再起動"
|
"button": "triggerを再起動",
|
||||||
|
"tooltip": "トリガーを再起動する"
|
||||||
},
|
},
|
||||||
"date format": "日付形式",
|
"date format": "日付形式",
|
||||||
"timezone": "タイムゾーン",
|
"timezone": "タイムゾーン",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "このTriggerはコードを通じてのみ有効にできます。",
|
"trigger_disabled": "このTriggerはコードを通じてのみ有効にできます。",
|
||||||
"no_flow_description": "このflowには説明がありません。",
|
"no_flow_description": "このflowには説明がありません。",
|
||||||
"description": "説明",
|
"description": "説明",
|
||||||
"not_auth": "必要な権限がないため、このウィジェットを表示できません。"
|
"not_auth": "必要な権限がないため、このウィジェットを表示できません。",
|
||||||
|
"see_all": "すべて表示",
|
||||||
|
"success_ratio_tooltip": "成功率は、SUCCESS、CANCELLED、WARNINGの実行の合計を、終了状態にある実行の総数で割ったものです。",
|
||||||
|
"failure_ratio_tooltip": "失敗率は、FAILED、KILLED、および RETRIED の実行の合計を、終了状態にある実行の総数で割ったものです。"
|
||||||
},
|
},
|
||||||
"delete_log": "ログを削除してもよろしいですか?",
|
"delete_log": "ログを削除してもよろしいですか?",
|
||||||
"docs": "ドキュメント",
|
"docs": "ドキュメント",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "アクティブスロット",
|
"active-slots": "アクティブスロット",
|
||||||
"concurrency": "並行性",
|
"concurrency": "並行性",
|
||||||
"open sidebar": "サイドバーを開く",
|
"open sidebar": "サイドバーを開く",
|
||||||
"close sidebar": "サイドバーを閉じる"
|
"close sidebar": "サイドバーを閉じる",
|
||||||
|
"change_status": "ステータスを変更",
|
||||||
|
"in-our-documentation": "ドキュメント内で。",
|
||||||
|
"read-more": "続きを読む",
|
||||||
|
"flow-dependencies": "依存関係",
|
||||||
|
"flow-no-dependencies": "あなたのflowには依存関係がありません",
|
||||||
|
"files": "ファイル",
|
||||||
|
"add-trigger-in-editor": "最初にFlowにTriggerを追加してください",
|
||||||
|
"delete confirm multiple": "<code>{name}</code> KV(s) を削除してもよろしいですか?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger가 잠금 해제되었습니다"
|
"success": "Trigger가 잠금 해제되었습니다"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Trigger 재시작"
|
"button": "Trigger 재시작",
|
||||||
|
"tooltip": "트리거 재시작"
|
||||||
},
|
},
|
||||||
"date format": "날짜 형식",
|
"date format": "날짜 형식",
|
||||||
"timezone": "시간대",
|
"timezone": "시간대",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "이 trigger는 코드로만 활성화할 수 있습니다.",
|
"trigger_disabled": "이 trigger는 코드로만 활성화할 수 있습니다.",
|
||||||
"no_flow_description": "이 flow에는 설명이 없습니다.",
|
"no_flow_description": "이 flow에는 설명이 없습니다.",
|
||||||
"description": "설명",
|
"description": "설명",
|
||||||
"not_auth": "이 위젯을 볼 수 있는 권한이 없습니다."
|
"not_auth": "이 위젯을 볼 수 있는 권한이 없습니다.",
|
||||||
|
"see_all": "모두 보기",
|
||||||
|
"success_ratio_tooltip": "성공 비율은 SUCCESS, CANCELLED 및 WARNING 실행의 합계를 종료된 상태의 총 실행 수로 나눈 값입니다.",
|
||||||
|
"failure_ratio_tooltip": "실패 비율은 FAILED, KILLED, RETRIED 실행의 합을 종료된 상태의 전체 실행 수로 나눈 값입니다."
|
||||||
},
|
},
|
||||||
"delete_log": "로그를 삭제하시겠습니까?",
|
"delete_log": "로그를 삭제하시겠습니까?",
|
||||||
"docs": "문서",
|
"docs": "문서",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "활성 슬롯",
|
"active-slots": "활성 슬롯",
|
||||||
"concurrency": "동시성",
|
"concurrency": "동시성",
|
||||||
"open sidebar": "사이드바 열기",
|
"open sidebar": "사이드바 열기",
|
||||||
"close sidebar": "사이드바 닫기"
|
"close sidebar": "사이드바 닫기",
|
||||||
|
"change_status": "상태 변경",
|
||||||
|
"in-our-documentation": "우리의 문서에서.",
|
||||||
|
"read-more": "자세히 알아보기",
|
||||||
|
"flow-dependencies": "종속성",
|
||||||
|
"flow-no-dependencies": "귀하의 flow에는 종속성이 없습니다.",
|
||||||
|
"files": "파일",
|
||||||
|
"add-trigger-in-editor": "먼저 Flow에 Trigger를 추가하세요.",
|
||||||
|
"delete confirm multiple": "<code>{name}</code> KV(s)를 삭제하시겠습니까?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger odblokowany"
|
"success": "Trigger odblokowany"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Restartuj trigger"
|
"button": "Restartuj trigger",
|
||||||
|
"tooltip": "Uruchom ponownie trigger"
|
||||||
},
|
},
|
||||||
"date format": "Format daty",
|
"date format": "Format daty",
|
||||||
"timezone": "Strefa czasowa",
|
"timezone": "Strefa czasowa",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "Ten trigger może być włączony tylko za pomocą kodu.",
|
"trigger_disabled": "Ten trigger może być włączony tylko za pomocą kodu.",
|
||||||
"no_flow_description": "Ten flow nie ma opisu.",
|
"no_flow_description": "Ten flow nie ma opisu.",
|
||||||
"description": "Opis",
|
"description": "Opis",
|
||||||
"not_auth": "Nie masz niezbędnych uprawnień, aby wyświetlić ten widget."
|
"not_auth": "Nie masz niezbędnych uprawnień, aby wyświetlić ten widget.",
|
||||||
|
"see_all": "Zobacz wszystkie",
|
||||||
|
"success_ratio_tooltip": "Wskaźnik Sukcesu to suma wykonań w stanach SUCCESS, CANCELLED i WARNING podzielona przez całkowitą liczbę wykonań w stanie zakończonym.",
|
||||||
|
"failure_ratio_tooltip": "Stosunek niepowodzeń to suma wykonań w stanach FAILED, KILLED i RETRIED podzielona przez całkowitą liczbę wykonań w stanie zakończonym."
|
||||||
},
|
},
|
||||||
"delete_log": "Czy na pewno chcesz usunąć log?",
|
"delete_log": "Czy na pewno chcesz usunąć log?",
|
||||||
"docs": "Dokumentacja",
|
"docs": "Dokumentacja",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "Aktywne sloty",
|
"active-slots": "Aktywne sloty",
|
||||||
"concurrency": "Współbieżność",
|
"concurrency": "Współbieżność",
|
||||||
"open sidebar": "otwórz pasek boczny",
|
"open sidebar": "otwórz pasek boczny",
|
||||||
"close sidebar": "zamknij pasek boczny"
|
"close sidebar": "zamknij pasek boczny",
|
||||||
|
"change_status": "Zmień status",
|
||||||
|
"in-our-documentation": "w naszej dokumentacji.",
|
||||||
|
"read-more": "Czytaj więcej o",
|
||||||
|
"flow-dependencies": "zależności",
|
||||||
|
"flow-no-dependencies": "Twój flow nie ma żadnych zależności",
|
||||||
|
"files": "Pliki",
|
||||||
|
"add-trigger-in-editor": "Najpierw dodaj Trigger do swojego Flow",
|
||||||
|
"delete confirm multiple": "Czy na pewno chcesz usunąć <code>{name}</code> KV?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger desbloqueado"
|
"success": "Trigger desbloqueado"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Reiniciar trigger"
|
"button": "Reiniciar trigger",
|
||||||
|
"tooltip": "Reiniciar o trigger"
|
||||||
},
|
},
|
||||||
"date format": "Formato de data",
|
"date format": "Formato de data",
|
||||||
"timezone": "Fuso horário",
|
"timezone": "Fuso horário",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "Este trigger só pode ser habilitado através de código.",
|
"trigger_disabled": "Este trigger só pode ser habilitado através de código.",
|
||||||
"no_flow_description": "Este flow não tem descrição.",
|
"no_flow_description": "Este flow não tem descrição.",
|
||||||
"description": "Descrição",
|
"description": "Descrição",
|
||||||
"not_auth": "Você não tem as permissões necessárias para visualizar este widget."
|
"not_auth": "Você não tem as permissões necessárias para visualizar este widget.",
|
||||||
|
"see_all": "Ver todos",
|
||||||
|
"success_ratio_tooltip": "A Taxa de Sucesso é a soma das execuções em estados SUCCESS, CANCELLED e WARNING dividida pelo número total de execuções em um estado terminado.",
|
||||||
|
"failure_ratio_tooltip": "A proporção de falhas é a soma das execuções FAILED, KILLED e RETRIED dividida pelo número total de execuções em um estado terminado."
|
||||||
},
|
},
|
||||||
"delete_log": "Tem certeza de que deseja excluir o log?",
|
"delete_log": "Tem certeza de que deseja excluir o log?",
|
||||||
"docs": "Documentos",
|
"docs": "Documentos",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "Slots ativos",
|
"active-slots": "Slots ativos",
|
||||||
"concurrency": "Concorrência",
|
"concurrency": "Concorrência",
|
||||||
"open sidebar": "abrir barra lateral",
|
"open sidebar": "abrir barra lateral",
|
||||||
"close sidebar": "fechar barra lateral"
|
"close sidebar": "fechar barra lateral",
|
||||||
|
"change_status": "Alterar status",
|
||||||
|
"in-our-documentation": "na nossa documentação.",
|
||||||
|
"read-more": "Leia mais sobre",
|
||||||
|
"flow-dependencies": "dependências",
|
||||||
|
"flow-no-dependencies": "Seu flow não possui dependências",
|
||||||
|
"files": "Arquivos",
|
||||||
|
"add-trigger-in-editor": "Adicione um Trigger ao seu Flow primeiro",
|
||||||
|
"delete confirm multiple": "Tem certeza de que deseja excluir o(s) <code>{name}</code> KV(s)?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "Trigger разблокирован"
|
"success": "Trigger разблокирован"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "Перезапустить trigger"
|
"button": "Перезапустить trigger",
|
||||||
|
"tooltip": "Перезапустить trigger"
|
||||||
},
|
},
|
||||||
"date format": "Формат даты",
|
"date format": "Формат даты",
|
||||||
"timezone": "Часовой пояс",
|
"timezone": "Часовой пояс",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "Этот trigger может быть включен только через код.",
|
"trigger_disabled": "Этот trigger может быть включен только через код.",
|
||||||
"no_flow_description": "Этот flow не имеет описания.",
|
"no_flow_description": "Этот flow не имеет описания.",
|
||||||
"description": "Описание",
|
"description": "Описание",
|
||||||
"not_auth": "У вас нет необходимых разрешений для просмотра этого виджета."
|
"not_auth": "У вас нет необходимых разрешений для просмотра этого виджета.",
|
||||||
|
"see_all": "Посмотреть все",
|
||||||
|
"success_ratio_tooltip": "Коэффициент успеха — это сумма выполнений в состояниях SUCCESS, CANCELLED и WARNING, деленная на общее количество выполнений в завершенном состоянии.",
|
||||||
|
"failure_ratio_tooltip": "Отношение Failed — это сумма выполнений в состояниях FAILED, KILLED и RETRIED, деленная на общее количество выполнений в завершенном состоянии."
|
||||||
},
|
},
|
||||||
"delete_log": "Вы уверены, что хотите удалить log?",
|
"delete_log": "Вы уверены, что хотите удалить log?",
|
||||||
"docs": "Документы",
|
"docs": "Документы",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "Активные слоты",
|
"active-slots": "Активные слоты",
|
||||||
"concurrency": "Конкурентность",
|
"concurrency": "Конкурентность",
|
||||||
"open sidebar": "открыть боковую панель",
|
"open sidebar": "открыть боковую панель",
|
||||||
"close sidebar": "закрыть боковую панель"
|
"close sidebar": "закрыть боковую панель",
|
||||||
|
"change_status": "Изменить статус",
|
||||||
|
"in-our-documentation": "в нашей документации.",
|
||||||
|
"read-more": "Узнать больше о",
|
||||||
|
"flow-dependencies": "зависимости",
|
||||||
|
"flow-no-dependencies": "Ваш flow не имеет зависимостей",
|
||||||
|
"files": "Файлы",
|
||||||
|
"add-trigger-in-editor": "Сначала добавьте Trigger в ваш Flow",
|
||||||
|
"delete confirm multiple": "Вы уверены, что хотите удалить <code>{name}</code> KV?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,8 @@
|
|||||||
"success": "触发器已解锁"
|
"success": "触发器已解锁"
|
||||||
},
|
},
|
||||||
"restart trigger": {
|
"restart trigger": {
|
||||||
"button": "重新启动触发器"
|
"button": "重新启动触发器",
|
||||||
|
"tooltip": "重新启动trigger"
|
||||||
},
|
},
|
||||||
"date format": "日期格式",
|
"date format": "日期格式",
|
||||||
"timezone": "时区",
|
"timezone": "时区",
|
||||||
@@ -849,7 +850,10 @@
|
|||||||
"trigger_disabled": "此trigger只能通过代码启用。",
|
"trigger_disabled": "此trigger只能通过代码启用。",
|
||||||
"no_flow_description": "此flow没有描述。",
|
"no_flow_description": "此flow没有描述。",
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
"not_auth": "您没有查看此小部件的必要权限。"
|
"not_auth": "您没有查看此小部件的必要权限。",
|
||||||
|
"see_all": "查看全部",
|
||||||
|
"success_ratio_tooltip": "成功率是SUCCESS、CANCELLED和WARNING执行次数之和除以终止状态下执行总次数的结果。",
|
||||||
|
"failure_ratio_tooltip": "失败率是FAILED、KILLED和RETRIED执行次数之和除以终止状态下执行总次数的结果。"
|
||||||
},
|
},
|
||||||
"delete_log": "您确定要删除这个log吗?",
|
"delete_log": "您确定要删除这个log吗?",
|
||||||
"docs": "文档",
|
"docs": "文档",
|
||||||
@@ -861,6 +865,14 @@
|
|||||||
"active-slots": "活动槽位",
|
"active-slots": "活动槽位",
|
||||||
"concurrency": "并发",
|
"concurrency": "并发",
|
||||||
"open sidebar": "打开侧边栏",
|
"open sidebar": "打开侧边栏",
|
||||||
"close sidebar": "关闭侧边栏"
|
"close sidebar": "关闭侧边栏",
|
||||||
|
"change_status": "更改状态",
|
||||||
|
"in-our-documentation": "在我们的文档中。",
|
||||||
|
"read-more": "了解更多关于",
|
||||||
|
"flow-dependencies": "依赖项",
|
||||||
|
"flow-no-dependencies": "您的flow没有任何依赖项",
|
||||||
|
"files": "文件",
|
||||||
|
"add-trigger-in-editor": "首先为您的Flow添加一个Trigger",
|
||||||
|
"delete confirm multiple": "您确定要删除<code>{name}</code> KV吗?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,8 @@ export const storageKeys = {
|
|||||||
SELECTED_TENANT: "selectedTenant",
|
SELECTED_TENANT: "selectedTenant",
|
||||||
EXECUTE_FLOW_BEHAVIOUR: "executeFlowBehaviour",
|
EXECUTE_FLOW_BEHAVIOUR: "executeFlowBehaviour",
|
||||||
SHOW_CHART: "showChart",
|
SHOW_CHART: "showChart",
|
||||||
|
SHOW_FLOWS_CHART: "showFlowsChart",
|
||||||
|
SHOW_LOGS_CHART: "showLogsChart",
|
||||||
DEFAULT_NAMESPACE: "defaultNamespace",
|
DEFAULT_NAMESPACE: "defaultNamespace",
|
||||||
LATEST_NAMESPACE: "latestNamespace",
|
LATEST_NAMESPACE: "latestNamespace",
|
||||||
PAGINATION_SIZE: "paginationSize",
|
PAGINATION_SIZE: "paginationSize",
|
||||||
|
|||||||
@@ -233,4 +233,8 @@ export default class State {
|
|||||||
static icon() {
|
static icon() {
|
||||||
return _mapValues(STATE, (state) => state.icon);
|
return _mapValues(STATE, (state) => state.icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getTerminatedStates() {
|
||||||
|
return Object.values(STATE).filter(state => !state.isRunning).map(state => state.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ export default (app, store, router) => {
|
|||||||
router.beforeEach(async () => {
|
router.beforeEach(async () => {
|
||||||
if (store.getters["core/unsavedChange"]) {
|
if (store.getters["core/unsavedChange"]) {
|
||||||
if (confirm(confirmationMessage)) {
|
if (confirm(confirmationMessage)) {
|
||||||
|
store.commit("editor/changeOpenedTabs", {
|
||||||
|
action: "dirty",
|
||||||
|
name: "Flow",
|
||||||
|
path: "Flow.yaml",
|
||||||
|
dirty: false,
|
||||||
|
});
|
||||||
|
store.commit("flow/setFlow", store.getters["flow/lastSavedFlow"]);
|
||||||
store.commit("core/setUnsavedChange", false);
|
store.commit("core/setUnsavedChange", false);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -589,11 +589,11 @@ export default class YamlUtils {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
const order = ["id", "namespace", "description", "retry", "labels", "inputs", "variables", "tasks", "triggers", "errors", "pluginDefaults", "taskDefaults", "concurrency", "outputs"];
|
const order = ["id", "namespace", "description", "retry", "labels", "inputs", "variables", "tasks", "triggers", "errors", "pluginDefaults", "taskDefaults", "concurrency", "outputs", "disabled"];
|
||||||
const updatedItems = [];
|
const updatedItems = [];
|
||||||
for (const prop of order) {
|
for (const prop of order) {
|
||||||
const item = yamlDoc.contents.items.find(e => e.key.value === prop);
|
const item = yamlDoc.contents.items.find(e => e.key.value === prop);
|
||||||
if (item && (((isSeq(item.value) || isMap(item.value)) && item.value.items.length > 0) || item.value.value)) {
|
if (item && (((isSeq(item.value) || isMap(item.value)) && item.value.items.length > 0) || (item.value.value !== undefined && item.value.value !== null))) {
|
||||||
updatedItems.push(item);
|
updatedItems.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,19 @@ import io.kestra.core.events.CrudEventType;
|
|||||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||||
import io.kestra.core.exceptions.InternalException;
|
import io.kestra.core.exceptions.InternalException;
|
||||||
import io.kestra.core.models.Label;
|
import io.kestra.core.models.Label;
|
||||||
import io.kestra.core.models.executions.*;
|
import io.kestra.core.models.executions.Execution;
|
||||||
import io.kestra.core.models.flows.*;
|
import io.kestra.core.models.executions.ExecutionKilled;
|
||||||
|
import io.kestra.core.models.executions.ExecutionKilledExecution;
|
||||||
|
import io.kestra.core.models.executions.ExecutionMetadata;
|
||||||
|
import io.kestra.core.models.executions.ExecutionTrigger;
|
||||||
|
import io.kestra.core.models.executions.TaskRun;
|
||||||
|
import io.kestra.core.models.flows.Flow;
|
||||||
|
import io.kestra.core.models.flows.FlowForExecution;
|
||||||
|
import io.kestra.core.models.flows.FlowScope;
|
||||||
|
import io.kestra.core.models.flows.FlowWithException;
|
||||||
|
import io.kestra.core.models.flows.FlowWithSource;
|
||||||
|
import io.kestra.core.models.flows.Input;
|
||||||
|
import io.kestra.core.models.flows.State;
|
||||||
import io.kestra.core.models.flows.input.InputAndValue;
|
import io.kestra.core.models.flows.input.InputAndValue;
|
||||||
import io.kestra.core.models.hierarchies.FlowGraph;
|
import io.kestra.core.models.hierarchies.FlowGraph;
|
||||||
import io.kestra.core.models.storage.FileMetas;
|
import io.kestra.core.models.storage.FileMetas;
|
||||||
@@ -44,8 +55,19 @@ import io.micronaut.core.annotation.Introspected;
|
|||||||
import io.micronaut.core.annotation.Nullable;
|
import io.micronaut.core.annotation.Nullable;
|
||||||
import io.micronaut.core.async.annotation.SingleResult;
|
import io.micronaut.core.async.annotation.SingleResult;
|
||||||
import io.micronaut.core.convert.format.Format;
|
import io.micronaut.core.convert.format.Format;
|
||||||
import io.micronaut.http.*;
|
import io.micronaut.http.HttpRequest;
|
||||||
import io.micronaut.http.annotation.*;
|
import io.micronaut.http.HttpResponse;
|
||||||
|
import io.micronaut.http.HttpStatus;
|
||||||
|
import io.micronaut.http.MediaType;
|
||||||
|
import io.micronaut.http.MutableHttpResponse;
|
||||||
|
import io.micronaut.http.annotation.Body;
|
||||||
|
import io.micronaut.http.annotation.Controller;
|
||||||
|
import io.micronaut.http.annotation.Delete;
|
||||||
|
import io.micronaut.http.annotation.Get;
|
||||||
|
import io.micronaut.http.annotation.PathVariable;
|
||||||
|
import io.micronaut.http.annotation.Post;
|
||||||
|
import io.micronaut.http.annotation.Put;
|
||||||
|
import io.micronaut.http.annotation.QueryValue;
|
||||||
import io.micronaut.http.exceptions.HttpStatusException;
|
import io.micronaut.http.exceptions.HttpStatusException;
|
||||||
import io.micronaut.http.server.multipart.MultipartBody;
|
import io.micronaut.http.server.multipart.MultipartBody;
|
||||||
import io.micronaut.http.server.types.files.StreamedFile;
|
import io.micronaut.http.server.types.files.StreamedFile;
|
||||||
@@ -64,7 +86,8 @@ import jakarta.inject.Named;
|
|||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.*;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
@@ -85,13 +108,22 @@ import java.nio.charset.UnsupportedCharsetException;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static io.kestra.core.utils.DateUtils.validateTimeline;
|
import static io.kestra.core.utils.DateUtils.validateTimeline;
|
||||||
import static io.kestra.core.utils.Rethrow.*;
|
import static io.kestra.core.utils.Rethrow.throwConsumer;
|
||||||
|
import static io.kestra.core.utils.Rethrow.throwFunction;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Validated
|
@Validated
|
||||||
@@ -559,24 +591,11 @@ public class ExecutionController {
|
|||||||
@Parameter(description = "The labels as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
@Parameter(description = "The labels as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||||
@Parameter(description = "The flow revision or latest if null") @QueryValue Optional<Integer> revision
|
@Parameter(description = "The flow revision or latest if null") @QueryValue Optional<Integer> revision
|
||||||
) {
|
) {
|
||||||
return Mono.create(
|
Flow flow = flowService.getFlowIfExecutableOrThrow(tenantService.resolveTenant(), namespace, id, revision);
|
||||||
sink -> {
|
Execution execution = Execution.newExecution(flow, parseLabels(labels));
|
||||||
Flow flow;
|
return flowInputOutput
|
||||||
try {
|
.validateExecutionInputs(flow.getInputs(), execution, inputs)
|
||||||
flow = flowService.getFlowIfExecutableOrThrow(tenantService.resolveTenant(), namespace, id, revision);
|
.map(values -> ApiValidateExecutionInputsResponse.of(id, namespace, values));
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
sink.error(new IllegalStateException("Cannot validate execution inputs. Cause: " + e.getMessage()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Execution execution = Execution.newExecution(flow, parseLabels(labels));
|
|
||||||
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(flow.getInputs(), execution, inputs, true);
|
|
||||||
sink.success(ApiValidateExecutionInputsResponse.of(id, namespace, values));
|
|
||||||
} catch (Exception e) {
|
|
||||||
sink.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExecuteOn(TaskExecutors.IO)
|
@ExecuteOn(TaskExecutors.IO)
|
||||||
@@ -593,49 +612,39 @@ public class ExecutionController {
|
|||||||
@Parameter(description = "The flow revision or latest if null") @QueryValue Optional<Integer> revision,
|
@Parameter(description = "The flow revision or latest if null") @QueryValue Optional<Integer> revision,
|
||||||
@Parameter(description = "Schedule the flow on a specific date") @QueryValue Optional<ZonedDateTime> scheduleDate
|
@Parameter(description = "Schedule the flow on a specific date") @QueryValue Optional<ZonedDateTime> scheduleDate
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
return Mono.create(
|
Flow flow = flowService.getFlowIfExecutableOrThrow(tenantService.resolveTenant(), namespace, id, revision);
|
||||||
sink -> {
|
Execution current = Execution.newExecution(flow, null, parseLabels(labels), scheduleDate);
|
||||||
|
Mono<CompletableFuture<ExecutionResponse>> handle = flowInputOutput.readExecutionInputs(flow, current, inputs)
|
||||||
Flow flow;
|
.handle((executionInputs, sink) -> {
|
||||||
|
Execution executionWithInputs = current.withInputs(executionInputs);
|
||||||
try {
|
try {
|
||||||
flow = flowService.getFlowIfExecutableOrThrow(tenantService.resolveTenant(), namespace, id, revision);
|
executionQueue.emit(executionWithInputs);
|
||||||
} catch (IllegalStateException e) {
|
eventPublisher.publishEvent(new CrudEvent<>(executionWithInputs, CrudEventType.CREATE));
|
||||||
sink.error(new IllegalStateException("Cannot execute flow. Cause: " + e.getMessage()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Execution current = Execution.newExecution(
|
|
||||||
flow,
|
|
||||||
throwBiFunction((f, execution) -> flowInputOutput.readExecutionInputs(f, execution, inputs)),
|
|
||||||
parseLabels(labels),
|
|
||||||
scheduleDate
|
|
||||||
);
|
|
||||||
|
|
||||||
executionQueue.emit(current);
|
|
||||||
eventPublisher.publishEvent(new CrudEvent<>(current, CrudEventType.CREATE));
|
|
||||||
|
|
||||||
|
CompletableFuture<ExecutionResponse> future = new CompletableFuture<>();
|
||||||
if (!wait) {
|
if (!wait) {
|
||||||
sink.success(ExecutionResponse.fromExecution(current, executionUrl(current)));
|
future.complete(ExecutionResponse.fromExecution(executionWithInputs, executionUrl(executionWithInputs)));
|
||||||
} else {
|
} else {
|
||||||
Runnable receive = this.executionQueue.receive(either -> {
|
final AtomicReference<Runnable> disposable = new AtomicReference<>();
|
||||||
|
disposable.set(this.executionQueue.receive(either -> {
|
||||||
if (either.isRight()) {
|
if (either.isRight()) {
|
||||||
log.error("Unable to deserialize the execution: {}", either.getRight().getMessage());
|
log.error("Unable to deserialize the execution: {}", either.getRight().getMessage());
|
||||||
sink.success();
|
sink.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
Execution item = either.getLeft();
|
Execution item = either.getLeft();
|
||||||
if (item.getId().equals(current.getId()) && this.isStopFollow(flow, item)) {
|
if (item.getId().equals(executionWithInputs.getId()) && this.isStopFollow(flow, item)) {
|
||||||
sink.success(ExecutionResponse.fromExecution(item, executionUrl(item)));
|
disposable.get().run();
|
||||||
|
future.complete(ExecutionResponse.fromExecution(item, executionUrl(item)));
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
sink.onDispose(receive::run);
|
|
||||||
}
|
}
|
||||||
} catch (IOException | QueueException e) {
|
sink.next(future);
|
||||||
sink.error(new RuntimeException(e));
|
} catch (QueueException e) {
|
||||||
|
sink.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return handle.flatMap(Mono::fromFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI executionUrl(Execution execution) {
|
private URI executionUrl(Execution execution) {
|
||||||
@@ -694,9 +703,10 @@ public class ExecutionController {
|
|||||||
throw new NoSuchElementException("Unable to find execution id '" + executionId + "'");
|
throw new NoSuchElementException("Unable to find execution id '" + executionId + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Flow> flow = flowRepository.findById(execution.get().getTenantId(), execution.get().getNamespace(), execution.get().getFlowId());
|
String flowId = execution.get().getFlowId();
|
||||||
|
Optional<Flow> flow = flowRepository.findById(execution.get().getTenantId(), execution.get().getNamespace(), flowId);
|
||||||
if (flow.isEmpty()) {
|
if (flow.isEmpty()) {
|
||||||
throw new NoSuchElementException("Unable to find flow id '" + executionId + "'");
|
throw new NoSuchElementException("Unable to find flow id '" + flowId + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
String prefix = StorageContext
|
String prefix = StorageContext
|
||||||
@@ -1182,17 +1192,14 @@ public class ExecutionController {
|
|||||||
public Publisher<ApiValidateExecutionInputsResponse> validateInputsOnResume(
|
public Publisher<ApiValidateExecutionInputsResponse> validateInputsOnResume(
|
||||||
@Parameter(description = "The execution id") @PathVariable String executionId,
|
@Parameter(description = "The execution id") @PathVariable String executionId,
|
||||||
@Parameter(description = "The inputs") @Nullable @Body MultipartBody inputs
|
@Parameter(description = "The inputs") @Nullable @Body MultipartBody inputs
|
||||||
) throws Exception {
|
) {
|
||||||
return Mono.<ApiValidateExecutionInputsResponse>create(sink -> {
|
Execution execution = executionService.getExecutionIfPause(tenantService.resolveTenant(), executionId);
|
||||||
try {
|
Flow flow = flowRepository.findByExecutionWithoutAcl(execution);
|
||||||
Execution execution = executionService.getExecutionIfPause(tenantService.resolveTenant(), executionId);
|
|
||||||
Flow flow = flowRepository.findByExecutionWithoutAcl(execution);
|
return executionService.validateForResume(execution, flow, inputs)
|
||||||
List<InputAndValue> values = this.executionService.validateForResume(execution, flow, inputs);
|
.map(values -> ApiValidateExecutionInputsResponse.of(execution.getFlowId(), execution.getNamespace(), values))
|
||||||
sink.success(ApiValidateExecutionInputsResponse.of(execution.getFlowId(), execution.getNamespace(), values));
|
// need to consume the inputs in case of error
|
||||||
} catch (Exception e) {
|
.doOnError(t -> Flux.from(inputs).subscribeOn(Schedulers.boundedElastic()).blockLast());
|
||||||
sink.error(new RuntimeException(e));
|
|
||||||
}
|
|
||||||
}).doOnError(t -> Flux.from(inputs).subscribeOn(Schedulers.boundedElastic()).blockLast()); // need to consume the inputs in case of error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExecuteOn(TaskExecutors.IO)
|
@ExecuteOn(TaskExecutors.IO)
|
||||||
@@ -1205,19 +1212,20 @@ public class ExecutionController {
|
|||||||
@Parameter(description = "The execution id") @PathVariable String executionId,
|
@Parameter(description = "The execution id") @PathVariable String executionId,
|
||||||
@Parameter(description = "The inputs") @Nullable @Body MultipartBody inputs
|
@Parameter(description = "The inputs") @Nullable @Body MultipartBody inputs
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
return Mono.<HttpResponse<?>>create(sink -> {
|
Execution execution = executionService.getExecutionIfPause(tenantService.resolveTenant(), executionId);
|
||||||
try {
|
Flow flow = flowRepository.findByExecutionWithoutAcl(execution);
|
||||||
Execution execution = executionService.getExecutionIfPause(tenantService.resolveTenant(), executionId);
|
|
||||||
Flow flow = flowRepository.findByExecutionWithoutAcl(execution);
|
return this.executionService.resume(execution, flow, State.Type.RUNNING, inputs)
|
||||||
Execution resumeExecution = this.executionService.resume(execution, flow, State.Type.RUNNING, inputs);
|
.<HttpResponse<?>>handle((resumeExecution, sink) -> {
|
||||||
this.executionQueue.emit(resumeExecution);
|
try {
|
||||||
sink.success(HttpResponse.noContent());
|
this.executionQueue.emit(resumeExecution);
|
||||||
} catch (IllegalStateException | NoSuchElementException e) {
|
sink.next(HttpResponse.noContent());
|
||||||
sink.error(e);
|
} catch (QueueException e) {
|
||||||
} catch (Exception e) {
|
sink.error(e);
|
||||||
sink.error(new RuntimeException(e));
|
}
|
||||||
}
|
})
|
||||||
}).doOnError(t -> Flux.from(inputs).subscribeOn(Schedulers.boundedElastic()).blockLast()); // need to consume the inputs in case of error
|
// need to consume the inputs in case of error
|
||||||
|
.doOnError(t -> Flux.from(inputs).subscribeOn(Schedulers.boundedElastic()).blockLast());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExecuteOn(TaskExecutors.IO)
|
@ExecuteOn(TaskExecutors.IO)
|
||||||
|
|||||||
Reference in New Issue
Block a user