Compare commits

...

46 Commits

Author SHA1 Message Date
Florian Hussonnois
86f7eadb3c chore: update version to v0.19.2 2024-10-08 15:03:27 +02:00
Loïc Mathieu
2def5cf7f8 fix(jdbc): always include deleted the the logs and metrics queries
Even if not needed to be sure we use the correct index.
2024-10-08 13:11:51 +02:00
Florian Hussonnois
d184858abf feat(core): move service usages 2024-10-08 11:02:11 +02:00
Sachin
dfa5875fa1 feat(ui): add chart visibility toggle to flows and logs page (#5345)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-08 10:08:33 +02:00
Sachin
ac4f7f261d fix(ui): amend translation keys usage (#5346)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-08 09:48:38 +02:00
GitHub Action
ae55685d2e chore(translations): auto generate values for languages other than english 2024-10-08 09:26:20 +02:00
Sai Mounika Peri
dd34317e4f feat(ui): improve page shown when flow has no dependencies (#5340) 2024-10-08 09:26:11 +02:00
riya mustare
f95e3073dd chore(ui): reduced line height on input description (#5344) 2024-10-08 09:18:03 +02:00
Florian Hussonnois
9f20988997 fix(core): use tenant for resolving worker groups 2024-10-07 14:16:00 +02:00
Sachin
5da3ab4f71 fix(ui): add bottom border on debug outputs (#5334)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 13:06:30 +02:00
Sachin
243eaab826 fix(ui): prevent removal of empty fields in metadata editor (#5313)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:25:37 +02:00
Sachin
6d362d688d fix(ui): amend flow disable from low code editor (#5315)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:20:28 +02:00
brian.mulier
39a01e0e7d fix(core): windows backslashes in paths were leading to wrong URI being created leading to error upon execution deletion 2024-10-07 11:19:35 +02:00
Sachin
a44b2ef7cb fix(ui): persisting flow metadata from low code editor (#5316)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:15:16 +02:00
Sachin
6bcad13444 feat(ui): added executions tab to single namespace (#5322)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:05:02 +02:00
Antoine Gauthier
02acf01ea5 chore(ui): update button conditions based on flow states (#5319) 2024-10-07 10:39:06 +02:00
Sai Mounika Peri
55193361b8 chore(ui): improve validation for kv store (#5321)
* Validation error of previous type should be cleared once the KV type is changed

* chore(ui): remove comment as code is self-explanatory

---------

Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2024-10-07 09:28:38 +02:00
brian.mulier
8d509a3ba5 fix(core): path matcher for windows were not working 2024-10-04 19:41:30 +02:00
GitHub Action
500680bcf7 chore(translations): auto generate values for languages other than english 2024-10-04 15:47:10 +02:00
Miloš Paunović
412c27cb12 chore(ui): improve the dashboard ratios calculation (#5311) 2024-10-04 15:46:59 +02:00
Sachin
8d7d9a356f chore(ui): use improved chart for flow executions (#5309)
* Replace the Flows Execution barchart with the barchart used on the main dashboard

* chore(ui): added bottom margin

---------

Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2024-10-04 15:01:14 +02:00
Miloš Paunović
d2ab2e97b4 fix(ui): prevent cases where dashboard totals shows nan instead of value (#5308) 2024-10-04 11:01:41 +02:00
Miloš Paunović
6a0f360fc6 fix(ui): amend end date on dashboard refresh (#5303) 2024-10-04 09:14:07 +02:00
Vivek Gangwani
0484fd389a chore(ui): making the color scheme the same for gantt and topology(#5280) 2024-10-04 09:13:14 +02:00
Miloš Paunović
e92aac3b39 chore(ui): re-calculate translation strings for left menu after language change (#5302) 2024-10-04 08:04:02 +02:00
Miloš Paunović
39b8ac8804 chore(ci): add check for translation keys matching (#5301) 2024-10-04 07:37:15 +02:00
Miloš Paunović
f928ed5876 chore(ui): uniform translation keys across languages (#5298) 2024-10-04 07:37:06 +02:00
Miloš Paunović
54856af0a8 fix(ui): amend logs scrolling for the last task (#5294) 2024-10-03 16:28:02 +02:00
MilosPaunovic
8bd79e82ab chore(ci): exit workflow with success if no changes are present 2024-10-03 16:27:53 +02:00
MilosPaunovic
104a491b92 chore(ci): separate direct pull requests and the ones from forked repositories 2024-10-03 16:27:44 +02:00
MilosPaunovic
5f46a0dd16 chore(ci): expose paste to editor function globally for testing 2024-10-03 16:27:35 +02:00
Loïc Mathieu
24c3703418 fix(core): hide secret inputs in logs
Fixes #5259
2024-10-03 10:34:27 +02:00
yuri
e5af245855 fix(ui): enable keyboard shortcut to launch execution (#5288) 2024-10-03 08:19:06 +02:00
Vivek Gangwani
d58e8f98a2 fix (ui): Unable to unselect the currently chosen log level (#5287)
* Update root.scss to Fix Topology View for Light Mode

* Update root-dark.scss to unify Gantt and TOpology View Colors

* Added deselect button for Log Levels
2024-10-03 08:18:49 +02:00
MilosPaunovic
ce2f1bfdb3 chore(ui): uniform using import class 2024-10-02 15:15:48 +02:00
Miloš Paunović
b619f88eff chore(ci): generate translation values as a commit to existing pull request (#5278) 2024-10-02 12:48:39 +02:00
Sai Mounika Peri
1f1775752b chore(ui): update parent from metadata editor (#5265) 2024-10-02 11:10:03 +02:00
AbdurRahman2004
b2475e53a2 chore(ui): move the delete logs button to top (#5266)
* Move 'Delete logs' button to top right corner of navigation

---------

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2024-10-02 10:49:59 +02:00
Antoine Gauthier
7e8956a0b7 fix(ui): amend typos in french translations (#5272) 2024-10-02 10:48:14 +02:00
brian.mulier
6537ee984b chore(version): update to version 'v0.19.1'. 2024-10-01 22:32:48 +02:00
brian.mulier
573aa48237 fix(ci): add back datahub plugin to ci build 2024-10-01 22:32:07 +02:00
brian.mulier
66ddeaa219 chore(version): update to version 'v0.19.0'. 2024-10-01 18:15:40 +02:00
brian.mulier
02c5e8a1a2 fix(ci): remove datahub plugin for now as it's not finished 2024-10-01 18:15:40 +02:00
brian.mulier
733c7897b9 fix(ci): restore github release on main workflow in case of skipped e2e 2024-10-01 15:33:36 +02:00
brian.mulier
c051287688 fix(ci): publish maven even if E2E were skipped 2024-10-01 14:26:02 +02:00
brian.mulier
1af8de6bce fix(ci): no more docker build & E2E for tags build 2024-10-01 13:43:35 +02:00
69 changed files with 1098 additions and 186 deletions

View File

@@ -1,45 +1,111 @@
name: Generate Translations
on:
pull_request:
types: [opened, synchronize]
paths:
- "ui/src/translations/en.json"
push:
branches:
- develop
paths:
- 'ui/src/translations/en.json'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
jobs:
generate-translations:
name: Generate Translations and Create PR
commit:
name: Commit directly to PR
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.head.repo.fork == false }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 10 # Ensures that at least 10 commits are fetched for comparison
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 10
ref: ${{ github.head_ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: pip install gitpython openai
- name: Install Python dependencies
run: pip install gitpython openai
- name: Generate translations
run: python ui/src/translations/generate_translations.py
- name: Generate translations
run: python ui/src/translations/generate_translations.py
- name: Commit, push changes, and create PR
env:
GH_TOKEN: ${{ github.token }}
run: |
git config --global user.name "GitHub Action"
git config --global user.email "actions@github.com"
BRANCH_NAME="translations/update-translations-$(date +%s)"
git checkout -b $BRANCH_NAME
git add ui/src/translations/*.json
git commit -m "Auto-generate translations from en.json"
git push --set-upstream origin $BRANCH_NAME
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
- 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: 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

View File

@@ -68,6 +68,7 @@ jobs:
# Get Plugins List
- name: Get Plugins List
uses: ./.github/actions/plugins-list
if: "!startsWith(github.ref, 'refs/tags/v')"
id: plugins-list
with:
plugin-version: ${{ env.PLUGIN_VERSION }}
@@ -75,6 +76,7 @@ jobs:
# Set Plugins List
- name: Set Plugin List
id: plugins
if: "!startsWith(github.ref, 'refs/tags/v')"
run: |
PLUGINS="${{ steps.plugins-list.outputs.plugins }}"
TAG=${GITHUB_REF#refs/*/}
@@ -122,6 +124,7 @@ jobs:
# Docker Build
- name: Build & Export Docker Image
uses: docker/build-push-action@v6
if: "!startsWith(github.ref, 'refs/tags/v')"
with:
context: .
push: false
@@ -149,6 +152,7 @@ jobs:
- name: Upload Docker
uses: actions/upload-artifact@v4
if: "!startsWith(github.ref, 'refs/tags/v')"
with:
name: ${{ steps.vars.outputs.artifact }}
path: /tmp/${{ steps.vars.outputs.artifact }}.tar
@@ -156,7 +160,7 @@ jobs:
check-e2e:
name: Check E2E Tests
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
strategy:
fail-fast: false
@@ -276,7 +280,11 @@ jobs:
name: Github Release
runs-on: ubuntu-latest
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:
# Download Exec
- name: Download executable
@@ -368,7 +376,11 @@ jobs:
name: Publish to Maven
runs-on: ubuntu-latest
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:
- uses: actions/checkout@v4

View File

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

View File

@@ -62,4 +62,8 @@ public class Usage {
@Valid
private final ExecutionUsage executions;
@Valid
@Nullable
private ServiceUsage services;
}

View File

@@ -11,6 +11,7 @@ import io.kestra.core.services.KVStoreService;
import io.kestra.core.storages.Storage;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.storages.kv.KVStore;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.utils.VersionProvider;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Value;
@@ -30,7 +31,6 @@ import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static io.kestra.core.utils.MapUtils.mergeWithNullableValues;
@@ -67,6 +67,7 @@ public class DefaultRunContext extends RunContext {
private String triggerExecutionId;
private Storage storage;
private Map<String, Object> pluginConfiguration;
private List<String> secretInputs;
private final AtomicBoolean isInitialized = new AtomicBoolean(false);
@@ -98,6 +99,15 @@ public class DefaultRunContext extends RunContext {
return variables;
}
/**
* {@inheritDoc}
*/
@Override
@JsonInclude
public List<String> getSecretInputs() {
return secretInputs;
}
@JsonIgnore
public ApplicationContext getApplicationContext() {
return applicationContext;
@@ -123,6 +133,17 @@ public class DefaultRunContext extends RunContext {
void setLogger(final RunContextLogger 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) {
@@ -488,6 +509,7 @@ public class DefaultRunContext extends RunContext {
private String triggerExecutionId;
private RunContextLogger logger;
private KVStoreService kvStoreService;
private List<String> secretInputs;
/**
* Builds the new {@link DefaultRunContext} object.
@@ -507,6 +529,7 @@ public class DefaultRunContext extends RunContext {
context.storage = storage;
context.triggerExecutionId = triggerExecutionId;
context.kvStoreService = kvStoreService;
context.secretInputs = secretInputs;
return context;
}
}

View File

@@ -748,7 +748,8 @@ public class ExecutorService {
.map(WorkerGroup::getKey)
.orElse(null);
// 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
if (workerGroupExecutorInterface.isWorkerGroupAvailableForKey(workerGroup)) {
return workerTask;

View File

@@ -47,6 +47,12 @@ public abstract class RunContext {
@JsonInclude
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 Object renderTyped(String inline) throws IllegalVariableEvaluationException;

View File

@@ -5,6 +5,7 @@ import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.TaskRun;
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.triggers.AbstractTrigger;
import io.kestra.core.plugins.PluginConfigurations;
@@ -15,12 +16,12 @@ import io.kestra.core.storages.StorageContext;
import io.kestra.core.storages.StorageInterface;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Value;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@@ -83,8 +84,10 @@ public class RunContextFactory {
.withFlow(flow)
.withExecution(execution)
.withDecryptVariables(true)
.withSecretInputs(secretInputsFromFlow(flow))
)
.build(runContextLogger))
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -107,8 +110,10 @@ public class RunContextFactory {
.withExecution(execution)
.withTaskRun(taskRun)
.withDecryptVariables(decryptVariables)
.withSecretInputs(secretInputsFromFlow(flow))
.build(runContextLogger))
.withKvStoreService(kvStoreService)
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -122,8 +127,10 @@ public class RunContextFactory {
.withVariables(newRunVariablesBuilder()
.withFlow(flow)
.withTrigger(trigger)
.withSecretInputs(secretInputsFromFlow(flow))
.build(runContextLogger)
)
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -135,6 +142,7 @@ public class RunContextFactory {
.withLogger(runContextLogger)
.withStorage(new InternalStorage(runContextLogger.logger(), StorageContext.forFlow(flow), storageInterface, flowService))
.withVariables(variables)
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -177,6 +185,16 @@ public class RunContextFactory {
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() {
return new DefaultRunContext.Builder()
// inject mandatory services and config

View File

@@ -9,6 +9,7 @@ import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.input.SecretInput;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.utils.ListUtils;
import lombok.AllArgsConstructor;
import lombok.With;
@@ -125,6 +126,8 @@ public final class RunVariables {
Builder withGlobals(Map<?, ?> globals);
Builder withSecretInputs(List<String> secretInputs);
/**
* Builds the immutable map of run variables.
*
@@ -152,6 +155,7 @@ public final class RunVariables {
protected Map<String, ?> envs;
protected Map<?, ?> globals;
private final Optional<String> secretKey;
private List<String> secretInputs;
public DefaultBuilder() {
this(Optional.empty());
@@ -252,6 +256,16 @@ public final class RunVariables {
if (!inputs.isEmpty()) {
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) {

View File

@@ -704,8 +704,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
}
private WorkerTask runAttempt(WorkerTask workerTask) throws QueueException {
DefaultRunContext runContext = (DefaultRunContext) workerTask.getRunContext();
runContextInitializer.forWorker(runContext, workerTask);
DefaultRunContext runContext = runContextInitializer.forWorker((DefaultRunContext) workerTask.getRunContext(), workerTask);;
Logger logger = runContext.logger();

View File

@@ -13,12 +13,13 @@ import java.util.Set;
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 tenant The tenant's ID - can be {@code null}.
* @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.
@@ -46,7 +47,7 @@ public interface WorkerGroupExecutorInterface {
class DefaultWorkerGroupExecutorInterface implements WorkerGroupExecutorInterface {
@Override
public boolean isWorkerGroupExistForKey(String key) {
public boolean isWorkerGroupExistForKey(String key, String tenant) {
return true;
}

View File

@@ -5,6 +5,7 @@ import io.kestra.core.models.collectors.*;
import io.kestra.core.plugins.PluginRegistry;
import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.repositories.ServiceInstanceRepositoryInterface;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.VersionProvider;
@@ -24,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
import java.lang.management.ManagementFactory;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -66,6 +68,9 @@ public class CollectorService {
@Value("${kestra.anonymous-usage-report.uri}")
protected URI url;
@Inject
private ServiceInstanceRepositoryInterface serviceRepository;
private transient Usage defaultUsage;
protected synchronized Usage defaultUsage() {
@@ -109,7 +114,8 @@ public class CollectorService {
if (details) {
builder = builder
.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();
}

View File

@@ -99,7 +99,7 @@ public final class PathMatcherPredicate implements Predicate<Path> {
} else {
pattern = mayAddRecursiveMatch(p);
}
syntaxAndPattern = SYNTAX_GLOB + pattern;
syntaxAndPattern = SYNTAX_GLOB + pattern.replace("\\", "/");
}
return syntaxAndPattern;
})
@@ -125,7 +125,7 @@ public final class PathMatcherPredicate implements Predicate<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) {

View File

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

View File

@@ -3,16 +3,23 @@ package io.kestra.core.runners;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
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.State;
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.storages.StorageInterface;
import io.kestra.core.utils.TestsUtils;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.jcodings.util.Hash;
import org.junit.jupiter.api.Test;
import jakarta.validation.ConstraintViolationException;
import reactor.core.publisher.Flux;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -22,17 +29,19 @@ import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class InputsTest extends AbstractMemoryRunnerTest {
@Inject
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
private QueueInterface<LogEntry> logQueue;
public static Map<String, Object> inputs = ImmutableMap.<String, Object>builder()
.put("string", "myString")
.put("enum", "ENUM_VALUE")
@@ -351,4 +360,22 @@ public class InputsTest extends AbstractMemoryRunnerTest {
assertThat(((Map<?, ?>) execution.getInputs().get("json")).size(), is(0));
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: ********"));
}
}

View 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}}"

View File

@@ -1,5 +1,5 @@
version=0.19.0-SNAPSHOT
version=0.19.2
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.priority=low
org.gradle.priority=low

View File

@@ -350,7 +350,10 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
DSLContext context = DSL.using(configuration);
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();
});
}

View File

@@ -150,7 +150,10 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
DSLContext context = DSL.using(configuration);
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();
});
}
@@ -168,8 +171,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
.getDslContextWrapper()
.transactionResult(configuration -> {
DSLContext context = DSL.using(configuration);
SelectConditionStep<Record1<Object>> select = DSL
.using(configuration)
SelectConditionStep<Record1<Object>> select = context
.selectDistinct(field(field))
.from(this.jdbcRepository.getTable())
.where(this.defaultFilter(tenantId));
@@ -185,8 +187,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
.getDslContextWrapper()
.transactionResult(configuration -> {
DSLContext context = DSL.using(configuration);
SelectConditionStep<Record1<Object>> select = DSL
.using(configuration)
SelectConditionStep<Record1<Object>> select = context
.select(field("value"))
.from(this.jdbcRepository.getTable())
.where(this.defaultFilter(tenantId));

View File

@@ -237,7 +237,7 @@ public class LocalStorage implements StorageInterface {
Path prefix = (tenantId == null) ?
basePath.toAbsolutePath() :
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) {

View File

@@ -10,7 +10,8 @@
"preview": "vite preview",
"test:unit": "vitest run",
"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": {
"@js-joda/core": "^5.6.3",

View File

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

View File

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

View File

@@ -48,7 +48,7 @@
<el-col :xs="24" :sm="8" :lg="4">
<refresh-button
class="float-right"
@refresh="fetchAll()"
@refresh="refresh()"
:can-auto-refresh="canAutoRefresh"
/>
</el-col>
@@ -61,6 +61,7 @@
<Card
:icon="CheckBold"
:label="t('dashboard.success_ratio')"
:tooltip="t('dashboard.success_ratio_tooltip')"
:value="stats.success"
:redirect="{
name: 'executions/list',
@@ -77,6 +78,7 @@
<Card
:icon="Alert"
:label="t('dashboard.failure_ratio')"
:tooltip="t('dashboard.failure_ratio_tooltip')"
:value="stats.failed"
:redirect="{
name: 'executions/list',
@@ -140,7 +142,10 @@
v-model="descriptionDialog"
:title="$t('description')"
>
<Markdown :source="description" class="p-4 description" />
<Markdown
:source="description"
class="p-4 description"
/>
</el-dialog>
</span>
@@ -197,7 +202,6 @@
import {useI18n} from "vue-i18n";
import moment from "moment";
import _cloneDeep from "lodash/cloneDeep";
import {apiUrl} from "override/utils/route";
import State from "../../utils/state";
@@ -271,6 +275,13 @@
scope: ["USER"],
});
const refresh = async () => {
await updateParams({
startDate: filters.value.startDate,
endDate: moment().toISOString(true),
});
fetchAll();
};
const canAutoRefresh = ref(false);
const toggleAutoRefresh = (event) => {
canAutoRefresh.value = event;
@@ -290,29 +301,45 @@
const executions = ref({raw: {}, all: {}, yesterday: {}, today: {}});
const stats = computed(() => {
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) {
return total ? ((count / total) * 100).toFixed(2) : "0.00";
}
const total = Object.values(statesToCount).reduce(
(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 {
total,
success: `${percentage(counts[State.SUCCESS] || 0, total)}%`,
failed: `${percentage(counts[State.FAILED] || 0, total)}%`,
success: `${successRatio.toFixed(2)}%`,
failed: `${failedRatio.toFixed(2)}%`,
};
});
const transformer = (data) => {
return data.reduce((accumulator, value) => {
if (!accumulator) accumulator = _cloneDeep(value);
else {
for (const key in value.executionCounts) {
accumulator.executionCounts[key] += value.executionCounts[key];
}
accumulator = accumulator || {executionCounts: {}, duration: {}};
for (const key in value.duration) {
accumulator.duration[key] += value.duration[key];
}
for (const key in value.executionCounts) {
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;

View File

@@ -2,7 +2,14 @@
<div class="p-4 card">
<div class="d-flex pb-2 justify-content-between">
<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">
{{ label }}
@@ -31,6 +38,10 @@
type: String,
required: true,
},
tooltip: {
type: String,
default: undefined,
},
value: {
type: [String, Number],
required: true,
@@ -63,3 +74,9 @@
}
}
</style>
<style lang="scss">
.dashboard-card-tooltip {
width: 300px;
}
</style>

View File

@@ -4,7 +4,7 @@
:persistent="false"
transition=""
:hide-after="0"
:content="$t('change status tooltip')"
:content="$t('change state tooltip')"
raw-content
:placement="tooltipPosition"
>
@@ -15,7 +15,7 @@
:disabled="!enabled"
class="ms-0 me-1"
>
{{ $t('change status') }}
{{ $t('change state') }}
</component>
</el-tooltip>
@@ -25,7 +25,7 @@
</template>
<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>
Current status is : <status size="small" class="me-1" :status="execution.state.current" />
@@ -186,4 +186,4 @@
padding-left: 10px;
}
}
</style>
</style>

View File

@@ -5,7 +5,7 @@
@click="visible = !visible"
: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">
<template #header>
@@ -13,7 +13,7 @@
</template>
<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>
Current status is : <status size="small" class="me-1" :status="taskRun.state.current" />

View File

@@ -857,7 +857,7 @@
);
},
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() {
const includeNonTerminated = ref(false);

View File

@@ -68,6 +68,7 @@
:target-execution="execution"
:target-flow="flow"
: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>

View File

@@ -26,6 +26,7 @@
:total-count="countByLogLevel[logLevel]"
@previous="previousLogForLevel(logLevel)"
@next="nextLogForLevel(logLevel)"
@close="clearLogLevel(logLevel)"
/>
</el-form-item>
<el-form-item>
@@ -217,7 +218,17 @@
const sortedIndices = [...logIndicesForLevel, this.logCursor].filter(Utils.distinctFilter).sort(this.sortLogsByViewOrder);
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>

View File

@@ -373,4 +373,9 @@
.bordered {
border: 1px solid var(--bs-border-color)
}
.bordered > .el-collapse-item{
margin-bottom :0px !important
}
</style>

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

View File

@@ -22,6 +22,7 @@
import Tabs from "../Tabs.vue";
import Overview from "./Overview.vue";
import FlowDependencies from "./FlowDependencies.vue";
import FlowNoDependencies from "./FlowNoDependencies.vue";
import FlowMetrics from "./FlowMetrics.vue";
import FlowEditor from "./FlowEditor.vue";
import FlowTriggers from "./FlowTriggers.vue";
@@ -247,7 +248,7 @@
) {
tabs.push({
name: "dependencies",
component: FlowDependencies,
component: this.routeFlowDependencies,
title: this.$t("dependencies"),
count: this.dependenciesCount,
});
@@ -317,6 +318,9 @@
this.flow.namespace,
);
},
routeFlowDependencies() {
return this.dependenciesCount > 0 ? FlowDependencies : FlowNoDependencies;
}
},
unmounted() {
this.$store.commit("flow/setFlow", undefined);

View File

@@ -6,7 +6,7 @@
</el-alert>
<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-item :title="$t('advanced configuration')" name="advanced">
@@ -50,7 +50,7 @@
@click="onSubmit($refs.form); executeClicked = true;"
type="primary"
native-type="submit"
:disabled="flow.disabled || haveBadLabels"
:disabled="!flowCanBeExecuted"
>
{{ $t('launch execution') }}
</el-button>
@@ -111,6 +111,9 @@
haveBadLabels() {
return this.executionLabels.some(label => (label.key && !label.value) || (!label.key && label.value));
},
flowCanBeExecuted() {
return this.flow && !this.flow.disabled && !this.haveBadLabels;
}
},
methods: {
getExecutionLabels() {
@@ -152,7 +155,7 @@
return inputs;
},
onSubmit(formRef) {
if (formRef) {
if (formRef && this.flowCanBeExecuted) {
formRef.validate((valid) => {
if (!valid) {
return false;
@@ -175,7 +178,6 @@
});
}
},
state(input) {
const required = input.required === undefined ? true : input.required;

View File

@@ -67,18 +67,22 @@
@update:model-value="onDataTableValue('labels', $event)"
/>
</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>
<filters :storage-key="storageKeys.FLOWS_FILTERS" />
</el-form-item>
</template>
<template #top>
<state-global-chart
class="mb-4"
v-if="daily"
:ready="dailyReady"
:data="daily"
/>
<el-card v-if="showStatChart()" shadow="never" class="mb-4">
<ExecutionsBar :data="daily" :total="executionsCount" />
</el-card>
</template>
<template #table>
@@ -109,10 +113,10 @@
<el-button v-if="canDelete" @click="deleteFlows" :icon="TrashCan">
{{ $t('delete') }}
</el-button>
<el-button v-if="canUpdate" @click="enableFlows" :icon="FileDocumentCheckOutline">
<el-button v-if="canUpdate && anyFlowDisabled()" @click="enableFlows" :icon="FileDocumentCheckOutline">
{{ $t('enable') }}
</el-button>
<el-button v-if="canUpdate" @click="disableFlows" :icon="FileDocumentRemoveOutline">
<el-button v-if="canUpdate && anyFlowEnabled()" @click="disableFlows" :icon="FileDocumentRemoveOutline">
{{ $t('disable') }}
</el-button>
</bulk-select>
@@ -245,7 +249,6 @@
import DataTable from "../layout/DataTable.vue";
import SearchField from "../layout/SearchField.vue";
import StateChart from "../stats/StateChart.vue";
import StateGlobalChart from "../stats/StateGlobalChart.vue";
import Status from "../Status.vue";
import TriggerAvatar from "./TriggerAvatar.vue";
import MarkdownTooltip from "../layout/MarkdownTooltip.vue"
@@ -255,6 +258,7 @@
import LabelFilter from "../labels/LabelFilter.vue";
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue"
import {storageKeys} from "../../utils/constants";
import ExecutionsBar from "../../components/dashboard/components/charts/executions/Bar.vue"
export default {
mixins: [RouteContext, RestoreUrl, DataTableActions, SelectTableActions],
@@ -265,7 +269,6 @@
DateAgo,
SearchField,
StateChart,
StateGlobalChart,
Status,
TriggerAvatar,
MarkdownTooltip,
@@ -274,7 +277,8 @@
Upload,
LabelFilter,
ScopeFilterButtons,
TopNavBar
TopNavBar,
ExecutionsBar
},
data() {
return {
@@ -285,6 +289,7 @@
lastExecutionByFlowReady: false,
dailyReady: false,
file: undefined,
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_FLOWS_CHART))
};
},
computed: {
@@ -318,7 +323,12 @@
},
canUpdate() {
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);
},
},
beforeCreate(){
if(!this.$route.query.scope) {
@@ -329,9 +339,18 @@
selectionMapper(element) {
return {
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);
this.loadStats();
},
exportFlows() {
this.$toast().confirm(
this.$t("flow export", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
@@ -386,6 +405,12 @@
}
)
},
anyFlowDisabled() {
return this.selection.some(flow => !flow.enabled);
},
anyFlowEnabled() {
return this.selection.some(flow => flow.enabled);
},
enableFlows() {
this.$toast().confirm(
this.$t("flow enable", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
@@ -483,10 +508,10 @@
return _merge(base, queryFilter)
},
loadData(callback) {
loadStats() {
this.dailyReady = false;
if (this.user.hasAny(permission.EXECUTION)) {
if (this.user.hasAny(permission.EXECUTION) && this.showStatChart) {
this.$store
.dispatch("stat/daily", this.loadQuery({
startDate: this.$moment(this.startDate).add(-1, "day").startOf("day").toISOString(true),
@@ -496,6 +521,9 @@
this.dailyReady = true;
});
}
},
loadData(callback) {
this.loadStats();
this.$store
.dispatch("flow/findFlows", this.loadQuery({
@@ -540,7 +568,7 @@
rowClasses(row) {
return row && row.row && row.row.disabled ? "disabled" : "";
}
}
}
};
</script>

View File

@@ -139,7 +139,7 @@
<code>disabled</code>
</template>
<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>
</el-form-item>
</el-form>

View File

@@ -54,6 +54,7 @@
<el-button
:icon="Minus"
@click="deleteInput(index)"
:disabled="index === 0 && newInputs.length === 1"
/>
</el-button-group>
</div>

View File

@@ -57,6 +57,7 @@
<el-button
:icon="Minus"
@click="deleteInput(index)"
:disabled="index === 0 && newVariables.length === 1"
/>
</el-button-group>
</div>
@@ -117,8 +118,8 @@
this.newVariables[index][1] = event;
}
},
deleteInput(key) {
delete this.newVariables[key];
deleteInput(index) {
this.newVariables.splice(index, 1);
},
addVariable() {
this.newVariables.push(["", undefined]);

View File

@@ -29,7 +29,6 @@
import TaskEditor from "../flows/TaskEditor.vue";
import MetadataEditor from "../flows/MetadataEditor.vue";
import Editor from "./Editor.vue";
import yamlUtils from "../../utils/yamlUtils";
import {SECTIONS} from "../../utils/constants.js";
import LowCodeEditor from "../inputs/LowCodeEditor.vue";
import {editorViewTypes} from "../../utils/constants";
@@ -406,7 +405,7 @@
};
const updatePluginDocumentation = (event) => {
const taskType = yamlUtils.getTaskType(
const taskType = YamlUtils.getTaskType(
event.model.getValue(),
event.position
);
@@ -706,7 +705,7 @@
};
const save = async (e) => {
if (!currentTab?.value?.dirty && !props.isCreating) {
if (!haveChange.value && !props.isCreating) {
return;
}
if (e) {

View File

@@ -14,6 +14,7 @@
v-if="input.type === 'STRING' || input.type === 'URI'"
v-model="inputs[input.id]"
@update:model-value="onChange"
@confirm="onSubmit"
/>
<el-select
:full-height="false"
@@ -209,7 +210,7 @@
multiSelectInputs: {},
};
},
emits: ["update:modelValue"],
emits: ["update:modelValue", "confirm"],
created() {
this.inputsList.push(...(this.initialInputs ?? []));
this.validateInputs();
@@ -223,9 +224,10 @@
}, 500)
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();
this.onSubmit(this.$refs.form);
this.onSubmit();
}
};
@@ -248,6 +250,9 @@
onChange() {
this.$emit("update:modelValue", this.inputs);
},
onSubmit() {
this.$emit("confirm");
},
onMultiSelectChange(input, e) {
this.inputs[input] = JSON.stringify(e).toString();
this.onChange();

View File

@@ -302,6 +302,16 @@
}
})
}
// Expose paste function globally for testing
window.pasteToEditor = (textToPaste) => {
this.editor.executeEdits("", [
{
range: this.editor.getSelection(),
text: textToPaste,
},
]);
};
},
beforeUnmount: function () {
this.destroy();

View File

@@ -185,10 +185,11 @@
}
}
}
.markdown-tooltip {
*:last-child {
margin-bottom: 0;
}
line-height: 15px;
padding: 5px;
}
</style>

View File

@@ -14,10 +14,16 @@
</slot>
</h1>
</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">
<global-search class="trigger-flow-guided-step" />
</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" />
<div class="d-flex fixed-buttons">
<el-dropdown popper-class="">
@@ -100,6 +106,7 @@
import Update from "vue-material-design-icons/Update.vue";
import ProgressQuestion from "vue-material-design-icons/ProgressQuestion.vue";
import GlobalSearch from "./GlobalSearch.vue";
import TrashCan from "vue-material-design-icons/TrashCan.vue";
export default {
components: {
@@ -113,6 +120,7 @@
Update,
ProgressQuestion,
GlobalSearch,
TrashCan,
Impersonating
},
props: {
@@ -128,6 +136,7 @@
computed: {
...mapState("api", ["version"]),
...mapState("core", ["tutorialFlows"]),
...mapState("log", ["logs"]),
...mapGetters("core", ["guidedProperties"]),
...mapGetters("auth", ["user"]),
displayNavBar() {
@@ -136,7 +145,10 @@
tourEnabled(){
// Temporary solution to not showing the tour menu item for EE
return this.tutorialFlows?.length && !Object.keys(this.user).length
}
},
shouldDisplayDeleteButton() {
return this.$route.name === "flows/update" && this.$route.params?.tab === "logs"
},
},
methods: {
restartGuidedTour() {
@@ -144,6 +156,13 @@
this.$store.commit("core/setGuidedProperties", {tourStarted: false});
this.$tours["guidedTour"]?.start();
},
deleteLogs() {
this.$toast().confirm(
this.$t("delete_all_logs"),
() => this.$store.dispatch("log/deleteLogs", {namespace: this.namespace, flowId: this.flowId}),
() => {}
)
}
}
};

View File

@@ -7,12 +7,14 @@
<div class="d-flex align-items-center gap-2">
<chevron-up class="medium-icon nav-button" @click="forwardEvent('previous')" />
<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>
</template>
<script setup>
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
import Close from "vue-material-design-icons/Close.vue";
</script>
<script>
export default {
@@ -66,5 +68,12 @@
.medium-icon {
font-size: 1.1rem;
}
.close-button {
color: var(--el-text-color-secondary);
&:hover {
color: var(--el-text-color-primary);
}
}
}
</style>

View File

@@ -29,6 +29,13 @@
@update:filter-value="onDataTableValue"
/>
</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>
<filters :storage-key="storageKeys.LOGS_FILTERS" />
</el-form-item>
@@ -37,7 +44,7 @@
</el-form-item>
</template>
<template v-if="charts" #top>
<template v-if="showStatChart()" #top>
<el-card shadow="never" class="mb-3" v-loading="!statsReady">
<div class="state-global-charts">
<template v-if="hasStatsData">
@@ -55,11 +62,6 @@
</template>
</div>
</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 #table>
@@ -103,13 +105,13 @@
import LogChart from "../stats/LogChart.vue";
import Filters from "../saved-filters/Filters.vue";
import {storageKeys} from "../../utils/constants";
import TrashCan from "vue-material-design-icons/TrashCan.vue";
export default {
mixins: [RouteContext, RestoreUrl, DataTableActions],
components: {
Filters,
DataTable, LogLine, NamespaceSelect, DateFilter, SearchField, LogLevelSelector, RefreshButton, TopNavBar, LogChart, TrashCan},
DataTable, LogLine, NamespaceSelect, DateFilter, SearchField, LogLevelSelector, RefreshButton, TopNavBar, LogChart},
props: {
logLevel: {
type: String,
@@ -140,7 +142,8 @@
refreshDates: false,
statsReady: false,
statsData: [],
canAutoRefresh: false
canAutoRefresh: false,
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_LOGS_CHART))
};
},
computed: {
@@ -157,9 +160,6 @@
isFlowEdit() {
return this.$route.name === "flows/update"
},
shouldDisplayDeleteButton() {
return this.$route.name === "flows/update"
},
isNamespaceEdit() {
return this.$route.name === "namespaces/update"
},
@@ -203,6 +203,16 @@
onDateFilterTypeChange(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() {
this.refreshDates = !this.refreshDates;
this.load();
@@ -262,13 +272,6 @@
.then(() => {
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 +305,4 @@
}
}
}
.delete-logs-btn {
width: 200px;
}
</style>
</style>

View File

@@ -0,0 +1,15 @@
<template>
<Executions :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>

View File

@@ -48,6 +48,7 @@
import permission from "../../models/permission";
import action from "../../models/action";
import Overview from "./Overview.vue";
import Executions from "./Executions.vue";
import NamespaceKV from "./NamespaceKV.vue";
import NamespaceFlows from "./NamespaceFlows.vue";
import EditorView from "../inputs/EditorView.vue";
@@ -144,6 +145,14 @@
id: this.$route.query.id
}
},
{
name: "executions",
component: Executions,
title: this.$t("executions"),
query: {
id: this.$route.query.id
}
},
{
name: "dependencies",
component: NamespaceDependenciesWrapper,

View File

@@ -195,6 +195,11 @@
if (!newValue) {
this.resetKv();
}
},
"kv.type"() {
if (this.$refs.form) {
this.$refs.form.clearValidate("value");
}
}
},
data() {

View File

@@ -305,6 +305,12 @@
this.expandParentIfNeeded();
},
watch: {
"$i18n.locale": {
deep: true,
handler(){
this.localMenu = this.disabledCurrentRoute(this.generateMenu());
}
},
menu: {
handler(newVal, oldVal) {
// Check if the active menu item has changed, if yes then update the menu

View File

@@ -32,7 +32,7 @@ html.dark {
#{--bs-link-color-rgb}: to-rgb($secondary);
#{--bs-tertiary-color}: #C3BBE3;
$levels: info, running, danger, warning, success;
$levels: info, running, danger, warning;
@each $level in $levels {
.bg-#{$level} {
#{--bs-bg-opacity}: 0.2;

View File

@@ -88,7 +88,7 @@
$content-running: #7400df;
$content-alert: #ab0009;
$content-warning: #c15300;
$content-success: #017f5c;
$content-success: #03DABA;
#{--background-failed}: #fed6d9;
#{--background-success}: #e4f9f3;
#{--content-information}: $content-information;
@@ -178,4 +178,4 @@ $logLevels: "trace", "debug", "info", "warn", "error";
.text-tertiary {
color: var(--bs-tertiary-color);
}
}

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

View File

@@ -510,7 +510,8 @@
"success": "Trigger ist entsperrt"
},
"restart trigger": {
"button": "Trigger neu starten"
"button": "Trigger neu starten",
"tooltip": "Den Trigger neu starten"
},
"date format": "Datumsformat",
"timezone": "Zeitzone",
@@ -849,7 +850,10 @@
"trigger_disabled": "Dieser Trigger kann nur durch Code aktiviert werden.",
"no_flow_description": "Dieser Flow hat keine 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?",
"docs": "Dokumentation",
@@ -861,6 +865,11 @@
"active-slots": "Aktive Slots",
"concurrency": "Nebenläufigkeit",
"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."
}
}

View File

@@ -428,6 +428,10 @@
"flow enable": "Are you sure you want to enable <code>{flowCount}</code> flow(s)?",
"flows disabled": "<code>{count}</code> Flow(s) disabled",
"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",
"see dependencies": "See dependencies",
"dependencies missing acls": "No permissions on this flow",
@@ -855,7 +859,9 @@
"trigger_check_warning": "Warning: Usage of the `trigger` variable detected, executing your flow manually won't fullfill the trigger variable.",
"dashboard": {
"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_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_namespace": " (per namespace)",
"total_executions": "Total Executions",
@@ -877,6 +883,7 @@
"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",
"close sidebar": "close sidebar"
"close sidebar": "close sidebar",
"change_status": "Change status"
}
}

View File

@@ -510,7 +510,8 @@
"success": "Trigger desbloqueado"
},
"restart trigger": {
"button": "Reiniciar trigger"
"button": "Reiniciar trigger",
"tooltip": "Reiniciar el trigger"
},
"date format": "Formato de fecha",
"timezone": "Zona horaria",
@@ -849,7 +850,10 @@
"trigger_disabled": "Este trigger solo puede ser habilitado a través de código.",
"no_flow_description": "Este flow no tiene 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?",
"docs": "Documentos",
@@ -861,6 +865,11 @@
"active-slots": "Ranuras activas",
"concurrency": "Concurrente",
"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."
}
}

View File

@@ -28,6 +28,7 @@
"details": "Regardez l'onglet Révisions pour plus de détails sur la dernière version."
},
"create": {
"title": "Le flux 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."
}
@@ -214,14 +215,14 @@
"topology-graph": {
"graph-orientation": "Orientation du graph",
"zoom-in": "Zoomer",
"zoom-out": "Dézommer",
"zoom-out": "Dézoomer",
"zoom-reset": "Zoom par défaut",
"zoom-fit": "Voir tout"
},
"show task logs": "Afficher les journaux 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",
"specific task": "Tâche specifique",
"specific task": "Tâche spécifique",
"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 direct sub tasks count": "Display les sous tâches directes",
@@ -509,7 +510,8 @@
"success": "Le déclencheur est débloqué"
},
"restart trigger": {
"button": "Redémarrer trigger"
"button": "Redémarrer trigger",
"tooltip": "Redémarrer le trigger"
},
"date format": "Format de date",
"timezone": "Fuseau horaire",
@@ -624,12 +626,12 @@
"Set labels": "Ajouter des labels",
"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",
"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",
"loaded x dependencies": "{count} dépendances chargées",
"security_advice": {
"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",
"enable": "Activer l'authentification"
},
@@ -848,7 +850,10 @@
"trigger_disabled": "Ce trigger ne peut être activé que par le code.",
"no_flow_description": "Ce flow n'a pas de 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 ?",
"docs": "Documentation",
@@ -860,6 +865,11 @@
"active-slots": "Slots actifs",
"concurrency": "Concurrence",
"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."
}
}

View File

@@ -93,6 +93,7 @@ def translate_dict(en_dict, target_language):
translated_value = translate_dict(value, target_language)
else:
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
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)
en_flat = flatten_dict(current_en_dict)
to_translate = {k: en_flat[k] for k in keys_to_translate}
print("Changed data requiring translatation:", to_translate)
return to_translate

View File

@@ -510,7 +510,8 @@
"success": "Trigger अनलॉक किया गया"
},
"restart trigger": {
"button": "Trigger पुनः प्रारंभ करें"
"button": "Trigger पुनः प्रारंभ करें",
"tooltip": "ट्रिगर को पुनः प्रारंभ करें"
},
"date format": "दिनांक प्रारूप",
"timezone": "समय क्षेत्र",
@@ -849,7 +850,10 @@
"trigger_disabled": "इस trigger को केवल कोड के माध्यम से सक्षम किया जा सकता है।",
"no_flow_description": "इस flow का कोई विवरण नहीं है।",
"description": "विवरण",
"not_auth": "आपके पास इस विजेट को देखने की आवश्यक अनुमतियाँ नहीं हैं।"
"not_auth": "आपके पास इस विजेट को देखने की आवश्यक अनुमतियाँ नहीं हैं।",
"see_all": "सभी देखें",
"success_ratio_tooltip": "सफलता अनुपात SUCCESS, CANCELLED और WARNING निष्पादन का योग है, जिसे समाप्त स्थिति में कुल निष्पादन की संख्या से विभाजित किया जाता है।",
"failure_ratio_tooltip": "असफल अनुपात, FAILED, KILLED और RETRIED निष्पादन का योग है, जिसे समाप्त स्थिति में कुल निष्पादन की संख्या से विभाजित किया जाता है।"
},
"delete_log": "क्या आप वाकई log को हटाना चाहते हैं?",
"docs": "डॉक्स",
@@ -861,6 +865,11 @@
"active-slots": "सक्रिय स्लॉट्स",
"concurrency": "समानांतरता",
"open sidebar": "साइडबार खोलें",
"close sidebar": "साइडबार बंद करें"
"close sidebar": "साइडबार बंद करें",
"change_status": "स्थिति बदलें",
"in-our-documentation": "हमारे दस्तावेज़ में।",
"read-more": "और अधिक पढ़ें",
"flow-dependencies": "निर्भरता",
"flow-no-dependencies": "आपके flow की कोई dependencies नहीं हैं।"
}
}

View File

@@ -510,7 +510,8 @@
"success": "Trigger sbloccato"
},
"restart trigger": {
"button": "Riavvia trigger"
"button": "Riavvia trigger",
"tooltip": "Riavvia il trigger"
},
"date format": "Formato data",
"timezone": "Fuso orario",
@@ -849,7 +850,10 @@
"trigger_disabled": "Questo trigger può essere abilitato solo tramite codice.",
"no_flow_description": "Questo flow non ha 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?",
"docs": "Documenti",
@@ -861,6 +865,11 @@
"active-slots": "Slot attivi",
"concurrency": "Concurrency",
"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"
}
}

View File

@@ -510,7 +510,8 @@
"success": "triggerのロックが解除されました"
},
"restart trigger": {
"button": "triggerを再起動"
"button": "triggerを再起動",
"tooltip": "トリガーを再起動する"
},
"date format": "日付形式",
"timezone": "タイムゾーン",
@@ -849,7 +850,10 @@
"trigger_disabled": "このTriggerはコードを通じてのみ有効にできます。",
"no_flow_description": "このflowには説明がありません。",
"description": "説明",
"not_auth": "必要な権限がないため、このウィジェットを表示できません。"
"not_auth": "必要な権限がないため、このウィジェットを表示できません。",
"see_all": "すべて表示",
"success_ratio_tooltip": "成功率は、SUCCESS、CANCELLED、WARNINGの実行の合計を、終了状態にある実行の総数で割ったものです。",
"failure_ratio_tooltip": "失敗率は、FAILED、KILLED、および RETRIED の実行の合計を、終了状態にある実行の総数で割ったものです。"
},
"delete_log": "ログを削除してもよろしいですか?",
"docs": "ドキュメント",
@@ -861,6 +865,11 @@
"active-slots": "アクティブスロット",
"concurrency": "並行性",
"open sidebar": "サイドバーを開く",
"close sidebar": "サイドバーを閉じる"
"close sidebar": "サイドバーを閉じる",
"change_status": "ステータスを変更",
"in-our-documentation": "ドキュメント内で。",
"read-more": "続きを読む",
"flow-dependencies": "依存関係",
"flow-no-dependencies": "あなたのflowには依存関係がありません"
}
}

View File

@@ -510,7 +510,8 @@
"success": "Trigger가 잠금 해제되었습니다"
},
"restart trigger": {
"button": "Trigger 재시작"
"button": "Trigger 재시작",
"tooltip": "트리거 재시작"
},
"date format": "날짜 형식",
"timezone": "시간대",
@@ -849,7 +850,10 @@
"trigger_disabled": "이 trigger는 코드로만 활성화할 수 있습니다.",
"no_flow_description": "이 flow에는 설명이 없습니다.",
"description": "설명",
"not_auth": "이 위젯을 볼 수 있는 권한이 없습니다."
"not_auth": "이 위젯을 볼 수 있는 권한이 없습니다.",
"see_all": "모두 보기",
"success_ratio_tooltip": "성공 비율은 SUCCESS, CANCELLED 및 WARNING 실행의 합계를 종료된 상태의 총 실행 수로 나눈 값입니다.",
"failure_ratio_tooltip": "실패 비율은 FAILED, KILLED, RETRIED 실행의 합을 종료된 상태의 전체 실행 수로 나눈 값입니다."
},
"delete_log": "로그를 삭제하시겠습니까?",
"docs": "문서",
@@ -861,6 +865,11 @@
"active-slots": "활성 슬롯",
"concurrency": "동시성",
"open sidebar": "사이드바 열기",
"close sidebar": "사이드바 닫기"
"close sidebar": "사이드바 닫기",
"change_status": "상태 변경",
"in-our-documentation": "우리의 문서에서.",
"read-more": "자세히 알아보기",
"flow-dependencies": "종속성",
"flow-no-dependencies": "귀하의 flow에는 종속성이 없습니다."
}
}

View File

@@ -510,7 +510,8 @@
"success": "Trigger odblokowany"
},
"restart trigger": {
"button": "Restartuj trigger"
"button": "Restartuj trigger",
"tooltip": "Uruchom ponownie trigger"
},
"date format": "Format daty",
"timezone": "Strefa czasowa",
@@ -849,7 +850,10 @@
"trigger_disabled": "Ten trigger może być włączony tylko za pomocą kodu.",
"no_flow_description": "Ten flow nie ma opisu.",
"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?",
"docs": "Dokumentacja",
@@ -861,6 +865,11 @@
"active-slots": "Aktywne sloty",
"concurrency": "Współbieżność",
"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"
}
}

View File

@@ -510,7 +510,8 @@
"success": "Trigger desbloqueado"
},
"restart trigger": {
"button": "Reiniciar trigger"
"button": "Reiniciar trigger",
"tooltip": "Reiniciar o trigger"
},
"date format": "Formato de data",
"timezone": "Fuso horário",
@@ -849,7 +850,10 @@
"trigger_disabled": "Este trigger só pode ser habilitado através de código.",
"no_flow_description": "Este flow não tem 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?",
"docs": "Documentos",
@@ -861,6 +865,11 @@
"active-slots": "Slots ativos",
"concurrency": "Concorrência",
"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"
}
}

View File

@@ -510,7 +510,8 @@
"success": "Trigger разблокирован"
},
"restart trigger": {
"button": "Перезапустить trigger"
"button": "Перезапустить trigger",
"tooltip": "Перезапустить trigger"
},
"date format": "Формат даты",
"timezone": "Часовой пояс",
@@ -849,7 +850,10 @@
"trigger_disabled": "Этот trigger может быть включен только через код.",
"no_flow_description": "Этот flow не имеет описания.",
"description": "Описание",
"not_auth": "У вас нет необходимых разрешений для просмотра этого виджета."
"not_auth": "У вас нет необходимых разрешений для просмотра этого виджета.",
"see_all": "Посмотреть все",
"success_ratio_tooltip": "Коэффициент успеха — это сумма выполнений в состояниях SUCCESS, CANCELLED и WARNING, деленная на общее количество выполнений в завершенном состоянии.",
"failure_ratio_tooltip": "Отношение Failed — это сумма выполнений в состояниях FAILED, KILLED и RETRIED, деленная на общее количество выполнений в завершенном состоянии."
},
"delete_log": "Вы уверены, что хотите удалить log?",
"docs": "Документы",
@@ -861,6 +865,11 @@
"active-slots": "Активные слоты",
"concurrency": "Конкурентность",
"open sidebar": "открыть боковую панель",
"close sidebar": "закрыть боковую панель"
"close sidebar": "закрыть боковую панель",
"change_status": "Изменить статус",
"in-our-documentation": "в нашей документации.",
"read-more": "Узнать больше о",
"flow-dependencies": "зависимости",
"flow-no-dependencies": "Ваш flow не имеет зависимостей"
}
}

View File

@@ -510,7 +510,8 @@
"success": "触发器已解锁"
},
"restart trigger": {
"button": "重新启动触发器"
"button": "重新启动触发器",
"tooltip": "重新启动trigger"
},
"date format": "日期格式",
"timezone": "时区",
@@ -849,7 +850,10 @@
"trigger_disabled": "此trigger只能通过代码启用。",
"no_flow_description": "此flow没有描述。",
"description": "描述",
"not_auth": "您没有查看此小部件的必要权限。"
"not_auth": "您没有查看此小部件的必要权限。",
"see_all": "查看全部",
"success_ratio_tooltip": "成功率是SUCCESS、CANCELLED和WARNING执行次数之和除以终止状态下执行总次数的结果。",
"failure_ratio_tooltip": "失败率是FAILED、KILLED和RETRIED执行次数之和除以终止状态下执行总次数的结果。"
},
"delete_log": "您确定要删除这个log吗",
"docs": "文档",
@@ -861,6 +865,11 @@
"active-slots": "活动槽位",
"concurrency": "并发",
"open sidebar": "打开侧边栏",
"close sidebar": "关闭侧边栏"
"close sidebar": "关闭侧边栏",
"change_status": "更改状态",
"in-our-documentation": "在我们的文档中。",
"read-more": "了解更多关于",
"flow-dependencies": "依赖项",
"flow-no-dependencies": "您的flow没有任何依赖项"
}
}

View File

@@ -31,6 +31,8 @@ export const storageKeys = {
SELECTED_TENANT: "selectedTenant",
EXECUTE_FLOW_BEHAVIOUR: "executeFlowBehaviour",
SHOW_CHART: "showChart",
SHOW_FLOWS_CHART: "showFlowsChart",
SHOW_LOGS_CHART: "showLogsChart",
DEFAULT_NAMESPACE: "defaultNamespace",
LATEST_NAMESPACE: "latestNamespace",
PAGINATION_SIZE: "paginationSize",

View File

@@ -233,4 +233,8 @@ export default class State {
static icon() {
return _mapValues(STATE, (state) => state.icon);
}
static getTerminatedStates() {
return Object.values(STATE).filter(state => !state.isRunning).map(state => state.name);
}
}

View File

@@ -589,11 +589,11 @@ export default class YamlUtils {
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 = [];
for (const prop of order) {
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);
}
}

View File

@@ -694,9 +694,10 @@ public class ExecutionController {
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()) {
throw new NoSuchElementException("Unable to find flow id '" + executionId + "'");
throw new NoSuchElementException("Unable to find flow id '" + flowId + "'");
}
String prefix = StorageContext