mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
Replace the default dashboard with custom dashboard (#8769)
* feat: - Implement width property - Replace custom dashboard - Started to integrate the KPI chart * feat(ui): introduce dashboard chart layout system * feat(ui): introduce dashboard chart kpi card * chore(ui): amend layout widths for sm screen size * chore(ui): prevent editing of default dashboard * chore(ui): centering the KPI text inside the box * chore(ui): dashboard edit preview to respect set layouts * chore(ui): initial work on setting the default flow & namespace dashboards * fix(ui): make sure there is no naming clashes * feat: KPI chart backend implementation * feat: validation annotations * chore(ui): make chart legend align to right side * chore(ui): properly show chart labels * chore(ui): improve state and ID components inside custom tabels * chore(ui): add proper link to execution in tables * feat: implemented Triggers as Datasource for custom dashboards close kestra-io/kestra-ee#3740 * feat: modified the Markdown chart so now it accept different sources * feat: rename KPI property to numerator & where close #3739 * chore(ui): improve markdown component * chore(ui): markdown charts * chore(ui): markdown charts * chore(ui): markdown charts remove padding * chore(ui): markdown charts * feat: fixes + define custom dashboard equivalent to current default dashboard with some modification * fix: round double value * chore(ui): improve flows and ns charts * chore(ui): make sure that table shows execution links only if namespace and flowId exist * chore(ui): make sure markdown is properly shown on dashboard edititng * fix: correctly do preview instead of load on homepage * fix: correctly preview markdown chart and add description in default flow dashboard * fix: apply review changes * fix: modify test following classes modifications on charts * tests: restore package-lock * remove chromatic tools and a warning --------- Co-authored-by: MilosPaunovic <paun992@hotmail.com> Co-authored-by: Bart Ledoux <bledoux@kestra.io>
This commit is contained in:
@@ -24,8 +24,10 @@ import io.kestra.core.models.annotations.PluginProperty;
|
||||
import io.kestra.core.models.conditions.Condition;
|
||||
import io.kestra.core.models.conditions.ScheduleCondition;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.Chart;
|
||||
import io.kestra.core.models.dashboards.charts.DataChart;
|
||||
import io.kestra.core.models.dashboards.charts.DataChartKPI;
|
||||
import io.kestra.core.models.property.Data;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.tasks.Output;
|
||||
@@ -668,10 +670,25 @@ public class JsonSchemaGenerator {
|
||||
|
||||
TypeVariable<? extends Class<? extends Chart<?>>> dataFilterType = clz.getTypeParameters()[1];
|
||||
ParameterizedType chartAwareColumnDescriptor = ((ParameterizedType) ((WildcardType) ((ParameterizedType) dataFilterType.getBounds()[0]).getActualTypeArguments()[1]).getUpperBounds()[0]);
|
||||
|
||||
dataFilters.forEach(dataFilter -> {
|
||||
Type fieldsEnum = ((ParameterizedType) dataFilter.getGenericSuperclass()).getActualTypeArguments()[0];
|
||||
consumer.accept(typeContext.resolve(clz, fieldsEnum, typeContext.resolve(dataFilter, typeContext.resolve(chartAwareColumnDescriptor, fieldsEnum))));
|
||||
});
|
||||
} else if (DataChartKPI.class.isAssignableFrom(clz)) {
|
||||
List<Class<? extends DataFilterKPI<?, ?>>> dataFilterKPIs = getRegisteredPlugins()
|
||||
.stream()
|
||||
.flatMap(registeredPlugin -> registeredPlugin.getDataFiltersKPI().stream())
|
||||
.filter(Predicate.not(io.kestra.core.models.Plugin::isInternal))
|
||||
.toList();
|
||||
|
||||
TypeVariable<? extends Class<? extends Chart<?>>> dataFilterType = clz.getTypeParameters()[1];
|
||||
ParameterizedType chartAwareColumnDescriptor = ((ParameterizedType) ((WildcardType) ((ParameterizedType) dataFilterType.getBounds()[0]).getActualTypeArguments()[1]).getUpperBounds()[0]);
|
||||
|
||||
dataFilterKPIs.forEach(dataFilterKPI -> {
|
||||
Type fieldsEnum = ((ParameterizedType) dataFilterKPI.getGenericSuperclass()).getActualTypeArguments()[0];
|
||||
consumer.accept(typeContext.resolve(clz, fieldsEnum, typeContext.resolve(dataFilterKPI, typeContext.resolve(chartAwareColumnDescriptor, fieldsEnum))));
|
||||
});
|
||||
} else {
|
||||
consumer.accept(typeContext.resolve(clz));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package io.kestra.core.models.dashboards;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -21,6 +24,11 @@ public class ChartOption {
|
||||
|
||||
private String description;
|
||||
|
||||
@Builder.Default
|
||||
@Min(1)
|
||||
@Max(12)
|
||||
private int width = 6;
|
||||
|
||||
public List<String> neededColumns() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.kestra.plugin.core.dashboard.data.IData;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
@@ -24,13 +25,12 @@ import java.util.Set;
|
||||
@NoArgsConstructor
|
||||
@Plugin
|
||||
@EqualsAndHashCode
|
||||
public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin {
|
||||
public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
|
||||
@NotNull
|
||||
@NotBlank
|
||||
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
||||
private String type;
|
||||
|
||||
|
||||
private Map<String, C> columns;
|
||||
|
||||
@Setter
|
||||
@@ -42,8 +42,10 @@ public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
public void updateWhereWithGlobalFilters(List<QueryFilter> queryFilterList, ZonedDateTime startDate, ZonedDateTime endDate) {
|
||||
this.where = whereWithGlobalFilters(queryFilterList, startDate, endDate, this.where);
|
||||
}
|
||||
|
||||
public abstract Class<? extends QueryBuilderInterface<F>> repositoryClass();
|
||||
|
||||
public abstract void setGlobalFilter(List<QueryFilter> queryFilterList, ZonedDateTime startDate, ZonedDateTime endDate);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package io.kestra.core.models.dashboards;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.kestra.plugin.core.dashboard.data.IData;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@Plugin
|
||||
@EqualsAndHashCode
|
||||
public abstract class DataFilterKPI<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin, IData<F> {
|
||||
@NotNull
|
||||
@NotBlank
|
||||
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
||||
private String type;
|
||||
|
||||
private C columns;
|
||||
|
||||
@Setter
|
||||
private List<AbstractFilter<F>> numerator;
|
||||
|
||||
private List<AbstractFilter<F>> where;
|
||||
|
||||
public Set<F> aggregationForbiddenFields() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
public DataFilterKPI<F, C> clearFilters() {
|
||||
this.numerator = Collections.emptyList();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void updateWhereWithGlobalFilters(List<QueryFilter> queryFilterList, ZonedDateTime startDate, ZonedDateTime endDate) {
|
||||
this.numerator = whereWithGlobalFilters(queryFilterList, startDate, endDate, this.numerator);
|
||||
}
|
||||
|
||||
public abstract Class<? extends QueryBuilderInterface<F>> repositoryClass();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.kestra.core.models.dashboards.charts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.validations.DataChartKPIValidation;
|
||||
import io.kestra.plugin.core.dashboard.chart.kpis.KpiOption;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@Plugin
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
@EqualsAndHashCode
|
||||
@DataChartKPIValidation
|
||||
public abstract class DataChartKPI<P extends KpiOption, D extends DataFilterKPI<?, ?>> extends Chart<P> implements io.kestra.core.models.Plugin {
|
||||
@NotNull
|
||||
private D data;
|
||||
|
||||
public Integer minNumberOfAggregations() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Integer maxNumberOfAggregations() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import io.kestra.core.app.AppPluginInterface;
|
||||
import io.kestra.core.models.conditions.Condition;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.Chart;
|
||||
import io.kestra.core.models.tasks.ExecutableTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
@@ -36,6 +37,7 @@ public class PluginModule extends SimpleModule {
|
||||
addDeserializer(Task.class, new PluginDeserializer<>());
|
||||
addDeserializer(Chart.class, new PluginDeserializer<>());
|
||||
addDeserializer(DataFilter.class, new PluginDeserializer<>());
|
||||
addDeserializer(DataFilterKPI.class, new PluginDeserializer<>());
|
||||
addDeserializer(AbstractTrigger.class, new PluginDeserializer<>());
|
||||
addDeserializer(Condition.class, new PluginDeserializer<>());
|
||||
addDeserializer(TaskRunner.class, new PluginDeserializer<>());
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.app.AppPluginInterface;
|
||||
import io.kestra.core.models.Plugin;
|
||||
import io.kestra.core.models.conditions.Condition;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.Chart;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.tasks.logs.LogExporter;
|
||||
@@ -109,6 +110,7 @@ public class PluginScanner {
|
||||
List<Class<? extends AppBlockInterface>> appBlocks = new ArrayList<>();
|
||||
List<Class<? extends Chart<?>>> charts = new ArrayList<>();
|
||||
List<Class<? extends DataFilter<?, ?>>> dataFilters = new ArrayList<>();
|
||||
List<Class<? extends DataFilterKPI<?, ?>>> dataFiltersKPI = new ArrayList<>();
|
||||
List<Class<? extends LogExporter<?>>> logExporter = new ArrayList<>();
|
||||
List<Class<? extends AdditionalPlugin>> additionalPlugins = new ArrayList<>();
|
||||
List<String> guides = new ArrayList<>();
|
||||
@@ -169,6 +171,11 @@ public class PluginScanner {
|
||||
//noinspection unchecked
|
||||
dataFilters.add((Class<? extends DataFilter<?, ?>>) dataFilter.getClass());
|
||||
}
|
||||
case DataFilterKPI<?, ?> dataFilterKPI -> {
|
||||
log.debug("Loading DataFilterKPI plugin: '{}'", plugin.getClass());
|
||||
//noinspection unchecked
|
||||
dataFiltersKPI.add((Class<? extends DataFilterKPI<?, ?>>) dataFilterKPI.getClass());
|
||||
}
|
||||
case LogExporter<?> shipper -> {
|
||||
log.debug("Loading LogExporter plugin: '{}'", plugin.getClass());
|
||||
logExporter.add((Class<? extends LogExporter<?>>) shipper.getClass());
|
||||
@@ -225,6 +232,7 @@ public class PluginScanner {
|
||||
.taskRunners(taskRunners)
|
||||
.charts(charts)
|
||||
.dataFilters(dataFilters)
|
||||
.dataFiltersKPI(dataFiltersKPI)
|
||||
.guides(guides)
|
||||
.logExporters(logExporter)
|
||||
.additionalPlugins(additionalPlugins)
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.app.AppPluginInterface;
|
||||
import io.kestra.core.models.annotations.PluginSubGroup;
|
||||
import io.kestra.core.models.conditions.Condition;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.Chart;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.tasks.logs.LogExporter;
|
||||
@@ -45,6 +46,7 @@ public class RegisteredPlugin {
|
||||
private final List<Class<? extends AppBlockInterface>> appBlocks;
|
||||
private final List<Class<? extends Chart<?>>> charts;
|
||||
private final List<Class<? extends DataFilter<?, ?>>> dataFilters;
|
||||
private final List<Class<? extends DataFilterKPI<?, ?>>> dataFiltersKPI;
|
||||
private final List<Class<? extends LogExporter<?>>> logExporters;
|
||||
private final List<Class<? extends AdditionalPlugin>> additionalPlugins;
|
||||
private final List<String> guides;
|
||||
@@ -62,6 +64,7 @@ public class RegisteredPlugin {
|
||||
!appBlocks.isEmpty() ||
|
||||
!charts.isEmpty() ||
|
||||
!dataFilters.isEmpty() ||
|
||||
!dataFiltersKPI.isEmpty() ||
|
||||
!logExporters.isEmpty() ||
|
||||
!additionalPlugins.isEmpty()
|
||||
;
|
||||
@@ -116,6 +119,10 @@ public class RegisteredPlugin {
|
||||
return DataFilter.class;
|
||||
}
|
||||
|
||||
if (this.getDataFiltersKPI().stream().anyMatch(r -> r.getName().equals(cls))) {
|
||||
return DataFilterKPI.class;
|
||||
}
|
||||
|
||||
if (this.getAppBlocks().stream().anyMatch(r -> r.getName().equals(cls))) {
|
||||
return AppBlockInterface.class;
|
||||
}
|
||||
@@ -163,6 +170,7 @@ public class RegisteredPlugin {
|
||||
result.put("app-blocks", Arrays.asList(this.getAppBlocks().toArray(Class[]::new)));
|
||||
result.put("charts", Arrays.asList(this.getCharts().toArray(Class[]::new)));
|
||||
result.put("data-filters", Arrays.asList(this.getDataFilters().toArray(Class[]::new)));
|
||||
result.put("data-filters-kpi", Arrays.asList(this.getDataFiltersKPI().toArray(Class[]::new)));
|
||||
result.put("log-exporters", Arrays.asList(this.getLogExporters().toArray(Class[]::new)));
|
||||
result.put("additional-plugins", Arrays.asList(this.getAdditionalPlugins().toArray(Class[]::new)));
|
||||
|
||||
@@ -357,6 +365,12 @@ public class RegisteredPlugin {
|
||||
b.append("] ");
|
||||
}
|
||||
|
||||
if (!this.getDataFiltersKPI().isEmpty()) {
|
||||
b.append("[DataFiltersKPI: ");
|
||||
b.append(this.getDataFiltersKPI().stream().map(Class::getName).collect(Collectors.joining(", ")));
|
||||
b.append("] ");
|
||||
}
|
||||
|
||||
if (!this.getLogExporters().isEmpty()) {
|
||||
b.append("[Log Exporters: ");
|
||||
b.append(this.getLogExporters().stream().map(Class::getName).collect(Collectors.joining(", ")));
|
||||
|
||||
@@ -3,7 +3,9 @@ package io.kestra.core.repositories;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.Dashboard;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.DataChart;
|
||||
import io.kestra.core.models.dashboards.charts.DataChartKPI;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import jakarta.annotation.Nullable;
|
||||
|
||||
@@ -31,4 +33,6 @@ public interface DashboardRepositoryInterface {
|
||||
Dashboard delete(String tenantId, String id);
|
||||
|
||||
<F extends Enum<F>> ArrayListTotal<Map<String, Object>> generate(String tenantId, DataChart<?, DataFilter<F, ? extends ColumnDescriptor<F>>> dataChart, ZonedDateTime startDate, ZonedDateTime endDate, Pageable pageable) throws IOException;
|
||||
|
||||
<F extends Enum<F>> List<Map<String, Object>> generateKPI(String tenantId, DataChartKPI<?, DataFilterKPI<F, ? extends ColumnDescriptor<F>>> dataChart, ZonedDateTime startDate, ZonedDateTime endDate) throws IOException;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.kestra.core.repositories;
|
||||
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -18,4 +19,6 @@ public interface QueryBuilderInterface<F extends Enum<F>> {
|
||||
F dateFilterField();
|
||||
|
||||
ArrayListTotal<Map<String, Object>> fetchData(String tenantId, DataFilter<F, ? extends ColumnDescriptor<F>> filter, ZonedDateTime startDate, ZonedDateTime endDate, Pageable pageable) throws IOException;
|
||||
|
||||
Double fetchValue(String tenantId, DataFilterKPI<F, ? extends ColumnDescriptor<F>> descriptors, ZonedDateTime startDate, ZonedDateTime endDate, boolean numeratorFilter) throws IOException;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.models.triggers.TriggerContext;
|
||||
import io.kestra.plugin.core.dashboard.data.Triggers;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import jakarta.annotation.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -12,7 +13,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
public interface TriggerRepositoryInterface {
|
||||
public interface TriggerRepositoryInterface extends QueryBuilderInterface<Triggers.Fields> {
|
||||
Optional<Trigger> findLast(TriggerContext trigger);
|
||||
|
||||
Optional<Trigger> findByExecution(Execution execution);
|
||||
|
||||
25
core/src/main/java/io/kestra/core/utils/MathUtils.java
Normal file
25
core/src/main/java/io/kestra/core/utils/MathUtils.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package io.kestra.core.utils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
public class MathUtils {
|
||||
/**
|
||||
* Rounds a double value to a specified number of decimal places.
|
||||
*
|
||||
* @param value The double value to be rounded.
|
||||
* @param decimalPlaces The number of decimal places to round to.
|
||||
* Must be a non-negative integer.
|
||||
* @return The rounded double value.
|
||||
* @throws IllegalArgumentException If decimalPlaces is negative.
|
||||
*/
|
||||
public static double roundDouble(double value, int decimalPlaces) {
|
||||
if (decimalPlaces < 0) {
|
||||
throw new IllegalArgumentException("The number of decimal places must be non-negative.");
|
||||
}
|
||||
BigDecimal bd = BigDecimal.valueOf(value);
|
||||
bd = bd.setScale(decimalPlaces, RoundingMode.HALF_UP);
|
||||
|
||||
return bd.doubleValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import io.kestra.core.validations.validator.DataChartKPIValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = DataChartKPIValidator.class)
|
||||
public @interface DataChartKPIValidation {
|
||||
String message() default "invalid data chart";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import io.kestra.core.validations.validator.ExecutionsDataFilterValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = ExecutionsDataFilterValidator.class)
|
||||
public @interface ExecutionsDataFilterKPIValidation {
|
||||
String message() default "invalid executions data filter";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import io.kestra.core.validations.validator.RegexValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
|
||||
import static java.lang.annotation.ElementType.CONSTRUCTOR;
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import static java.lang.annotation.ElementType.*;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = RegexValidator.class)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package io.kestra.core.validations.validator;
|
||||
|
||||
import io.kestra.core.models.dashboards.charts.DataChartKPI;
|
||||
import io.kestra.core.validations.DataChartKPIValidation;
|
||||
import io.kestra.plugin.core.dashboard.data.Executions;
|
||||
import io.micronaut.context.annotation.Value;
|
||||
import io.micronaut.core.annotation.AnnotationValue;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import io.micronaut.core.annotation.NonNull;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidator;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
@Introspected
|
||||
public class DataChartKPIValidator implements ConstraintValidator<DataChartKPIValidation, DataChartKPI<?, ?>> {
|
||||
@Value("${kestra.repository.type}")
|
||||
private String repositoryType;
|
||||
|
||||
@Override
|
||||
public boolean isValid(
|
||||
@Nullable DataChartKPI<?, ?> dataChart,
|
||||
@NonNull AnnotationValue<DataChartKPIValidation> annotationMetadata,
|
||||
@NonNull ConstraintValidatorContext context) {
|
||||
if (dataChart == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> violations = new ArrayList<>();
|
||||
|
||||
if(dataChart.getData().getColumns() != null) {
|
||||
if (dataChart.getData().getColumns().getAgg() == null) {
|
||||
violations.add("Agg on column is required.");
|
||||
}
|
||||
}
|
||||
|
||||
if (dataChart.getData().getColumns().getField() != null && dataChart.getData().getColumns().getField().equals(Executions.Fields.LABELS)
|
||||
&& !repositoryType.equals("elasticsearch")) {
|
||||
violations.add("LABELS column is only supported with an ElasticSearch database.");
|
||||
}
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate("Invalid data chart: " + String.join(", ", violations))
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.kestra.core.validations.validator;
|
||||
|
||||
import io.kestra.core.validations.ExecutionsDataFilterValidation;
|
||||
import io.kestra.plugin.core.dashboard.data.Executions;
|
||||
import io.kestra.plugin.core.dashboard.data.ExecutionsKPI;
|
||||
import io.micronaut.core.annotation.AnnotationValue;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import io.micronaut.core.annotation.NonNull;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidator;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
@Introspected
|
||||
public class ExecutionsDataFilterKPIValidator implements ConstraintValidator<ExecutionsDataFilterValidation, ExecutionsKPI<?>> {
|
||||
@Override
|
||||
public boolean isValid(
|
||||
@Nullable ExecutionsKPI<?> executionsDataFilter,
|
||||
@NonNull AnnotationValue<ExecutionsDataFilterValidation> annotationMetadata,
|
||||
@NonNull ConstraintValidatorContext context) {
|
||||
if (executionsDataFilter == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> violations = new ArrayList<>();
|
||||
|
||||
if (executionsDataFilter.getColumns().getField() == Executions.Fields.LABELS && executionsDataFilter.getColumns().getLabelKey() == null) {
|
||||
violations.add("Column must have a `labelKey`.");
|
||||
}
|
||||
|
||||
executionsDataFilter.getNumerator().forEach(filter -> {
|
||||
if (filter.getField() == Executions.Fields.LABELS && filter.getLabelKey() == null) {
|
||||
violations.add("Label filters must have a `labelKey`.");
|
||||
}
|
||||
});
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate("Invalid Chart: " + String.join(", ", violations))
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.kestra.plugin.core.dashboard.chart;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.DataChartKPI;
|
||||
import io.kestra.plugin.core.dashboard.chart.kpis.KpiOption;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
@EqualsAndHashCode
|
||||
@Schema(
|
||||
title = "Track a specific value."
|
||||
)
|
||||
public class KPI <F extends Enum<F>, D extends DataFilterKPI<F, ? extends ColumnDescriptor<F>>> extends DataChartKPI<KpiOption, D> {
|
||||
|
||||
@Override
|
||||
public Integer minNumberOfAggregations() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer maxNumberOfAggregations() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package io.kestra.plugin.core.dashboard.chart;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ChartOption;
|
||||
import io.kestra.core.models.dashboards.charts.Chart;
|
||||
import io.kestra.plugin.core.dashboard.chart.mardown.sources.MarkdownSource;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
@@ -45,5 +45,12 @@ import lombok.experimental.SuperBuilder;
|
||||
}
|
||||
)
|
||||
public class Markdown extends Chart<ChartOption> {
|
||||
@Deprecated(forRemoval = true)
|
||||
@Schema(
|
||||
title = "[DEPRECATED]Markdown content to display",
|
||||
description = "Use the String source instead"
|
||||
)
|
||||
private String content;
|
||||
|
||||
private MarkdownSource source;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import lombok.experimental.SuperBuilder;
|
||||
}
|
||||
)
|
||||
public class TimeSeries<F extends Enum<F>, D extends DataFilter<F, ? extends TimeSeriesColumnDescriptor<F>>> extends DataChart<TimeSeriesOption, D> {
|
||||
|
||||
@Override
|
||||
public Integer minNumberOfAggregations() {
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.kestra.plugin.core.dashboard.chart.kpis;
|
||||
|
||||
import io.kestra.core.models.dashboards.ChartOption;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class KpiOption extends ChartOption {
|
||||
@Builder.Default
|
||||
private NumberType numberType = NumberType.FLAT;
|
||||
|
||||
public enum NumberType {
|
||||
FLAT,
|
||||
PERCENTAGE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.kestra.plugin.core.dashboard.chart.mardown.sources;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
|
||||
@Schema(
|
||||
title = "Flow Source to fetch description"
|
||||
)
|
||||
@Getter
|
||||
public class FlowDescription extends MarkdownSource {
|
||||
@Schema(
|
||||
title = "Flow ID"
|
||||
)
|
||||
@NotNull
|
||||
private String flowId;
|
||||
|
||||
@Schema(
|
||||
title = "Flow Namespace"
|
||||
)
|
||||
@NotNull
|
||||
private String namespace;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.kestra.plugin.core.dashboard.chart.mardown.sources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
property = "type",
|
||||
visible = true
|
||||
)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = FlowDescription.class, name = "FlowDescription"),
|
||||
@JsonSubTypes.Type(value = Text.class, name = "Text")
|
||||
})
|
||||
public class MarkdownSource {
|
||||
@NotNull
|
||||
@NotBlank
|
||||
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.kestra.plugin.core.dashboard.chart.mardown.sources;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Schema(
|
||||
title = "Markdown from text"
|
||||
)
|
||||
public class Text extends MarkdownSource {
|
||||
@Schema(
|
||||
title = "Markdown content to display"
|
||||
)
|
||||
private String content;
|
||||
}
|
||||
@@ -2,15 +2,10 @@ package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.dashboards.filters.Contains;
|
||||
import io.kestra.core.models.dashboards.filters.GreaterThanOrEqualTo;
|
||||
import io.kestra.core.models.dashboards.filters.LessThanOrEqualTo;
|
||||
import io.kestra.core.repositories.ExecutionRepositoryInterface;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.kestra.core.validations.ExecutionsDataFilterValidation;
|
||||
@@ -20,10 +15,6 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@@ -63,61 +54,9 @@ import java.util.List;
|
||||
}
|
||||
)
|
||||
@JsonTypeName("Executions")
|
||||
public class Executions<C extends ColumnDescriptor<Executions.Fields>> extends DataFilter<Executions.Fields, C> {
|
||||
public class Executions<C extends ColumnDescriptor<Executions.Fields>> extends DataFilter<Executions.Fields, C> implements IExecutions {
|
||||
@Override
|
||||
public Class<? extends QueryBuilderInterface<Executions.Fields>> repositoryClass() {
|
||||
return ExecutionRepositoryInterface.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGlobalFilter(List<QueryFilter> filters, ZonedDateTime startDate, ZonedDateTime endDate) {
|
||||
List<AbstractFilter<Fields>> where = this.getWhere() != null ? new ArrayList<>(this.getWhere()) : new ArrayList<>();
|
||||
|
||||
if (filters == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<QueryFilter> namespaceFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.NAMESPACE)).toList();
|
||||
if (!namespaceFilters.isEmpty()) {
|
||||
where.removeIf(filter -> filter.getField().equals(Executions.Fields.NAMESPACE));
|
||||
namespaceFilters.forEach(f -> {
|
||||
where.add(f.toDashboardFilterBuilder(Executions.Fields.NAMESPACE, f.value()));
|
||||
});
|
||||
}
|
||||
|
||||
List<QueryFilter> labelFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.LABELS)).toList();
|
||||
if (!labelFilters.isEmpty()) {
|
||||
where.removeIf(filter -> filter.getField().equals(Fields.LABELS));
|
||||
labelFilters.forEach(f -> {
|
||||
where.add(Contains.<Executions.Fields>builder().field(Fields.LABELS).value(f.value()).build());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (startDate != null || endDate != null) {
|
||||
if (startDate != null) {
|
||||
where.removeIf(f -> f.getField().equals(Fields.START_DATE));
|
||||
where.add(GreaterThanOrEqualTo.<Executions.Fields>builder().field(Fields.START_DATE).value(startDate.toInstant()).build());
|
||||
}
|
||||
if (endDate != null) {
|
||||
where.removeIf(f -> f.getField().equals(Fields.END_DATE));
|
||||
where.add(LessThanOrEqualTo.<Executions.Fields>builder().field(Fields.END_DATE).value(endDate.toInstant()).build());
|
||||
}
|
||||
}
|
||||
|
||||
this.setWhere(where);
|
||||
}
|
||||
|
||||
public enum Fields {
|
||||
ID,
|
||||
NAMESPACE,
|
||||
FLOW_ID,
|
||||
FLOW_REVISION,
|
||||
STATE,
|
||||
DURATION,
|
||||
LABELS,
|
||||
START_DATE,
|
||||
END_DATE,
|
||||
TRIGGER_EXECUTION_ID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.repositories.ExecutionRepositoryInterface;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.kestra.core.validations.ExecutionsDataFilterKPIValidation;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
@EqualsAndHashCode
|
||||
//@ExecutionsDataFilterValidation
|
||||
@Schema(
|
||||
title = "Display a chart with executions in success in a given namespace.",
|
||||
description = "Change."
|
||||
)
|
||||
@Plugin(
|
||||
examples = {
|
||||
@Example(
|
||||
title = "Display a chart with executions in success in a given namespace.",
|
||||
full = true,
|
||||
code = {
|
||||
"id: kpi_success_ratio\n" +
|
||||
"type: io.kestra.plugin.core.dashboard.chart.KPI\n" +
|
||||
"chartOptions:\n" +
|
||||
" displayName: Success Ratio\n" +
|
||||
" numberType: PERCENTAGE\n" +
|
||||
" width: 3\n" +
|
||||
"data:\n" +
|
||||
" type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI\n" +
|
||||
" columns:\n" +
|
||||
" field: ID\n" +
|
||||
" agg: COUNT\n" +
|
||||
" numerator:\n" +
|
||||
" - type: IN\n" +
|
||||
" field: STATE\n" +
|
||||
" values:\n" +
|
||||
" - SUCCESS\n"
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
@JsonTypeName("ExecutionsKPI")
|
||||
@ExecutionsDataFilterKPIValidation
|
||||
public class ExecutionsKPI<C extends ColumnDescriptor<ExecutionsKPI.Fields>> extends DataFilterKPI<ExecutionsKPI.Fields, C> implements IExecutions {
|
||||
@Override
|
||||
public Class<? extends QueryBuilderInterface<ExecutionsKPI.Fields>> repositoryClass() {
|
||||
return ExecutionRepositoryInterface.class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public interface IData<F extends Enum<F>> {
|
||||
List<AbstractFilter<F>> whereWithGlobalFilters(List<QueryFilter> queryFilterList, ZonedDateTime startDate, ZonedDateTime endDate, List<AbstractFilter<F>> where);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.dashboards.filters.Contains;
|
||||
import io.kestra.core.models.dashboards.filters.GreaterThanOrEqualTo;
|
||||
import io.kestra.core.models.dashboards.filters.LessThanOrEqualTo;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface IExecutions extends IData<IExecutions.Fields> {
|
||||
|
||||
default List<AbstractFilter<Fields>> whereWithGlobalFilters(List<QueryFilter> filters, ZonedDateTime startDate, ZonedDateTime endDate, List<AbstractFilter<Fields>> where) {
|
||||
List<AbstractFilter<Fields>> updatedWhere = where != null ? new ArrayList<>(where) : new ArrayList<>();
|
||||
|
||||
if (filters != null) {
|
||||
List<QueryFilter> namespaceFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.NAMESPACE)).toList();
|
||||
if (!namespaceFilters.isEmpty()) {
|
||||
updatedWhere.removeIf(filter -> filter.getField().equals(Fields.NAMESPACE));
|
||||
namespaceFilters.forEach(f -> {
|
||||
updatedWhere.add(f.toDashboardFilterBuilder(Fields.NAMESPACE, f.value()));
|
||||
});
|
||||
}
|
||||
|
||||
List<QueryFilter> labelFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.LABELS)).toList();
|
||||
if (!labelFilters.isEmpty()) {
|
||||
updatedWhere.removeIf(filter -> filter.getField().equals(Fields.LABELS));
|
||||
labelFilters.forEach(f -> {
|
||||
updatedWhere.add(Contains.<Fields>builder().field(Fields.LABELS).value(f.value()).build());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (startDate != null || endDate != null) {
|
||||
if (startDate != null) {
|
||||
updatedWhere.removeIf(f -> f.getField().equals(Fields.START_DATE));
|
||||
updatedWhere.add(GreaterThanOrEqualTo.<Fields>builder().field(Fields.START_DATE).value(startDate.toInstant()).build());
|
||||
}
|
||||
if (endDate != null) {
|
||||
updatedWhere.removeIf(f -> f.getField().equals(Fields.END_DATE));
|
||||
updatedWhere.add(LessThanOrEqualTo.<Fields>builder().field(Fields.END_DATE).value(endDate.toInstant()).build());
|
||||
}
|
||||
}
|
||||
|
||||
return updatedWhere;
|
||||
}
|
||||
|
||||
enum Fields {
|
||||
ID,
|
||||
NAMESPACE,
|
||||
FLOW_ID,
|
||||
FLOW_REVISION,
|
||||
STATE,
|
||||
DURATION,
|
||||
LABELS,
|
||||
START_DATE,
|
||||
END_DATE,
|
||||
TRIGGER_EXECUTION_ID
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.dashboards.filters.GreaterThanOrEqualTo;
|
||||
import io.kestra.core.models.dashboards.filters.LessThanOrEqualTo;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface ILogs extends IData<ILogs.Fields> {
|
||||
|
||||
default List<AbstractFilter<ILogs.Fields>> whereWithGlobalFilters(List<QueryFilter> filters, ZonedDateTime startDate, ZonedDateTime endDate, List<AbstractFilter<ILogs.Fields>> where) {
|
||||
List<AbstractFilter<ILogs.Fields>> updatedWhere = where != null ? new ArrayList<>(where) : new ArrayList<>();
|
||||
|
||||
if (filters != null) {
|
||||
|
||||
List<QueryFilter> namespaceFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.NAMESPACE)).toList();
|
||||
if (!namespaceFilters.isEmpty()) {
|
||||
updatedWhere.removeIf(filter -> filter.getField().equals(Fields.NAMESPACE));
|
||||
namespaceFilters.forEach(f -> {
|
||||
updatedWhere.add(f.toDashboardFilterBuilder(Fields.NAMESPACE, f.value()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (startDate != null || endDate != null) {
|
||||
updatedWhere.removeIf(f -> f.getField().equals(Fields.DATE));
|
||||
if (startDate != null) {
|
||||
updatedWhere.add(GreaterThanOrEqualTo.<Fields>builder().field(Fields.DATE).value(startDate.toInstant()).build());
|
||||
}
|
||||
if (endDate != null) {
|
||||
updatedWhere.add(LessThanOrEqualTo.<Fields>builder().field(Fields.DATE).value(endDate.toInstant()).build());
|
||||
}
|
||||
}
|
||||
|
||||
return updatedWhere;
|
||||
}
|
||||
|
||||
enum Fields {
|
||||
NAMESPACE,
|
||||
FLOW_ID,
|
||||
EXECUTION_ID,
|
||||
TASK_ID,
|
||||
DATE,
|
||||
TASK_RUN_ID,
|
||||
ATTEMPT_NUMBER,
|
||||
TRIGGER_ID,
|
||||
LEVEL,
|
||||
MESSAGE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.filters.*;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface IMetrics extends IData<IMetrics.Fields> {
|
||||
|
||||
default List<AbstractFilter<IMetrics.Fields>> whereWithGlobalFilters(List<QueryFilter> filters, ZonedDateTime startDate, ZonedDateTime endDate, List<AbstractFilter<IMetrics.Fields>> where) {
|
||||
List<AbstractFilter<IMetrics.Fields>> updatedWhere = where != null ? new ArrayList<>(where) : new ArrayList<>();
|
||||
|
||||
if (filters == null) {
|
||||
return updatedWhere;
|
||||
}
|
||||
|
||||
List<QueryFilter> namespaceFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.NAMESPACE)).toList();
|
||||
if (!namespaceFilters.isEmpty()) {
|
||||
updatedWhere.removeIf(filter -> filter.getField().equals(Fields.NAMESPACE));
|
||||
namespaceFilters.forEach(f -> {
|
||||
updatedWhere.add(EqualTo.<Fields>builder().field(Fields.NAMESPACE).value(f.value()).build());
|
||||
});
|
||||
}
|
||||
|
||||
return updatedWhere;
|
||||
}
|
||||
|
||||
enum Fields {
|
||||
NAMESPACE,
|
||||
FLOW_ID,
|
||||
TASK_ID,
|
||||
EXECUTION_ID,
|
||||
TASK_RUN_ID,
|
||||
TYPE,
|
||||
NAME,
|
||||
VALUE,
|
||||
DATE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.dashboards.filters.EqualTo;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface ITriggers extends IData<ITriggers.Fields> {
|
||||
|
||||
default List<AbstractFilter<ITriggers.Fields>> whereWithGlobalFilters(List<QueryFilter> filters, ZonedDateTime startDate, ZonedDateTime endDate, List<AbstractFilter<ITriggers.Fields>> where) {
|
||||
List<AbstractFilter<ITriggers.Fields>> updatedWhere = where != null ? new ArrayList<>(where) : new ArrayList<>();
|
||||
|
||||
if (filters == null) {
|
||||
return updatedWhere;
|
||||
}
|
||||
|
||||
List<QueryFilter> namespaceFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.NAMESPACE)).toList();
|
||||
if (!namespaceFilters.isEmpty()) {
|
||||
updatedWhere.removeIf(filter -> filter.getField().equals(ITriggers.Fields.NAMESPACE));
|
||||
namespaceFilters.forEach(f -> {
|
||||
updatedWhere.add(EqualTo.<ITriggers.Fields>builder().field(ITriggers.Fields.NAMESPACE).value(f.value()).build());
|
||||
});
|
||||
}
|
||||
|
||||
return updatedWhere;
|
||||
}
|
||||
|
||||
|
||||
enum Fields {
|
||||
ID,
|
||||
NAMESPACE,
|
||||
FLOW_ID,
|
||||
TRIGGER_ID,
|
||||
EXECUTION_ID,
|
||||
NEXT_EXECUTION_DATE,
|
||||
WORKER_ID
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.dashboards.filters.GreaterThanOrEqualTo;
|
||||
import io.kestra.core.models.dashboards.filters.LessThanOrEqualTo;
|
||||
import io.kestra.core.repositories.LogRepositoryInterface;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -17,9 +13,6 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@@ -62,56 +55,14 @@ import java.util.Set;
|
||||
)
|
||||
}
|
||||
)
|
||||
public class Logs<C extends ColumnDescriptor<Logs.Fields>> extends DataFilter<Logs.Fields, C> {
|
||||
public class Logs<C extends ColumnDescriptor<ILogs.Fields>> extends DataFilter<ILogs.Fields, C> implements ILogs {
|
||||
@Override
|
||||
public Class<? extends QueryBuilderInterface<Logs.Fields>> repositoryClass() {
|
||||
public Class<? extends QueryBuilderInterface<ILogs.Fields>> repositoryClass() {
|
||||
return LogRepositoryInterface.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGlobalFilter(List<QueryFilter> filters, ZonedDateTime startDate, ZonedDateTime endDate) {
|
||||
List<AbstractFilter<Fields>> where = this.getWhere() != null ? new ArrayList<>(this.getWhere()) : new ArrayList<>();
|
||||
|
||||
if (filters == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<QueryFilter> namespaceFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.NAMESPACE)).toList();
|
||||
if (!namespaceFilters.isEmpty()) {
|
||||
where.removeIf(filter -> filter.getField().equals(Logs.Fields.NAMESPACE));
|
||||
namespaceFilters.forEach(f -> {
|
||||
where.add(f.toDashboardFilterBuilder(Logs.Fields.NAMESPACE, f.value()));
|
||||
});
|
||||
}
|
||||
|
||||
if (startDate != null || endDate != null) {
|
||||
where.removeIf(f -> f.getField().equals(Fields.DATE));
|
||||
if (startDate != null) {
|
||||
where.add(GreaterThanOrEqualTo.<Logs.Fields>builder().field(Fields.DATE).value(startDate.toInstant()).build());
|
||||
}
|
||||
if (endDate != null) {
|
||||
where.add(LessThanOrEqualTo.<Logs.Fields>builder().field(Fields.DATE).value(endDate.toInstant()).build());
|
||||
}
|
||||
}
|
||||
|
||||
this.setWhere(where);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Fields> aggregationForbiddenFields() {
|
||||
return Set.of(Fields.MESSAGE);
|
||||
}
|
||||
|
||||
public enum Fields {
|
||||
NAMESPACE,
|
||||
FLOW_ID,
|
||||
EXECUTION_ID,
|
||||
TASK_ID,
|
||||
DATE,
|
||||
TASK_RUN_ID,
|
||||
ATTEMPT_NUMBER,
|
||||
TRIGGER_ID,
|
||||
LEVEL,
|
||||
MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.repositories.LogRepositoryInterface;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
@EqualsAndHashCode
|
||||
@Schema(
|
||||
title = "Display Log data in a dashboard chart.",
|
||||
description = "Log data can be displayed in a chart with certain parameters such as Exectution date or Log level."
|
||||
)
|
||||
@Plugin(
|
||||
examples = {
|
||||
@Example(
|
||||
title = "Display a chart with a count of Logs per date grouped by level.",
|
||||
full = true,
|
||||
code = {
|
||||
"id: logs_timeseries\n" +
|
||||
"type: io.kestra.plugin.core.dashboard.chart.TimeSeries\n" +
|
||||
"chartOptions:\n" +
|
||||
"displayName: Logs\n" +
|
||||
"description: Logs count per date grouped by level\n" +
|
||||
"legend:\n" +
|
||||
"enabled: true\n" +
|
||||
"column: date\n" +
|
||||
"colorByColumn: level\n" +
|
||||
"data:\n" +
|
||||
"type: io.kestra.plugin.core.dashboard.data.Logs\n" +
|
||||
"columns:\n" +
|
||||
"date:\n" +
|
||||
"field: DATE\n" +
|
||||
"displayName: Execution Date\n" +
|
||||
"level:\n" +
|
||||
"field: LEVEL\n" +
|
||||
"total:\n" +
|
||||
"displayName: Total Executions\n" +
|
||||
"agg: COUNT\n" +
|
||||
"graphStyle: BARS\n"
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
public class LogsKPI<C extends ColumnDescriptor<LogsKPI.Fields>> extends DataFilterKPI<LogsKPI.Fields, C> implements ILogs {
|
||||
@Override
|
||||
public Class<? extends QueryBuilderInterface<Fields>> repositoryClass() {
|
||||
return LogRepositoryInterface.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Fields> aggregationForbiddenFields() {
|
||||
return Set.of(Fields.MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Metric;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.dashboards.filters.EqualTo;
|
||||
import io.kestra.core.models.executions.metrics.Counter;
|
||||
import io.kestra.core.repositories.MetricRepositoryInterface;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -18,10 +13,6 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@@ -60,41 +51,9 @@ import java.util.List;
|
||||
)
|
||||
}
|
||||
)
|
||||
public class Metrics<C extends ColumnDescriptor<Metrics.Fields>> extends DataFilter<Metrics.Fields, C> {
|
||||
public class Metrics<C extends ColumnDescriptor<Metrics.Fields>> extends DataFilter<Metrics.Fields, C> implements IMetrics {
|
||||
@Override
|
||||
public Class<? extends QueryBuilderInterface<Metrics.Fields>> repositoryClass() {
|
||||
return MetricRepositoryInterface.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGlobalFilter(List<QueryFilter> filters, ZonedDateTime startDate, ZonedDateTime endDate) {
|
||||
List<AbstractFilter<Fields>> where = this.getWhere() != null ? new ArrayList<>(this.getWhere()) : new ArrayList<>();
|
||||
|
||||
if (filters == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<QueryFilter> namespaceFilters = filters.stream().filter(f -> f.field().equals(QueryFilter.Field.NAMESPACE)).toList();
|
||||
if (!namespaceFilters.isEmpty()) {
|
||||
where.removeIf(filter -> filter.getField().equals(Metrics.Fields.NAMESPACE));
|
||||
namespaceFilters.forEach(f -> {
|
||||
where.add(EqualTo.<Metrics.Fields>builder().field(Metrics.Fields.NAMESPACE).value(f.value()).build());
|
||||
});
|
||||
}
|
||||
|
||||
this.setWhere(where);
|
||||
}
|
||||
|
||||
|
||||
public enum Fields {
|
||||
NAMESPACE,
|
||||
FLOW_ID,
|
||||
TASK_ID,
|
||||
EXECUTION_ID,
|
||||
TASK_RUN_ID,
|
||||
TYPE,
|
||||
NAME,
|
||||
VALUE,
|
||||
DATE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.repositories.MetricRepositoryInterface;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
@EqualsAndHashCode
|
||||
@Schema(
|
||||
title = "Metrics are data exposed by tasks after execution.",
|
||||
description = "A chart using Metrics could display the number of rows loaded in a bigQuery task or an output count from a SQL Query; anything exposed by an execution."
|
||||
)
|
||||
@Plugin(
|
||||
examples = {
|
||||
@Example(
|
||||
title = "Display a chart with rows inserted by Namespace.",
|
||||
full = true,
|
||||
code = {
|
||||
"id: table_metrics\n" +
|
||||
"type: io.kestra.plugin.core.dashboard.chart.Table\n" +
|
||||
"chartOptions:\n" +
|
||||
"displayName: Rows Inserted by Namespace\n" +
|
||||
"data:\n" +
|
||||
"type: io.kestra.plugin.core.dashboard.data.Metrics\n" +
|
||||
"columns:\n" +
|
||||
"namespace:\n" +
|
||||
"field: NAMESPACE\n" +
|
||||
"inserted_rows:\n" +
|
||||
"field: VALUE\n" +
|
||||
"agg: SUM\n" +
|
||||
"where:\n" +
|
||||
"- field: NAME\n" +
|
||||
"type: EQUAL_TO\n" +
|
||||
"value: rows\n" +
|
||||
"orderBy:\n" +
|
||||
"- column: inserted_rows\n" +
|
||||
"order: DESC\n"
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
public class MetricsKPI<C extends ColumnDescriptor<MetricsKPI.Fields>> extends DataFilterKPI<MetricsKPI.Fields, C> implements IMetrics {
|
||||
@Override
|
||||
public Class<? extends QueryBuilderInterface<Fields>> repositoryClass() {
|
||||
return MetricRepositoryInterface.class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.kestra.plugin.core.dashboard.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.kestra.core.repositories.TriggerRepositoryInterface;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
@EqualsAndHashCode
|
||||
//@TriggersDataFilterValidation
|
||||
@Schema(
|
||||
title = "Display Execution data in a dashboard chart.",
|
||||
description = "Execution data can be displayed in charts broken out by Namespace and filtered by State, for example."
|
||||
)
|
||||
@Plugin(
|
||||
examples = {
|
||||
@Example(
|
||||
title = "Display a chart with a Triggers per Namespace broken out by State.",
|
||||
full = true,
|
||||
code = {
|
||||
"id: executions_per_namespace_bars\n"
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
@JsonTypeName("Triggers")
|
||||
public class Triggers<C extends ColumnDescriptor<Triggers.Fields>> extends DataFilter<Triggers.Fields, C> implements ITriggers {
|
||||
@Override
|
||||
public Class<? extends QueryBuilderInterface<Triggers.Fields>> repositoryClass() {
|
||||
return TriggerRepositoryInterface.class;
|
||||
}
|
||||
}
|
||||
@@ -283,17 +283,17 @@ class JsonSchemaGeneratorTest {
|
||||
|
||||
var definitions = (Map<String, Map<String, Object>>) generate.get("definitions");
|
||||
|
||||
String executionTimeSeriesColumnDescriptorExecutionFieldsKey = "io.kestra.plugin.core.dashboard.data.Executions_io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.Executions-Fields__";
|
||||
String executionTimeSeriesColumnDescriptorExecutionFieldsKey = "io.kestra.plugin.core.dashboard.data.Executions_io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.IExecutions-Fields__";
|
||||
assertThat(
|
||||
properties(definitions.get("io.kestra.plugin.core.dashboard.chart.TimeSeries_io.kestra.plugin.core.dashboard.data.Executions-Fields.io.kestra.plugin.core.dashboard.data.Executions_io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.Executions-Fields___"))
|
||||
properties(definitions.get("io.kestra.plugin.core.dashboard.chart.TimeSeries_io.kestra.plugin.core.dashboard.data.IExecutions-Fields.io.kestra.plugin.core.dashboard.data.Executions_io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.IExecutions-Fields___"))
|
||||
.get("data")
|
||||
.get("$ref"),
|
||||
Matchers.is("#/definitions/" + executionTimeSeriesColumnDescriptorExecutionFieldsKey)
|
||||
);
|
||||
|
||||
String timeseriesColumnDescriptorExecutionFields = "io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.Executions-Fields_";
|
||||
String timeseriesColumnDescriptorExecutionFields = "io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.IExecutions-Fields_";
|
||||
assertThat(
|
||||
((Map<String, String>) properties(definitions.get("io.kestra.plugin.core.dashboard.data.Executions_io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.Executions-Fields__"))
|
||||
((Map<String, String>) properties(definitions.get("io.kestra.plugin.core.dashboard.data.Executions_io.kestra.plugin.core.dashboard.chart.timeseries.TimeSeriesColumnDescriptor_io.kestra.plugin.core.dashboard.data.IExecutions-Fields__"))
|
||||
.get("columns")
|
||||
.get("additionalProperties")
|
||||
).get("$ref"),
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
package io.kestra.repository.h2;
|
||||
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.utils.DateUtils;
|
||||
import io.kestra.jdbc.repository.AbstractJdbcTriggerRepository;
|
||||
import io.kestra.jdbc.services.JdbcFilterService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.jooq.Field;
|
||||
import org.jooq.impl.DSL;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Singleton
|
||||
@H2RepositoryEnabled
|
||||
public class H2TriggerRepository extends AbstractJdbcTriggerRepository {
|
||||
@Inject
|
||||
public H2TriggerRepository(@Named("triggers") H2Repository<Trigger> repository) {
|
||||
super(repository);
|
||||
public H2TriggerRepository(@Named("triggers") H2Repository<Trigger> repository,
|
||||
JdbcFilterService filterService) {
|
||||
super(repository, filterService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType) {
|
||||
switch (groupType) {
|
||||
case MONTH:
|
||||
return DSL.field("FORMATDATETIME(\"" + dateField + "\", 'yyyy-MM')", Date.class);
|
||||
case WEEK:
|
||||
return DSL.field("FORMATDATETIME(\"" + dateField + "\", 'YYYY-ww')", Date.class);
|
||||
case DAY:
|
||||
return DSL.field("FORMATDATETIME(\"" + dateField + "\", 'yyyy-MM-dd')", Date.class);
|
||||
case HOUR:
|
||||
return DSL.field("FORMATDATETIME(\"" + dateField + "\", 'yyyy-MM-dd HH:00:00')", Date.class);
|
||||
case MINUTE:
|
||||
return DSL.field("FORMATDATETIME(\"" + dateField + "\", 'yyyy-MM-dd HH:mm:00')", Date.class);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported GroupType: " + groupType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,48 @@
|
||||
package io.kestra.repository.mysql;
|
||||
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.utils.DateUtils;
|
||||
import io.kestra.jdbc.repository.AbstractJdbcTriggerRepository;
|
||||
import io.kestra.jdbc.services.JdbcFilterService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.jooq.Condition;
|
||||
import org.jooq.Field;
|
||||
import org.jooq.impl.DSL;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
@MysqlRepositoryEnabled
|
||||
public class MysqlTriggerRepository extends AbstractJdbcTriggerRepository {
|
||||
@Inject
|
||||
public MysqlTriggerRepository(@Named("triggers") MysqlRepository<Trigger> repository) {
|
||||
super(repository);
|
||||
public MysqlTriggerRepository(@Named("triggers") MysqlRepository<Trigger> repository,
|
||||
JdbcFilterService filterService) {
|
||||
super(repository, filterService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Condition fullTextCondition(String query) {
|
||||
return query == null ? DSL.trueCondition() : jdbcRepository.fullTextCondition(List.of("namespace", "flow_id", "trigger_id", "execution_id"), query);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType) {
|
||||
switch (groupType) {
|
||||
case MONTH:
|
||||
return DSL.field("DATE_FORMAT({0}, '%Y-%m')", Date.class, DSL.field(dateField));
|
||||
case WEEK:
|
||||
return DSL.field("DATE_FORMAT({0}, '%x-%v')", Date.class, DSL.field(dateField));
|
||||
case DAY:
|
||||
return DSL.field("DATE({0})", Date.class, DSL.field(dateField));
|
||||
case HOUR:
|
||||
return DSL.field("DATE_FORMAT({0}, '%Y-%m-%d %H:00:00')", Date.class, DSL.field(dateField));
|
||||
case MINUTE:
|
||||
return DSL.field("DATE_FORMAT({0}, '%Y-%m-%d %H:%i:00')", Date.class, DSL.field(dateField));
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported GroupType: " + groupType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
package io.kestra.repository.postgres;
|
||||
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.utils.DateUtils;
|
||||
import io.kestra.jdbc.repository.AbstractJdbcTriggerRepository;
|
||||
import io.kestra.jdbc.services.JdbcFilterService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.jooq.Field;
|
||||
import org.jooq.impl.DSL;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Singleton
|
||||
@PostgresRepositoryEnabled
|
||||
public class PostgresTriggerRepository extends AbstractJdbcTriggerRepository {
|
||||
@Inject
|
||||
public PostgresTriggerRepository(@Named("triggers") PostgresRepository<Trigger> repository) {
|
||||
super(repository);
|
||||
public PostgresTriggerRepository(@Named("triggers") PostgresRepository<Trigger> repository,
|
||||
JdbcFilterService filterService) {
|
||||
super(repository, filterService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType) {
|
||||
switch (groupType) {
|
||||
case MONTH:
|
||||
return DSL.field("TO_CHAR({0}, 'YYYY-MM')", Date.class, DSL.field(dateField));
|
||||
case WEEK:
|
||||
return DSL.field("TO_CHAR({0}, 'IYYY-IW')", Date.class, DSL.field(dateField));
|
||||
case DAY:
|
||||
return DSL.field("DATE({0})", Date.class, DSL.field(dateField));
|
||||
case HOUR:
|
||||
return DSL.field("TO_CHAR({0}, 'YYYY-MM-DD HH24:00:00')", Date.class, DSL.field(dateField));
|
||||
case MINUTE:
|
||||
return DSL.field("TO_CHAR({0}, 'YYYY-MM-DD HH24:MI:00')", Date.class, DSL.field(dateField));
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported GroupType: " + groupType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ import io.kestra.core.events.CrudEventType;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.Dashboard;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.DataChart;
|
||||
import io.kestra.core.models.dashboards.charts.DataChartKPI;
|
||||
import io.kestra.core.repositories.ArrayListTotal;
|
||||
import io.kestra.core.repositories.DashboardRepositoryInterface;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.kestra.plugin.core.dashboard.chart.kpis.KpiOption;
|
||||
import io.micronaut.context.event.ApplicationEventPublisher;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
@@ -25,6 +28,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static io.kestra.core.utils.MathUtils.roundDouble;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public abstract class AbstractJdbcDashboardRepository extends AbstractJdbcRepository implements DashboardRepositoryInterface {
|
||||
@@ -167,6 +172,31 @@ public abstract class AbstractJdbcDashboardRepository extends AbstractJdbcReposi
|
||||
return queryBuilder.fetchData(tenantId, dataChart.getData(), startDate, endDate, pageable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <F extends Enum<F>> List<Map<String, Object>> generateKPI(String tenantId, DataChartKPI<?, DataFilterKPI<F, ? extends ColumnDescriptor<F>>> dataChart, ZonedDateTime startDate, ZonedDateTime endDate) throws IOException {
|
||||
Map<Class<? extends QueryBuilderInterface<?>>, QueryBuilderInterface<?>> queryBuilderByHandledFields = new HashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
QueryBuilderInterface<F> queryBuilder = (QueryBuilderInterface<F>) queryBuilderByHandledFields.computeIfAbsent(
|
||||
dataChart.getData().repositoryClass(),
|
||||
clazz -> queryBuilders
|
||||
.stream()
|
||||
.filter(b -> clazz.isAssignableFrom(b.getClass()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new UnsupportedOperationException("No query builder found for " + clazz))
|
||||
);
|
||||
|
||||
Double filteredValue = queryBuilder.fetchValue(tenantId, dataChart.getData(), startDate, endDate, true);
|
||||
|
||||
if (dataChart.getChartOptions() != null && dataChart.getChartOptions().getNumberType().equals(KpiOption.NumberType.PERCENTAGE)) {
|
||||
Double totalValue = queryBuilder.fetchValue(tenantId, dataChart.getData(), startDate, endDate, false);
|
||||
Double percentageValue = (filteredValue / totalValue) * 100;
|
||||
return List.of(Map.of("value", roundDouble(percentageValue, 2)));
|
||||
}
|
||||
|
||||
return List.of(Map.of("value", roundDouble(filteredValue, 2)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isEnabled() {
|
||||
return true;
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.dashboards.filters.Contains;
|
||||
import io.kestra.core.models.dashboards.filters.In;
|
||||
@@ -41,7 +42,10 @@ import org.jooq.impl.DSL;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
@@ -1183,7 +1187,7 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
||||
);
|
||||
|
||||
// Apply Where filter
|
||||
selectConditionStep = where(selectConditionStep, filterService, descriptors, fieldsMapping);
|
||||
selectConditionStep = where(selectConditionStep, filterService, descriptors.getWhere(), fieldsMapping);
|
||||
|
||||
List<? extends ColumnDescriptor<Executions.Fields>> columnsWithoutDateWithOutAggs = columnsWithoutDate.values().stream()
|
||||
.filter(column -> column.getAgg() == null)
|
||||
@@ -1205,6 +1209,42 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
||||
});
|
||||
}
|
||||
|
||||
public Double fetchValue(String tenantId, DataFilterKPI<Executions.Fields, ? extends ColumnDescriptor<Executions.Fields>> dataFilter, ZonedDateTime startDate, ZonedDateTime endDate, boolean numeratorFilter) {
|
||||
return this.jdbcRepository.getDslContextWrapper().transactionResult(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
ColumnDescriptor<Executions.Fields> columnDescriptor = dataFilter.getColumns();
|
||||
String columnKey = this.getFieldsMapping().get(columnDescriptor.getField());
|
||||
Field<?> field = columnToField(columnDescriptor, getFieldsMapping());
|
||||
if (columnDescriptor.getAgg() != null) {
|
||||
field = filterService.buildAggregation(field, columnDescriptor.getAgg());
|
||||
}
|
||||
|
||||
List<AbstractFilter<Executions.Fields>> filters = new ArrayList<>(ListUtils.emptyOnNull(dataFilter.getWhere()));
|
||||
if (numeratorFilter) {
|
||||
filters.addAll(dataFilter.getNumerator());
|
||||
}
|
||||
|
||||
SelectConditionStep selectStep = context
|
||||
.select(field)
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(this.defaultFilter(tenantId));
|
||||
|
||||
var selectConditionStep = where(
|
||||
selectStep,
|
||||
filterService,
|
||||
filters,
|
||||
getFieldsMapping()
|
||||
);
|
||||
|
||||
Record result = selectConditionStep.fetchOne();
|
||||
if (result != null) {
|
||||
return result.getValue(field, Double.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <F extends Enum<F>> Field<?> columnToField(ColumnDescriptor<?> column, Map<F, String> fieldsMapping) {
|
||||
if (column.getField() == null) {
|
||||
@@ -1226,10 +1266,10 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <F extends Enum<F>> SelectConditionStep<Record> where(SelectConditionStep<Record> selectConditionStep, JdbcFilterService jdbcFilterService, DataFilter<F, ? extends ColumnDescriptor<F>> descriptors, Map<F, String> fieldsMapping) {
|
||||
if (!ListUtils.isEmpty(descriptors.getWhere())) {
|
||||
protected <F extends Enum<F>> SelectConditionStep<Record> where(SelectConditionStep<Record> selectConditionStep, JdbcFilterService jdbcFilterService, List<AbstractFilter<F>> filters, Map<F, String> fieldsMapping) {
|
||||
if (!ListUtils.isEmpty(filters)) {
|
||||
// Check if descriptors contain a filter of type Executions.Fields.STATE and apply the custom filter "statesFilter" if present
|
||||
List<In<Executions.Fields>> stateFilters = descriptors.getWhere().stream()
|
||||
List<In<Executions.Fields>> stateFilters = filters.stream()
|
||||
.filter(descriptor -> descriptor.getField().equals(Executions.Fields.STATE) && descriptor instanceof In)
|
||||
.map(descriptor -> (In<Executions.Fields>) descriptor)
|
||||
.toList();
|
||||
@@ -1244,7 +1284,7 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
||||
}
|
||||
|
||||
// Check if descriptors contain a filter of type EXECUTIONS.Fields.LABELS and apply the findCondition() method if present
|
||||
List<Contains<Executions.Fields>> labelFilters = descriptors.getWhere().stream()
|
||||
List<Contains<Executions.Fields>> labelFilters = filters.stream()
|
||||
.filter(descriptor -> descriptor.getField().equals(Executions.Fields.LABELS) && descriptor instanceof Contains<F>)
|
||||
.map(descriptor -> (Contains<Executions.Fields>) descriptor)
|
||||
.toList();
|
||||
@@ -1264,7 +1304,7 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
||||
}
|
||||
|
||||
// Remove the state filters from descriptors
|
||||
List<AbstractFilter<F>> remainingFilters = descriptors.getWhere().stream()
|
||||
List<AbstractFilter<F>> remainingFilters = filters.stream()
|
||||
.filter(descriptor -> !descriptor.getField().equals(Executions.Fields.STATE) || !(descriptor instanceof In)) // Filter state
|
||||
.filter(descriptor -> !descriptor.getField().equals(Executions.Fields.LABELS) || !(descriptor instanceof Contains<F>)) // Filter labels
|
||||
.toList();
|
||||
@@ -1279,21 +1319,4 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
||||
}
|
||||
|
||||
abstract protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType);
|
||||
|
||||
protected <F extends Enum<F>> List<Field<Date>> generateDateFields(
|
||||
DataFilter<F, ? extends ColumnDescriptor<F>> descriptors,
|
||||
Map<F, String> fieldsMapping,
|
||||
ZonedDateTime startDate,
|
||||
ZonedDateTime endDate,
|
||||
Set<F> dateFields
|
||||
) {
|
||||
return descriptors.getColumns().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getAgg() == null && dateFields.contains(entry.getValue().getField()))
|
||||
.map(entry -> {
|
||||
Duration duration = Duration.between(startDate, endDate);
|
||||
return formatDateField(fieldsMapping.get(entry.getValue().getField()), DateUtils.groupByType(duration)).as(entry.getKey());
|
||||
})
|
||||
.toList();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package io.kestra.jdbc.repository;
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.executions.statistics.LogStatistics;
|
||||
@@ -15,8 +17,8 @@ import io.kestra.plugin.core.dashboard.data.Logs;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.*;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.impl.DSL;
|
||||
import org.slf4j.event.Level;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -28,8 +30,8 @@ import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Comparator;
|
||||
import java.util.*;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -660,6 +662,53 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
||||
return field("level").in(levels.stream().map(level -> level.name()).toList());
|
||||
}
|
||||
|
||||
public Double fetchValue(String tenantId, DataFilterKPI<Logs.Fields, ? extends ColumnDescriptor<Logs.Fields>> dataFilter, ZonedDateTime startDate, ZonedDateTime endDate, boolean numeratorFilter) {
|
||||
return this.jdbcRepository.getDslContextWrapper().transactionResult(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
ColumnDescriptor<Logs.Fields> columnDescriptor = dataFilter.getColumns();
|
||||
String columnKey = this.getFieldsMapping().get(columnDescriptor.getField());
|
||||
Field<?> field = columnToField(columnDescriptor, getFieldsMapping());
|
||||
if (columnDescriptor.getAgg() != null) {
|
||||
field = filterService.buildAggregation(field, columnDescriptor.getAgg());
|
||||
}
|
||||
|
||||
List<AbstractFilter<Logs.Fields>> filters = new ArrayList<>(ListUtils.emptyOnNull(dataFilter.getWhere()));
|
||||
if (numeratorFilter) {
|
||||
filters.addAll(dataFilter.getNumerator());
|
||||
}
|
||||
|
||||
SelectConditionStep selectStep = context
|
||||
.select(field)
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(this.defaultFilter(tenantId));
|
||||
|
||||
var selectConditionStep = where(
|
||||
selectStep,
|
||||
filterService,
|
||||
filters,
|
||||
getFieldsMapping()
|
||||
);
|
||||
|
||||
Record result = selectConditionStep.fetchOne();
|
||||
if (result != null) {
|
||||
return result.getValue(field, Double.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Field<?> aggregate(String aggregation) {
|
||||
return switch (aggregation) {
|
||||
case "avg" -> DSL.avg(field("attempt_number", Double.class)).as("metric_value");
|
||||
case "sum" -> DSL.sum(field("attempt_number", Double.class)).as("metric_value");
|
||||
case "min" -> DSL.min(field("attempt_number", Double.class)).as("metric_value");
|
||||
case "max" -> DSL.max(field("attempt_number", Double.class)).as("metric_value");
|
||||
case "count" -> DSL.count().as("metric_value");
|
||||
default -> throw new IllegalArgumentException("Invalid aggregation: " + aggregation);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayListTotal<Map<String, Object>> fetchData(
|
||||
String tenantId,
|
||||
@@ -692,7 +741,7 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
||||
);
|
||||
|
||||
// Apply Where filter
|
||||
selectConditionStep = where(selectConditionStep, filterService, descriptors, getWhereMapping());
|
||||
selectConditionStep = where(selectConditionStep, filterService, descriptors.getWhere(), getWhereMapping());
|
||||
|
||||
List<? extends ColumnDescriptor<Logs.Fields>> columnsWithoutDateWithOutAggs = columnsWithoutDate.values().stream()
|
||||
.filter(column -> column.getAgg() == null)
|
||||
@@ -715,21 +764,4 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
||||
}
|
||||
|
||||
abstract protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType);
|
||||
|
||||
protected <F extends Enum<F>> List<Field<Date>> generateDateFields(
|
||||
DataFilter<F, ? extends ColumnDescriptor<F>> descriptors,
|
||||
Map<F, String> fieldsMapping,
|
||||
ZonedDateTime startDate,
|
||||
ZonedDateTime endDate,
|
||||
Set<F> dateFields
|
||||
) {
|
||||
return descriptors.getColumns().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getAgg() == null && dateFields.contains(entry.getValue().getField()))
|
||||
.map(entry -> {
|
||||
Duration duration = Duration.between(startDate, endDate);
|
||||
return formatDateField(fieldsMapping.get(entry.getValue().getField()), DateUtils.groupByType(duration)).as(entry.getKey());
|
||||
})
|
||||
.toList();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package io.kestra.jdbc.repository;
|
||||
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.MetricEntry;
|
||||
import io.kestra.core.models.executions.metrics.MetricAggregation;
|
||||
@@ -15,8 +17,8 @@ import io.kestra.plugin.core.dashboard.data.Metrics;
|
||||
import io.micrometer.common.lang.Nullable;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import lombok.Getter;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.*;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.impl.DSL;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
@@ -374,6 +376,43 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
|
||||
return mapper::get;
|
||||
}
|
||||
|
||||
public Double fetchValue(String tenantId, DataFilterKPI<Metrics.Fields, ? extends ColumnDescriptor<Metrics.Fields>> dataFilter, ZonedDateTime startDate, ZonedDateTime endDate, boolean numeratorFilter) {
|
||||
return this.jdbcRepository.getDslContextWrapper().transactionResult(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
ColumnDescriptor<Metrics.Fields> columnDescriptor = dataFilter.getColumns();
|
||||
String columnKey = this.getFieldsMapping().get(columnDescriptor.getField());
|
||||
Field<?> field = columnToField(columnDescriptor, getFieldsMapping());
|
||||
if (columnDescriptor.getAgg() != null) {
|
||||
field = filterService.buildAggregation(field, columnDescriptor.getAgg());
|
||||
}
|
||||
|
||||
List<AbstractFilter<Metrics.Fields>> filters = new ArrayList<>(ListUtils.emptyOnNull(dataFilter.getWhere()));
|
||||
if (numeratorFilter) {
|
||||
filters.addAll(dataFilter.getNumerator());
|
||||
}
|
||||
|
||||
SelectConditionStep selectStep = context
|
||||
.select(field)
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(this.defaultFilter(tenantId));
|
||||
|
||||
var selectConditionStep = where(
|
||||
selectStep,
|
||||
filterService,
|
||||
filters,
|
||||
getFieldsMapping()
|
||||
);
|
||||
|
||||
Record result = selectConditionStep.fetchOne();
|
||||
if (result != null) {
|
||||
return result.getValue(field, Double.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ArrayListTotal<Map<String, Object>> fetchData(
|
||||
String tenantId,
|
||||
@@ -406,7 +445,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
|
||||
);
|
||||
|
||||
// Apply Where filter
|
||||
selectConditionStep = where(selectConditionStep, filterService, descriptors, fieldsMapping);
|
||||
selectConditionStep = where(selectConditionStep, filterService, descriptors.getWhere(), fieldsMapping);
|
||||
|
||||
List<? extends ColumnDescriptor<Metrics.Fields>> columnsWithoutDateWithOutAggs = columnsWithoutDate.values().stream()
|
||||
.filter(column -> column.getAgg() == null)
|
||||
@@ -429,21 +468,4 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
|
||||
}
|
||||
|
||||
abstract protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType);
|
||||
|
||||
protected <F extends Enum<F>> List<Field<Date>> generateDateFields(
|
||||
DataFilter<F, ? extends ColumnDescriptor<F>> descriptors,
|
||||
Map<F, String> fieldsMapping,
|
||||
ZonedDateTime startDate,
|
||||
ZonedDateTime endDate,
|
||||
Set<F> dateFields
|
||||
) {
|
||||
return descriptors.getColumns().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getAgg() == null && dateFields.contains(entry.getValue().getField()))
|
||||
.map(entry -> {
|
||||
Duration duration = Duration.between(startDate, endDate);
|
||||
return formatDateField(fieldsMapping.get(entry.getValue().getField()), DateUtils.groupByType(duration)).as(entry.getKey());
|
||||
})
|
||||
.toList();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.Order;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.flows.FlowScope;
|
||||
import io.kestra.core.models.flows.State;
|
||||
@@ -16,8 +17,8 @@ import io.micronaut.context.annotation.Value;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import lombok.Getter;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.*;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.impl.DSL;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
@@ -25,12 +26,8 @@ import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static io.kestra.core.utils.NamespaceUtils.SYSTEM_FLOWS_DEFAULT_NAMESPACE;
|
||||
|
||||
@@ -160,8 +157,8 @@ public abstract class AbstractJdbcRepository {
|
||||
* @param <F> the type of the fields enum
|
||||
* @return the select condition step with the applied filters
|
||||
*/
|
||||
protected <F extends Enum<F>> SelectConditionStep<Record> where(SelectConditionStep<Record> selectConditionStep, JdbcFilterService jdbcFilterService, DataFilter<F, ? extends ColumnDescriptor<F>> descriptors, Map<F, String> fieldsMapping) {
|
||||
return jdbcFilterService.addFilters(selectConditionStep, fieldsMapping, descriptors.getWhere());
|
||||
protected <F extends Enum<F>> SelectConditionStep<Record> where(SelectConditionStep<Record> selectConditionStep, JdbcFilterService jdbcFilterService, List<AbstractFilter<F>> filters, Map<F, String> fieldsMapping) {
|
||||
return jdbcFilterService.addFilters(selectConditionStep, fieldsMapping, filters);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,4 +467,26 @@ public abstract class AbstractJdbcRepository {
|
||||
|
||||
return select;
|
||||
}
|
||||
|
||||
|
||||
protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType) {
|
||||
throw new UnsupportedOperationException("formatDateField() not implemented");
|
||||
}
|
||||
|
||||
protected <F extends Enum<F>> List<Field<Date>> generateDateFields(
|
||||
DataFilter<F, ? extends ColumnDescriptor<F>> descriptors,
|
||||
Map<F, String> fieldsMapping,
|
||||
ZonedDateTime startDate,
|
||||
ZonedDateTime endDate,
|
||||
Set<F> dateFields
|
||||
) {
|
||||
return descriptors.getColumns().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getAgg() == null && dateFields.contains(entry.getValue().getField()))
|
||||
.map(entry -> {
|
||||
Duration duration = Duration.between(startDate, endDate);
|
||||
return formatDateField(fieldsMapping.get(entry.getValue().getField()), DateUtils.groupByType(duration)).as(entry.getKey());
|
||||
})
|
||||
.toList();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package io.kestra.jdbc.repository;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.conditions.ConditionContext;
|
||||
import io.kestra.core.models.dashboards.ColumnDescriptor;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
@@ -10,28 +14,60 @@ import io.kestra.core.models.triggers.TriggerContext;
|
||||
import io.kestra.core.repositories.ArrayListTotal;
|
||||
import io.kestra.core.repositories.TriggerRepositoryInterface;
|
||||
import io.kestra.core.schedulers.ScheduleContextInterface;
|
||||
import io.kestra.core.utils.DateUtils;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.jdbc.runner.JdbcQueueIndexerInterface;
|
||||
import io.kestra.jdbc.runner.JdbcSchedulerContext;
|
||||
import io.kestra.jdbc.services.JdbcFilterService;
|
||||
import io.kestra.plugin.core.dashboard.data.ITriggers;
|
||||
import io.kestra.plugin.core.dashboard.data.Triggers;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import org.jooq.*;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.impl.DSL;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class AbstractJdbcTriggerRepository extends AbstractJdbcRepository implements TriggerRepositoryInterface, JdbcQueueIndexerInterface<Trigger> {
|
||||
public static final Field<Object> NAMESPACE_FIELD = field("namespace");
|
||||
|
||||
protected io.kestra.jdbc.AbstractJdbcRepository<Trigger> jdbcRepository;
|
||||
|
||||
public AbstractJdbcTriggerRepository(io.kestra.jdbc.AbstractJdbcRepository<Trigger> jdbcRepository) {
|
||||
private final JdbcFilterService filterService;
|
||||
|
||||
@Getter
|
||||
private final Map<Triggers.Fields, String> fieldsMapping = Map.of(
|
||||
Triggers.Fields.ID, "key",
|
||||
Triggers.Fields.NAMESPACE, "namespace",
|
||||
Triggers.Fields.FLOW_ID, "flow_id",
|
||||
Triggers.Fields.TRIGGER_ID, "trigger_id",
|
||||
Triggers.Fields.EXECUTION_ID, "execution_id",
|
||||
Triggers.Fields.NEXT_EXECUTION_DATE, "next_execution_date",
|
||||
Triggers.Fields.WORKER_ID, "worker_id"
|
||||
);
|
||||
|
||||
@Override
|
||||
public Set<Triggers.Fields> dateFields() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Triggers.Fields dateFilterField() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public AbstractJdbcTriggerRepository(io.kestra.jdbc.AbstractJdbcRepository<Trigger> jdbcRepository,
|
||||
JdbcFilterService filterService) {
|
||||
this.jdbcRepository = jdbcRepository;
|
||||
|
||||
this.filterService = filterService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -381,4 +417,99 @@ public abstract class AbstractJdbcTriggerRepository extends AbstractJdbcReposito
|
||||
|
||||
return s -> mapper.getOrDefault(s, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayListTotal<Map<String, Object>> fetchData(
|
||||
String tenantId,
|
||||
DataFilter<Triggers.Fields, ? extends ColumnDescriptor<Triggers.Fields>> descriptors,
|
||||
ZonedDateTime startDate,
|
||||
ZonedDateTime endDate,
|
||||
Pageable pageable
|
||||
) {
|
||||
return this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
.transactionResult(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
Map<String, ? extends ColumnDescriptor<Triggers.Fields>> columnsWithoutDate = descriptors.getColumns().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getField() == null || !dateFields().contains(entry.getValue().getField()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
// Generate custom fields for date as they probably need formatting
|
||||
List<Field<Date>> dateFields = generateDateFields(descriptors, fieldsMapping, startDate, endDate, dateFields());
|
||||
|
||||
// Init request
|
||||
SelectConditionStep<Record> selectConditionStep = select(
|
||||
context,
|
||||
filterService,
|
||||
columnsWithoutDate,
|
||||
dateFields,
|
||||
this.getFieldsMapping(),
|
||||
this.jdbcRepository.getTable(),
|
||||
tenantId
|
||||
);
|
||||
|
||||
// Apply Where filter
|
||||
selectConditionStep = where(selectConditionStep, filterService, descriptors.getWhere(), fieldsMapping);
|
||||
|
||||
List<? extends ColumnDescriptor<Triggers.Fields>> columnsWithoutDateWithOutAggs = columnsWithoutDate.values().stream()
|
||||
.filter(column -> column.getAgg() == null)
|
||||
.toList();
|
||||
|
||||
// Apply GroupBy for aggregation
|
||||
SelectHavingStep<Record> selectHavingStep = groupBy(
|
||||
selectConditionStep,
|
||||
columnsWithoutDateWithOutAggs,
|
||||
dateFields,
|
||||
fieldsMapping
|
||||
);
|
||||
|
||||
// Apply OrderBy
|
||||
SelectSeekStepN<Record> selectSeekStep = orderBy(selectHavingStep, descriptors);
|
||||
|
||||
// Fetch and paginate if provided
|
||||
return fetchSeekStep(selectSeekStep, pageable);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Double fetchValue(String tenantId, DataFilterKPI<ITriggers.Fields, ? extends ColumnDescriptor<ITriggers.Fields>> dataFilter, ZonedDateTime startDate, ZonedDateTime endDate, boolean numeratorFilter) {
|
||||
return this.jdbcRepository.getDslContextWrapper().transactionResult(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
ColumnDescriptor<ITriggers.Fields> columnDescriptor = dataFilter.getColumns();
|
||||
String columnKey = this.getFieldsMapping().get(columnDescriptor.getField());
|
||||
Field<?> field = columnToField(columnDescriptor, getFieldsMapping());
|
||||
if (columnDescriptor.getAgg() != null) {
|
||||
field = filterService.buildAggregation(field, columnDescriptor.getAgg());
|
||||
}
|
||||
|
||||
List<AbstractFilter<ITriggers.Fields>> filters = new ArrayList<>(ListUtils.emptyOnNull(dataFilter.getWhere()));
|
||||
if (numeratorFilter) {
|
||||
filters.addAll(dataFilter.getNumerator());
|
||||
}
|
||||
|
||||
SelectConditionStep selectStep = context
|
||||
.select(field)
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(this.defaultFilter(tenantId));
|
||||
|
||||
var selectConditionStep = where(
|
||||
selectStep,
|
||||
filterService,
|
||||
filters,
|
||||
getFieldsMapping()
|
||||
);
|
||||
|
||||
Record result = selectConditionStep.fetchOne();
|
||||
if (result != null) {
|
||||
return result.getValue(field, Double.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
abstract protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ const config: StorybookConfig = {
|
||||
addons: [
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-themes",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/experimental-addon-test"
|
||||
],
|
||||
framework: {
|
||||
|
||||
121
ui/package-lock.json
generated
121
ui/package-lock.json
generated
@@ -64,7 +64,6 @@
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.5",
|
||||
"@codecov/vite-plugin": "^1.9.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@eslint/js": "^9.27.0",
|
||||
@@ -959,27 +958,6 @@
|
||||
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@chromatic-com/storybook": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-3.2.6.tgz",
|
||||
"integrity": "sha512-FDmn5Ry2DzQdik+eq2sp/kJMMT36Ewe7ONXUXM2Izd97c7r6R/QyGli8eyh/F0iyqVvbLveNYFyF0dBOJNwLqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chromatic": "^11.15.0",
|
||||
"filesize": "^10.0.12",
|
||||
"jsonfile": "^6.1.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"yarn": ">=1.22.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codecov/bundler-plugin-core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.9.1.tgz",
|
||||
@@ -6225,9 +6203,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz",
|
||||
"integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==",
|
||||
"version": "19.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz",
|
||||
"integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -6905,30 +6883,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "2.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.13.tgz",
|
||||
"integrity": "sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==",
|
||||
"version": "2.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.14.tgz",
|
||||
"integrity": "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "2.4.13"
|
||||
"@volar/source-map": "2.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "2.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.13.tgz",
|
||||
"integrity": "sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==",
|
||||
"version": "2.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.14.tgz",
|
||||
"integrity": "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "2.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.13.tgz",
|
||||
"integrity": "sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==",
|
||||
"version": "2.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.14.tgz",
|
||||
"integrity": "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.13",
|
||||
"@volar/language-core": "2.4.14",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
}
|
||||
@@ -8502,30 +8480,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chromatic": {
|
||||
"version": "11.28.2",
|
||||
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.28.2.tgz",
|
||||
"integrity": "sha512-aCmUPcZUs4/p9zRZdMreOoO/5JqO2DiJC3md1/vRx8dlMRcmR/YI5ZbgXZcai2absVR+6hsXZ5XiPxV2sboTuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"chroma": "dist/bin.js",
|
||||
"chromatic": "dist/bin.js",
|
||||
"chromatic-cli": "dist/bin.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@chromatic-com/cypress": "^0.*.* || ^1.0.0",
|
||||
"@chromatic-com/playwright": "^0.*.* || ^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@chromatic-com/cypress": {
|
||||
"optional": true
|
||||
},
|
||||
"@chromatic-com/playwright": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
@@ -11113,16 +11067,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
"version": "10.1.6",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
|
||||
"integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 10.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -12742,6 +12686,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@@ -14367,13 +14318,6 @@
|
||||
"promise": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jstransformer/node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jump.js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jump.js/-/jump.js-1.0.2.tgz",
|
||||
@@ -18163,22 +18107,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-confetti": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz",
|
||||
"integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tween-functions": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
@@ -20320,13 +20248,6 @@
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tween-functions": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
|
||||
"integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==",
|
||||
"dev": true,
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.5",
|
||||
"@codecov/vite-plugin": "^1.9.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@eslint/js": "^9.27.0",
|
||||
|
||||
205
ui/src/assets/dashboard/default_flow_definition.yaml
Normal file
205
ui/src/assets/dashboard/default_flow_definition.yaml
Normal file
@@ -0,0 +1,205 @@
|
||||
title: Overview
|
||||
description: Default overview dashboard
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
charts:
|
||||
- id: kpi_success_ratio
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: Success Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- SUCCESS
|
||||
- id: kpi_failed_ratio
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: Failed Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- FAILED
|
||||
- id: kpi_in_progress_ration
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: In progress Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- RUNNING
|
||||
- PAUSED
|
||||
- KILLING
|
||||
- RETRYING
|
||||
- RESTARTED
|
||||
- id: kpi_pending_ration
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: In Pending Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- CREATED
|
||||
- QUEUED
|
||||
- id: description
|
||||
type: io.kestra.plugin.core.dashboard.chart.Markdown
|
||||
chartOptions:
|
||||
displayName: Description
|
||||
width: 12
|
||||
source:
|
||||
type: FlowDescription
|
||||
flowId: {{flowId}}
|
||||
namespace: {{namespace}}
|
||||
- id: total_executions_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
chartOptions:
|
||||
width: 9
|
||||
displayName: Total Executions
|
||||
description: Executions duration and count per date
|
||||
legend:
|
||||
enabled: true
|
||||
column: date
|
||||
colorByColumn: state
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
date:
|
||||
field: START_DATE
|
||||
displayName: Date
|
||||
state:
|
||||
field: STATE
|
||||
total:
|
||||
displayName: Executions
|
||||
agg: COUNT
|
||||
graphStyle: BARS
|
||||
duration:
|
||||
displayName: Duration
|
||||
field: DURATION
|
||||
agg: SUM
|
||||
graphStyle: LINES
|
||||
- id: total_executions_pie
|
||||
type: io.kestra.plugin.core.dashboard.chart.Pie
|
||||
chartOptions:
|
||||
displayName: Total Executions
|
||||
graphStyle: DONUT
|
||||
tooltip: ALL
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
id:
|
||||
field: ID
|
||||
displayName: Execution Id
|
||||
agg: COUNT
|
||||
state:
|
||||
displayName: State
|
||||
field: STATE
|
||||
- id: executions_in_progress
|
||||
type: io.kestra.plugin.core.dashboard.chart.Table
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
id:
|
||||
field: ID
|
||||
displayName: Execution Id
|
||||
namespace:
|
||||
field: NAMESPACE
|
||||
displayName: Namespace
|
||||
flow:
|
||||
field: FLOW_ID
|
||||
displayName: Flow
|
||||
duration:
|
||||
field: DURATION
|
||||
displayName: Duration
|
||||
state:
|
||||
displayName: State
|
||||
field: STATE
|
||||
- id: next_executions
|
||||
chartOptions:
|
||||
displayName: Next Executions
|
||||
width: 6
|
||||
type: io.kestra.plugin.core.dashboard.chart.Table
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Triggers
|
||||
columns:
|
||||
flowId:
|
||||
field: FLOW_ID
|
||||
displayName: FlowId
|
||||
nextExec:
|
||||
field: NEXT_EXECUTION_DATE
|
||||
displayName: nextExec
|
||||
- id: executions_per_namespace_bars
|
||||
type: io.kestra.plugin.core.dashboard.chart.Bar
|
||||
chartOptions:
|
||||
displayName: Executions (per namespace)
|
||||
description: Executions count per namespace
|
||||
legend:
|
||||
enabled: true
|
||||
column: namespace
|
||||
width: 12
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
namespace:
|
||||
field: NAMESPACE
|
||||
state:
|
||||
field: STATE
|
||||
total:
|
||||
displayName: Executions
|
||||
agg: COUNT
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
chartOptions:
|
||||
displayName: Logs
|
||||
description: Logs count per date grouped by level
|
||||
legend:
|
||||
enabled: true
|
||||
column: date
|
||||
colorByColumn: level
|
||||
width: 12
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Logs
|
||||
columns:
|
||||
date:
|
||||
field: DATE
|
||||
displayName: Execution Date
|
||||
level:
|
||||
field: LEVEL
|
||||
total:
|
||||
displayName: Total Executions
|
||||
agg: COUNT
|
||||
graphStyle: BARS
|
||||
|
||||
197
ui/src/assets/dashboard/default_main_definition.yaml
Normal file
197
ui/src/assets/dashboard/default_main_definition.yaml
Normal file
@@ -0,0 +1,197 @@
|
||||
title: Overview
|
||||
description: Default overview dashboard
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
charts:
|
||||
- id: kpi_success_ratio
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: Success Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- SUCCESS
|
||||
- id: kpi_failed_ratio
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: Failed Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- FAILED
|
||||
- id: kpi_in_progress_ration
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: In progress Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- RUNNING
|
||||
- PAUSED
|
||||
- KILLING
|
||||
- RETRYING
|
||||
- RESTARTED
|
||||
- id: kpi_pending_ration
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: In Pending Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- CREATED
|
||||
- QUEUED
|
||||
- id: total_executions_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
chartOptions:
|
||||
width: 9
|
||||
displayName: Total Executions
|
||||
description: Executions duration and count per date
|
||||
legend:
|
||||
enabled: true
|
||||
column: date
|
||||
colorByColumn: state
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
date:
|
||||
field: START_DATE
|
||||
displayName: Date
|
||||
state:
|
||||
field: STATE
|
||||
total:
|
||||
displayName: Executions
|
||||
agg: COUNT
|
||||
graphStyle: BARS
|
||||
duration:
|
||||
displayName: Duration
|
||||
field: DURATION
|
||||
agg: SUM
|
||||
graphStyle: LINES
|
||||
- id: total_executions_pie
|
||||
type: io.kestra.plugin.core.dashboard.chart.Pie
|
||||
chartOptions:
|
||||
displayName: Total Executions
|
||||
graphStyle: DONUT
|
||||
tooltip: ALL
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
id:
|
||||
field: ID
|
||||
displayName: Execution Id
|
||||
agg: COUNT
|
||||
state:
|
||||
displayName: State
|
||||
field: STATE
|
||||
- id: executions_in_progress
|
||||
type: io.kestra.plugin.core.dashboard.chart.Table
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
id:
|
||||
field: ID
|
||||
displayName: Execution Id
|
||||
namespace:
|
||||
field: NAMESPACE
|
||||
displayName: Namespace
|
||||
flow:
|
||||
field: FLOW_ID
|
||||
displayName: Flow
|
||||
duration:
|
||||
field: DURATION
|
||||
displayName: Duration
|
||||
state:
|
||||
displayName: State
|
||||
field: STATE
|
||||
- id: next_executions
|
||||
chartOptions:
|
||||
displayName: Next Executions
|
||||
width: 6
|
||||
type: io.kestra.plugin.core.dashboard.chart.Table
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Triggers
|
||||
columns:
|
||||
flowId:
|
||||
field: FLOW_ID
|
||||
displayName: FlowId
|
||||
nextExec:
|
||||
field: NEXT_EXECUTION_DATE
|
||||
displayName: nextExec
|
||||
- id: executions_per_namespace_bars
|
||||
type: io.kestra.plugin.core.dashboard.chart.Bar
|
||||
chartOptions:
|
||||
displayName: Executions (per namespace)
|
||||
description: Executions count per namespace
|
||||
legend:
|
||||
enabled: true
|
||||
column: namespace
|
||||
width: 12
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
namespace:
|
||||
field: NAMESPACE
|
||||
state:
|
||||
field: STATE
|
||||
total:
|
||||
displayName: Executions
|
||||
agg: COUNT
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
chartOptions:
|
||||
displayName: Logs
|
||||
description: Logs count per date grouped by level
|
||||
legend:
|
||||
enabled: true
|
||||
column: date
|
||||
colorByColumn: level
|
||||
width: 12
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Logs
|
||||
columns:
|
||||
date:
|
||||
field: DATE
|
||||
displayName: Execution Date
|
||||
level:
|
||||
field: LEVEL
|
||||
total:
|
||||
displayName: Total Executions
|
||||
agg: COUNT
|
||||
graphStyle: BARS
|
||||
|
||||
|
||||
196
ui/src/assets/dashboard/default_namespace_definition.yaml
Normal file
196
ui/src/assets/dashboard/default_namespace_definition.yaml
Normal file
@@ -0,0 +1,196 @@
|
||||
title: Overview
|
||||
description: Default overview dashboard
|
||||
timeWindow:
|
||||
default: P30D # P30DT30H
|
||||
max: P365D
|
||||
|
||||
charts:
|
||||
- id: kpi_success_ratio
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: Success Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- SUCCESS
|
||||
- id: kpi_failed_ratio
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: Failed Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- FAILED
|
||||
- id: kpi_in_progress_ration
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: In progress Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- RUNNING
|
||||
- PAUSED
|
||||
- KILLING
|
||||
- RETRYING
|
||||
- RESTARTED
|
||||
- id: kpi_pending_ration
|
||||
type: io.kestra.plugin.core.dashboard.chart.KPI
|
||||
chartOptions:
|
||||
displayName: In Pending Ratio
|
||||
numberType: PERCENTAGE
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.ExecutionsKPI
|
||||
columns:
|
||||
field: ID
|
||||
agg: COUNT
|
||||
numerator:
|
||||
- type: IN
|
||||
field: STATE
|
||||
values:
|
||||
- CREATED
|
||||
- QUEUED
|
||||
- id: total_executions_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
chartOptions:
|
||||
width: 9
|
||||
displayName: Total Executions
|
||||
description: Executions duration and count per date
|
||||
legend:
|
||||
enabled: true
|
||||
column: date
|
||||
colorByColumn: state
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
date:
|
||||
field: START_DATE
|
||||
displayName: Date
|
||||
state:
|
||||
field: STATE
|
||||
total:
|
||||
displayName: Executions
|
||||
agg: COUNT
|
||||
graphStyle: BARS
|
||||
duration:
|
||||
displayName: Duration
|
||||
field: DURATION
|
||||
agg: SUM
|
||||
graphStyle: LINES
|
||||
- id: total_executions_pie
|
||||
type: io.kestra.plugin.core.dashboard.chart.Pie
|
||||
chartOptions:
|
||||
displayName: Total Executions
|
||||
graphStyle: DONUT
|
||||
tooltip: ALL
|
||||
width: 3
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
id:
|
||||
field: ID
|
||||
displayName: Execution Id
|
||||
agg: COUNT
|
||||
state:
|
||||
displayName: State
|
||||
field: STATE
|
||||
- id: executions_in_progress
|
||||
type: io.kestra.plugin.core.dashboard.chart.Table
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
id:
|
||||
field: ID
|
||||
displayName: Execution Id
|
||||
namespace:
|
||||
field: NAMESPACE
|
||||
displayName: Namespace
|
||||
flow:
|
||||
field: FLOW_ID
|
||||
displayName: Flow
|
||||
duration:
|
||||
field: DURATION
|
||||
displayName: Duration
|
||||
state:
|
||||
displayName: State
|
||||
field: STATE
|
||||
- id: next_executions
|
||||
chartOptions:
|
||||
displayName: Next Executions
|
||||
width: 6
|
||||
type: io.kestra.plugin.core.dashboard.chart.Table
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Triggers
|
||||
columns:
|
||||
flowId:
|
||||
field: FLOW_ID
|
||||
displayName: FlowId
|
||||
nextExec:
|
||||
field: NEXT_EXECUTION_DATE
|
||||
displayName: nextExec
|
||||
- id: executions_per_namespace_bars
|
||||
type: io.kestra.plugin.core.dashboard.chart.Bar
|
||||
chartOptions:
|
||||
displayName: Executions (per namespace)
|
||||
description: Executions count per namespace
|
||||
legend:
|
||||
enabled: true
|
||||
column: namespace
|
||||
width: 12
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Executions
|
||||
columns:
|
||||
namespace:
|
||||
field: NAMESPACE
|
||||
state:
|
||||
field: STATE
|
||||
total:
|
||||
displayName: Executions
|
||||
agg: COUNT
|
||||
- id: logs_timeseries
|
||||
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
|
||||
chartOptions:
|
||||
displayName: Logs
|
||||
description: Logs count per date grouped by level
|
||||
legend:
|
||||
enabled: true
|
||||
column: date
|
||||
colorByColumn: level
|
||||
width: 12
|
||||
data:
|
||||
type: io.kestra.plugin.core.dashboard.data.Logs
|
||||
columns:
|
||||
date:
|
||||
field: DATE
|
||||
displayName: Execution Date
|
||||
level:
|
||||
field: LEVEL
|
||||
total:
|
||||
displayName: Total Executions
|
||||
agg: COUNT
|
||||
graphStyle: BARS
|
||||
|
||||
@@ -1,646 +1,115 @@
|
||||
<template>
|
||||
<Header
|
||||
v-if="!embed"
|
||||
:title="custom.shown ? custom.dashboard.title : t('overview')"
|
||||
:breadcrumb="[
|
||||
{
|
||||
label: t(custom.shown ? 'custom_dashboard' : 'dashboard_label'),
|
||||
link: {},
|
||||
},
|
||||
]"
|
||||
:id="custom.dashboard.id ?? undefined"
|
||||
/>
|
||||
<section id="header" v-if="!embed">
|
||||
<Header
|
||||
:title="dashboard.title ?? t('overview')"
|
||||
:breadcrumb="[{label: t('dashboard_label'), link: {}}]"
|
||||
:id="dashboard.id"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-filters">
|
||||
<!-- Force re-rendering when switching between custom and default -->
|
||||
<section id="filter">
|
||||
<KestraFilter
|
||||
:key="custom.shown"
|
||||
:prefix="custom.shown ? 'custom_dashboard' : 'dashboard'"
|
||||
:include="
|
||||
custom.shown
|
||||
? ['relative_date', 'absolute_date', 'namespace', 'labels']
|
||||
: [
|
||||
'namespace',
|
||||
'state',
|
||||
'scope',
|
||||
'absolute_date',
|
||||
]
|
||||
"
|
||||
:prefix="dashboard.id"
|
||||
:include="['relative_date', 'absolute_date', 'namespace', 'labels']"
|
||||
:buttons="{
|
||||
refresh: {
|
||||
shown: true,
|
||||
callback: custom.shown ? refreshCustom : fetchAll,
|
||||
},
|
||||
refresh: {shown: true, callback: () => load()},
|
||||
settings: {shown: false},
|
||||
}"
|
||||
:dashboards="{shown: customDashboardsEnabled && route.name === 'home'}"
|
||||
@dashboard="(v) => handleCustomUpdate(v)"
|
||||
:is-default-dashboard="!custom.shown"
|
||||
:dashboards="{shown: route.name === 'home'}"
|
||||
@dashboard="(value) => load(value)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="custom.shown">
|
||||
<p v-if="custom.dashboard.description" class="description">
|
||||
<small>{{ custom.dashboard.description }}</small>
|
||||
</p>
|
||||
<el-row class="custom">
|
||||
<el-col
|
||||
v-for="(chart, index) in custom.dashboard.charts"
|
||||
:key="index + JSON.stringify(route.query)"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
>
|
||||
<div class="p-4 d-flex flex-column">
|
||||
<p class="m-0 fs-6 fw-bold">
|
||||
{{ chart.chartOptions?.displayName ?? chart.id }}
|
||||
</p>
|
||||
<p
|
||||
v-if="chart.chartOptions?.description"
|
||||
class="m-0 fw-light"
|
||||
>
|
||||
<small>{{ chart.chartOptions.description }}</small>
|
||||
</p>
|
||||
<section id="description" v-if="dashboard.description">
|
||||
<small>{{ dashboard.description }}</small>
|
||||
</section>
|
||||
|
||||
<div class="mt-4 flex-grow-1">
|
||||
<component
|
||||
:is="types[chart.type]"
|
||||
:source="chart.content"
|
||||
:chart
|
||||
:identifier="custom.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-else class="dashboard">
|
||||
<Card
|
||||
:icon="CheckBold"
|
||||
:label="t('dashboard.success_ratio')"
|
||||
:tooltip="t('dashboard.success_ratio_tooltip')"
|
||||
:value="stats.success"
|
||||
:loading="executionsLoading"
|
||||
:redirect="{
|
||||
name: 'executions/list',
|
||||
query: {
|
||||
state: State.SUCCESS,
|
||||
scope: 'USER',
|
||||
size: 100,
|
||||
page: 1,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
|
||||
<Card
|
||||
:icon="Alert"
|
||||
:label="t('dashboard.failure_ratio')"
|
||||
:tooltip="t('dashboard.failure_ratio_tooltip')"
|
||||
:value="stats.failed"
|
||||
:loading="executionsLoading"
|
||||
:redirect="{
|
||||
name: 'executions/list',
|
||||
query: {
|
||||
state: State.FAILED,
|
||||
scope: 'USER',
|
||||
size: 100,
|
||||
page: 1,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
|
||||
<Card
|
||||
:icon="FileTree"
|
||||
:label="t('flows')"
|
||||
:value="numbers.flows"
|
||||
:loading="numbersLoading"
|
||||
:redirect="{
|
||||
name: 'flows/list',
|
||||
query: {scope: 'USER', size: 100, page: 1},
|
||||
}"
|
||||
/>
|
||||
|
||||
<Card
|
||||
:icon="LightningBolt"
|
||||
:label="t('triggers')"
|
||||
:value="numbers.triggers"
|
||||
:loading="numbersLoading"
|
||||
:redirect="{
|
||||
name: 'admin/triggers',
|
||||
query: {size: 100, page: 1},
|
||||
}"
|
||||
/>
|
||||
|
||||
<ExecutionsBar
|
||||
:data="graphData"
|
||||
:total="stats.total"
|
||||
:loading="executionsLoading"
|
||||
class="card card-2/3"
|
||||
/>
|
||||
|
||||
<ExecutionsDoughnut
|
||||
:data="graphData"
|
||||
:total="stats.total"
|
||||
:loading="executionsLoading"
|
||||
class="card card-1/3"
|
||||
/>
|
||||
|
||||
<div v-if="props.flow" class="h-100 p-4 card card-1/2">
|
||||
<span class="d-flex justify-content-between">
|
||||
<span class="fs-6 fw-bold">
|
||||
{{ t("dashboard.description") }}
|
||||
</span>
|
||||
<el-button
|
||||
:icon="BookOpenOutline"
|
||||
@click="descriptionDialog = true"
|
||||
>
|
||||
{{ t("open") }}
|
||||
</el-button>
|
||||
|
||||
<el-dialog
|
||||
v-model="descriptionDialog"
|
||||
:title="$t('description')"
|
||||
>
|
||||
<Markdown
|
||||
:source="description"
|
||||
:html="false"
|
||||
class="p-4 description"
|
||||
/>
|
||||
</el-dialog>
|
||||
</span>
|
||||
|
||||
<Markdown :source="description" :html="false" class="p-4 description" />
|
||||
</div>
|
||||
<ExecutionsInProgress
|
||||
v-else
|
||||
:flow="props.flowId"
|
||||
:namespace="props.namespace"
|
||||
:loading="executionsLoading"
|
||||
class="card card-1/2"
|
||||
/>
|
||||
|
||||
<ExecutionsNextScheduled
|
||||
v-if="props.flow"
|
||||
:flow="props.flowId"
|
||||
:namespace="props.namespace"
|
||||
:loading="executionsLoading"
|
||||
class="card card-1/2"
|
||||
/>
|
||||
<ExecutionsNextScheduled
|
||||
v-else-if="isAllowedTriggers"
|
||||
:flow="props.flowId"
|
||||
:namespace="props.namespace"
|
||||
:loading="executionsLoading"
|
||||
class="card card-1/2"
|
||||
/>
|
||||
|
||||
<ExecutionsEmptyNextScheduled
|
||||
v-else
|
||||
:loading="executionsLoading"
|
||||
class="card card-1/2"
|
||||
/>
|
||||
<ExecutionsNamespace
|
||||
v-if="!props.flow && Object.keys(namespaceExecutions).length > 1"
|
||||
class="card card-1"
|
||||
:data="namespaceExecutions"
|
||||
:total="stats.total"
|
||||
/>
|
||||
<Logs
|
||||
v-if="!props.flow"
|
||||
:data="logs"
|
||||
:loading="executionsLoading"
|
||||
class="card card-1"
|
||||
/>
|
||||
</div>
|
||||
<ChartsSection :charts :show-default="dashboard.id === 'default'" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onBeforeMount, ref, watch} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {useStore} from "vuex";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import {State} from "@kestra-io/ui-libs"
|
||||
import {onBeforeMount, ref} from "vue";
|
||||
|
||||
import Header from "./components/Header.vue";
|
||||
import Card from "./components/Card.vue";
|
||||
|
||||
import KestraFilter from "../filter/KestraFilter.vue";
|
||||
import ChartsSection from "./components/ChartsSection.vue";
|
||||
|
||||
import ExecutionsBar from "./components/charts/executions/Bar.vue";
|
||||
import ExecutionsDoughnut from "./components/charts/executions/Doughnut.vue";
|
||||
import ExecutionsNamespace from "./components/charts/executions/Namespace.vue";
|
||||
import Logs from "./components/charts/logs/Bar.vue";
|
||||
|
||||
import ExecutionsInProgress from "./components/tables/executions/InProgress.vue";
|
||||
import ExecutionsNextScheduled from "./components/tables/executions/NextScheduled.vue";
|
||||
import ExecutionsEmptyNextScheduled from "./components/tables/executions/EmptyNextScheduled.vue";
|
||||
|
||||
import Markdown from "../layout/Markdown.vue";
|
||||
import TimeSeries from "./components/charts/custom/TimeSeries.vue";
|
||||
import Bar from "./components/charts/custom/Bar.vue";
|
||||
import Pie from "./components/charts/custom/Pie.vue";
|
||||
import Table from "./components/tables/custom/Table.vue";
|
||||
|
||||
import CheckBold from "vue-material-design-icons/CheckBold.vue";
|
||||
import Alert from "vue-material-design-icons/Alert.vue";
|
||||
import LightningBolt from "vue-material-design-icons/LightningBolt.vue";
|
||||
import FileTree from "vue-material-design-icons/FileTree.vue";
|
||||
import BookOpenOutline from "vue-material-design-icons/BookOpenOutline.vue";
|
||||
import permission from "../../models/permission";
|
||||
import action from "../../models/action";
|
||||
import _cloneDeep from "lodash/cloneDeep.js";
|
||||
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
import {useStore} from "vuex";
|
||||
const store = useStore();
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
const user = store.getters["auth/user"];
|
||||
|
||||
const props = defineProps({
|
||||
embed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
flow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
flowId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
namespace: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
restoreURL: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
embed: {type: Boolean, default: false},
|
||||
isFlow: {type: Boolean, default: false},
|
||||
isNamespace: {type: Boolean, default: false},
|
||||
});
|
||||
|
||||
const customDashboardsEnabled = computed(
|
||||
() => store.state.misc?.configs?.isCustomDashboardsEnabled,
|
||||
);
|
||||
import yaml from "yaml";
|
||||
import {YamlUtils as YAML_UTILS} from "@kestra-io/ui-libs";
|
||||
|
||||
// Custom Dashboards
|
||||
const custom = ref({id: Math.random(), shown: false, dashboard: {}});
|
||||
const handleCustomUpdate = async (v) => {
|
||||
let dashboard = {};
|
||||
import YAML_MAIN from "../../assets/dashboard/default_main_definition.yaml?raw";
|
||||
import YAML_FLOW from "../../assets/dashboard/default_flow_definition.yaml?raw";
|
||||
import YAML_NAMESPACE from "../../assets/dashboard/default_namespace_definition.yaml?raw";
|
||||
|
||||
if (route.name === "home") {
|
||||
const initial = (dashboard) => ({id: "default", ...YAML_UTILS.parse(dashboard)});
|
||||
|
||||
const dashboard = ref({});
|
||||
const charts = ref([]);
|
||||
|
||||
const loadCharts = async (allCharts) => {
|
||||
charts.value = [];
|
||||
|
||||
for (const chart of allCharts) {
|
||||
charts.value.push({...chart, content: yaml.stringify(chart), raw: chart});
|
||||
}
|
||||
};
|
||||
|
||||
const load = async (id = "default", defaultYAML = YAML_MAIN) => {
|
||||
if (!["home", "flows/update", "namespaces/update"].includes(route.name)) return;
|
||||
|
||||
if(!props.isFlow && !props.isNamespace) {
|
||||
router.replace({
|
||||
params: {...route.params, id: v?.id ?? "default"},
|
||||
query: route.params.id != v?.id ? {} : {...route.query},
|
||||
params: {...route.params, id},
|
||||
query: route.params.id !== id ? {} : {...route.query},
|
||||
});
|
||||
if (v && v.id !== "default") {
|
||||
dashboard = await store.dispatch("dashboard/load", v.id);
|
||||
}
|
||||
|
||||
custom.value = {
|
||||
id: Math.random(),
|
||||
shown: !v || v.id === "default" ? false : true,
|
||||
dashboard,
|
||||
};
|
||||
}
|
||||
};
|
||||
const refreshCustom = async () => {
|
||||
const ID = custom.value.dashboard.id;
|
||||
let dashboard = await store.dispatch("dashboard/load", ID);
|
||||
custom.value = {id: Math.random(), shown: true, dashboard};
|
||||
};
|
||||
const types = {
|
||||
"io.kestra.plugin.core.dashboard.chart.TimeSeries": TimeSeries,
|
||||
"io.kestra.plugin.core.dashboard.chart.Bar": Bar,
|
||||
"io.kestra.plugin.core.dashboard.chart.Markdown": Markdown,
|
||||
"io.kestra.plugin.core.dashboard.chart.Table": Table,
|
||||
"io.kestra.plugin.core.dashboard.chart.Pie": Pie,
|
||||
};
|
||||
|
||||
const descriptionDialog = ref(false);
|
||||
const description = props.flow
|
||||
? (store.state?.flow?.flow?.description ??
|
||||
t("dashboard.no_flow_description"))
|
||||
: undefined;
|
||||
|
||||
const defaultNumbers = {flows: 0, triggers: 0};
|
||||
const numbers = ref({...defaultNumbers});
|
||||
const numbersLoading = ref(false);
|
||||
const fetchNumbers = () => {
|
||||
if (props.flowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
numbersLoading.value = true;
|
||||
store.$http
|
||||
.post(`${apiUrl(store)}/stats/summary`, mergeQuery())
|
||||
.then((response) => {
|
||||
if (!response.data) return;
|
||||
numbers.value = {...defaultNumbers, ...response.data};
|
||||
})
|
||||
.finally(() => {
|
||||
numbersLoading.value = false;
|
||||
});
|
||||
dashboard.value = id === "default" ? initial(defaultYAML) : await store.dispatch("dashboard/load", id);
|
||||
|
||||
loadCharts(dashboard.value.charts);
|
||||
};
|
||||
|
||||
const executions = ref({raw: {}, all: {}, yesterday: {}, today: {}});
|
||||
const stats = computed(() => {
|
||||
const counts = executions?.value?.all?.executionCounts || {};
|
||||
const terminatedStates = State.getTerminatedStates();
|
||||
const statesToCount = Object.fromEntries(
|
||||
Object.entries(counts).filter(([key]) =>
|
||||
terminatedStates.includes(key),
|
||||
),
|
||||
);
|
||||
const templateYamlFlow = () => {
|
||||
let yamlFlow = YAML_FLOW;
|
||||
yamlFlow = yamlFlow.replace(/{{namespace}}/g, route.params.namespace);
|
||||
yamlFlow = yamlFlow.replace(/{{flowId}}/g, route.params.id);
|
||||
|
||||
const total = Object.values(counts).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0,
|
||||
);
|
||||
|
||||
const totalTerminated = Object.values(statesToCount).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0,
|
||||
);
|
||||
|
||||
const successStates = ["SUCCESS", "CANCELLED", "WARNING", "SKIPPED"];
|
||||
const failedStates = ["FAILED", "KILLED"];
|
||||
const sumStates = (states) =>
|
||||
states.reduce((sum, state) => sum + (statesToCount[state] || 0), 0);
|
||||
const successRatio =
|
||||
totalTerminated > 0 ? (sumStates(successStates) / totalTerminated) * 100 : 0;
|
||||
const failedRatio = totalTerminated > 0 ? (sumStates(failedStates) / totalTerminated) * 100 : 0;
|
||||
|
||||
return {
|
||||
total: total,
|
||||
totalTerminated: totalTerminated,
|
||||
success: `${successRatio.toFixed(2)}%`,
|
||||
failed: `${failedRatio.toFixed(2)}%`,
|
||||
};
|
||||
});
|
||||
const transformer = (data) => {
|
||||
return data.reduce((accumulator, value) => {
|
||||
accumulator = accumulator || {executionCounts: {}, duration: {}};
|
||||
|
||||
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;
|
||||
}, null);
|
||||
};
|
||||
|
||||
const mergeQuery = () => {
|
||||
let queryFilter = _cloneDeep(route.query);
|
||||
|
||||
if (props.namespace) {
|
||||
queryFilter["namespace"] = props.namespace;
|
||||
}
|
||||
|
||||
if (props.flowId) {
|
||||
queryFilter["flowId"] = props.flowId;
|
||||
}
|
||||
|
||||
return queryFilter;
|
||||
return yamlFlow;
|
||||
}
|
||||
|
||||
const executionsLoading = ref(false);
|
||||
const fetchExecutions = () => {
|
||||
executionsLoading.value = true;
|
||||
|
||||
return store.dispatch("stat/daily", mergeQuery())
|
||||
.then((response) => {
|
||||
const sorted = response.sort(
|
||||
(a, b) => new Date(b.date) - new Date(a.date),
|
||||
);
|
||||
|
||||
executions.value = {
|
||||
raw: sorted,
|
||||
all: transformer(sorted),
|
||||
yesterday: sorted.at(-2),
|
||||
today: sorted.at(-1),
|
||||
};
|
||||
})
|
||||
.finally(() => {
|
||||
executionsLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const graphData = computed(() => store.state.stat.daily || []);
|
||||
|
||||
const namespaceExecutions = ref({});
|
||||
|
||||
const fetchNamespaceExecutions = () => {
|
||||
store.dispatch("stat/dailyGroupByNamespace", mergeQuery()).then((response) => {
|
||||
namespaceExecutions.value = response;
|
||||
});
|
||||
};
|
||||
|
||||
const logs = ref([]);
|
||||
const fetchLogs = () => {
|
||||
store.dispatch("stat/logDaily", mergeQuery()).then((response) => {
|
||||
logs.value = response;
|
||||
});
|
||||
};
|
||||
|
||||
const fetchAll = async () => {
|
||||
if (!custom.value.shown) {
|
||||
try {
|
||||
executionsLoading.value = true;
|
||||
await Promise.all([
|
||||
fetchNumbers(),
|
||||
fetchExecutions(),
|
||||
fetchNamespaceExecutions(),
|
||||
fetchLogs(),
|
||||
]).catch(error => {
|
||||
console.error("Failed to fetch dashboard data:", error);
|
||||
});
|
||||
} finally {
|
||||
executionsLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isAllowedTriggers = computed(() => {
|
||||
return (
|
||||
user &&
|
||||
user.isAllowed(permission.FLOW, action.READ, props.value?.namespace)
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
handleCustomUpdate(route.params?.id ? {id: route.params?.id} : undefined);
|
||||
if (props.isFlow) load("default", templateYamlFlow());
|
||||
else if (props.isNamespace) load("default", YAML_NAMESPACE);
|
||||
});
|
||||
|
||||
watch(
|
||||
route,
|
||||
async () => {
|
||||
await handleCustomUpdate(route.params?.id ? {id: route.params?.id} : undefined);
|
||||
fetchAll();
|
||||
},
|
||||
{immediate: true, deep: true},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||
|
||||
$spacing: 20px;
|
||||
|
||||
.dashboard-filters,
|
||||
.dashboard {
|
||||
section#filter {
|
||||
margin: 2rem 0.25rem 0;
|
||||
padding: 0 2rem;
|
||||
margin: 0;
|
||||
|
||||
.description {
|
||||
border: none !important;
|
||||
color: var(ks-content-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
$media-md: 500px;
|
||||
$media-lg: 1000px;
|
||||
|
||||
.dashboard{
|
||||
container-type: inline-size;
|
||||
padding-bottom: 1rem;
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.card {
|
||||
box-shadow: 0px 2px 4px 0px var(--ks-card-shadow);
|
||||
background: var(--ks-background-card);
|
||||
color: var(--ks-content-primary);
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
border-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
grid-column: span 12;
|
||||
@container (width > #{$media-md}) {
|
||||
grid-column: span 6;
|
||||
}
|
||||
@container (width > #{$media-lg}) {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
|
||||
@container (width > #{$media-md}) {
|
||||
.card-1\/2, .card-2\/3, .card-1\/3 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
.card-1\/2{
|
||||
@container (width > #{$media-lg}) {
|
||||
grid-column: span 6;
|
||||
}
|
||||
}
|
||||
|
||||
.card-2\/3{
|
||||
@container (width > #{$media-lg}) {
|
||||
grid-column: span 8;
|
||||
}
|
||||
}
|
||||
|
||||
.card-1\/3{
|
||||
@container (width > #{$media-lg}) {
|
||||
grid-column: span 4;
|
||||
}
|
||||
}
|
||||
|
||||
.card-1{
|
||||
@container (width > #{$media-md}) {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-filters {
|
||||
margin: 24px 0 0 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
& .el-row {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
& .el-col {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 0 2rem 1rem 2rem;
|
||||
margin: 0;
|
||||
section#description {
|
||||
margin: 0 0.25rem;
|
||||
padding: 0 2rem 1rem;
|
||||
color: var(--ks-content-secondary);
|
||||
}
|
||||
|
||||
.custom {
|
||||
padding: 0 2rem 1rem 2rem;
|
||||
|
||||
&.el-row {
|
||||
width: 100%;
|
||||
|
||||
& .el-col {
|
||||
padding-bottom: $spacing;
|
||||
|
||||
&:nth-of-type(even) > div {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
background: var(--ks-background-card);
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.legend) {
|
||||
&::-webkit-scrollbar {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--ks-background-body);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--ks-border-primary);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--ks-button-background-primary-hover);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
86
ui/src/components/dashboard/components/ChartsSection.vue
Normal file
86
ui/src/components/dashboard/components/ChartsSection.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<section id="charts">
|
||||
<el-row :gutter="8">
|
||||
<el-col
|
||||
v-for="(chart, index) in props.charts"
|
||||
:key="`${chart.id}__${index}`"
|
||||
:xs="24"
|
||||
:sm="(chart.chartOptions?.width || 6) * 4"
|
||||
:md="(chart.chartOptions?.width || 6) * 2"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<p v-if="!chart.type === 'io.kestra.plugin.core.dashboard.chart.KPI'" class="m-0">
|
||||
<span class="fs-6 fw-bold">{{ labels(chart).title }}</span>
|
||||
<template v-if="labels(chart)?.description">
|
||||
<br>
|
||||
<small class="fw-light">
|
||||
{{ labels(chart).description }}
|
||||
</small>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<component
|
||||
:is="TYPES[chart.type]"
|
||||
:default="route.params.id === 'default'"
|
||||
:source="chart.content"
|
||||
:chart="chart"
|
||||
:show-default="props.showDefault"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useRoute} from "vue-router";
|
||||
const route = useRoute();
|
||||
|
||||
import TimeSeries from "./charts/custom/TimeSeries.vue";
|
||||
import Bar from "./charts/custom/Bar.vue";
|
||||
import Markdown from "./MarkdownPanel.vue";
|
||||
import Table from "./tables/custom/Table.vue";
|
||||
import Pie from "./charts/custom/Pie.vue";
|
||||
import KPI from "./charts/custom/KPI.vue";
|
||||
|
||||
const TYPES = {
|
||||
"io.kestra.plugin.core.dashboard.chart.TimeSeries": TimeSeries,
|
||||
"io.kestra.plugin.core.dashboard.chart.Bar": Bar,
|
||||
"io.kestra.plugin.core.dashboard.chart.Markdown": Markdown,
|
||||
"io.kestra.plugin.core.dashboard.chart.Table": Table,
|
||||
"io.kestra.plugin.core.dashboard.chart.Pie": Pie,
|
||||
"io.kestra.plugin.core.dashboard.chart.KPI": KPI,
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
charts: {type: Array, required: true, default: () => []},
|
||||
showDefault: {type: Boolean, default: false},
|
||||
});
|
||||
|
||||
const labels = (chart) => ({
|
||||
title: chart?.chartOptions?.displayName ?? chart?.id,
|
||||
description: chart?.chartOptions?.description,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||
|
||||
section#charts {
|
||||
padding: 0 2rem 1rem;
|
||||
|
||||
& .el-row .el-col {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
padding: 1.5rem;
|
||||
background: var(--ks-background-card);
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -46,45 +46,8 @@
|
||||
{{ $t("save") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="w-100" v-if="currentView === views.DASHBOARD">
|
||||
<el-row class="custom">
|
||||
<el-col
|
||||
v-for="chart in charts"
|
||||
:key="JSON.stringify(chart)"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
>
|
||||
<div
|
||||
v-if="chart.data"
|
||||
class="p-4 d-flex flex-column"
|
||||
>
|
||||
<p class="m-0 fs-6 fw-bold">
|
||||
{{ chart.data.chartOptions?.displayName ?? chart.id }}
|
||||
</p>
|
||||
<p
|
||||
v-if="chart.chartOptions?.description"
|
||||
class="m-0 fw-light"
|
||||
>
|
||||
<small>{{ chart.data.chartOptions.description }}</small>
|
||||
</p>
|
||||
|
||||
<div class="mt-4 flex-grow-1">
|
||||
<component
|
||||
:is="types[chart.data.type]"
|
||||
:source="chart.data.content"
|
||||
:chart="chart.data"
|
||||
:identifier="chart.data.id"
|
||||
is-preview
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="d-flex justify-content-center align-items-center text-container">
|
||||
<el-tooltip :content="chart.error">
|
||||
{{ chart.error }}
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="w-100 p-4" v-if="currentView === views.DASHBOARD">
|
||||
<ChartsSection :charts="charts.map(chart => chart.data)" />
|
||||
</div>
|
||||
<div class="main-editor" v-else>
|
||||
<div
|
||||
@@ -161,6 +124,7 @@
|
||||
import {YamlUtils as YAML_UTILS} from "@kestra-io/ui-libs";
|
||||
|
||||
import PluginDocumentation from "../../plugins/PluginDocumentation.vue";
|
||||
import ChartsSection from "./ChartsSection.vue";
|
||||
import ValidationErrors from "../../flows/ValidationError.vue"
|
||||
import BookOpenVariant from "vue-material-design-icons/BookOpenVariant.vue";
|
||||
import ChartBar from "vue-material-design-icons/ChartBar.vue";
|
||||
@@ -178,11 +142,12 @@
|
||||
import yaml from "yaml";
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
import intro from "../../../assets/docs/dashboard_home.md?raw";
|
||||
import Markdown from "../../layout/Markdown.vue";
|
||||
import Markdown from "./MarkdownPanel.vue";
|
||||
import TimeSeries from "./charts/custom/TimeSeries.vue";
|
||||
import Bar from "./charts/custom/Bar.vue";
|
||||
import Pie from "./charts/custom/Pie.vue";
|
||||
import Table from "./tables/custom/Table.vue";
|
||||
import KPI from "./charts/custom/KPI.vue";
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
@@ -286,13 +251,13 @@
|
||||
},
|
||||
async loadChart(chart) {
|
||||
const yamlChart = yaml.stringify(chart);
|
||||
const result = {error: null, data: null};
|
||||
const result = {error: null, data: null, raw: {}};
|
||||
await this.$store.dispatch("dashboard/validateChart", yamlChart)
|
||||
.then(errors => {
|
||||
if (errors.constraints) {
|
||||
result.error = errors.constraints;
|
||||
} else {
|
||||
result.data = {...chart, content: yamlChart};
|
||||
result.data = {...chart, content: yamlChart, raw: chart};
|
||||
}
|
||||
});
|
||||
return result;
|
||||
@@ -320,6 +285,7 @@
|
||||
"io.kestra.plugin.core.dashboard.chart.Markdown": shallowRef(Markdown),
|
||||
"io.kestra.plugin.core.dashboard.chart.Table": shallowRef(Table),
|
||||
"io.kestra.plugin.core.dashboard.chart.Pie": shallowRef(Pie),
|
||||
"io.kestra.plugin.core.dashboard.chart.KPI": shallowRef(KPI)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TopNavBar :title="routeInfo.title" :breadcrumb="props.breadcrumb">
|
||||
<template #additional-right v-if="canCreate">
|
||||
<ul>
|
||||
<li v-if="props.id">
|
||||
<li v-if="props.id && props.id !== 'default'">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'dashboards/update',
|
||||
|
||||
74
ui/src/components/dashboard/components/MarkdownPanel.vue
Normal file
74
ui/src/components/dashboard/components/MarkdownPanel.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<template v-if="source">
|
||||
<section id="markdown">
|
||||
<Markdown :source />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<NoData v-else :text="t('custom_dashboard_empty')" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
|
||||
import Markdown from "../../layout/Markdown.vue";
|
||||
import NoData from "../../layout/NoData.vue";
|
||||
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
import {useStore} from "vuex";
|
||||
|
||||
const store = useStore();
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
import {decodeSearchParams} from "../../filter/utils/helpers.ts";
|
||||
|
||||
const props = defineProps({
|
||||
chart: {type: Object, required: true},
|
||||
showDefault: {type: Boolean, default: false}
|
||||
});
|
||||
|
||||
const source = ref();
|
||||
const generate = async (id) => {
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (!props.showDefault) {
|
||||
let params = {id, chartId: props.chart.id};
|
||||
if (route.query.namespace) {
|
||||
params.namespace = route.query.namespace;
|
||||
}
|
||||
if (route.query.labels) {
|
||||
params.labels = Object.fromEntries(
|
||||
route.query.labels.map((l) => l.split(":"))
|
||||
);
|
||||
}
|
||||
if (decodedParams) {
|
||||
params = {...params, filters: decodedParams};
|
||||
}
|
||||
const result = await store.dispatch("dashboard/generate", params);
|
||||
const description = result.results?.[0]?.description;
|
||||
|
||||
source.value = description ? description : t("dashboard.no_flow_description");
|
||||
} else {
|
||||
const result = await store.dispatch("dashboard/chartPreview", {
|
||||
chart: props.chart.content,
|
||||
globalFilter: {filter: decodedParams},
|
||||
})
|
||||
source.value = result.results[0]?.description;
|
||||
}
|
||||
};
|
||||
|
||||
watch(route, async (r) => {
|
||||
if (props.chart.source?.type === "FlowDescription") generate(r.params?.id);
|
||||
else source.value = props.chart.content ?? props.chart.source.content;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.chart.source?.type === "FlowDescription") generate(route.params?.id);
|
||||
else source.value = props.chart.content ?? props.chart.source.content;
|
||||
});
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@
|
||||
v-if="generated !== undefined"
|
||||
:data="parsedData"
|
||||
:options="options"
|
||||
:plugins="chartOptions.legend.enabled ? [customBarLegend] : []"
|
||||
:plugins="chartOptions?.legend?.enabled ? [customBarLegend] : []"
|
||||
class="chart"
|
||||
/>
|
||||
<NoData v-else />
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
import {Bar} from "vue-chartjs";
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import {customBarLegend} from "../legend.js";
|
||||
import {useTheme} from "../../../../../utils/utils.js";
|
||||
import {defaultConfig, getConsistentHEXColor, chartClick} from "../../../../../utils/charts.js";
|
||||
@@ -30,15 +32,12 @@
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
|
||||
const dashboard = computed(() => store.state.dashboard.dashboard);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
showDefault: {type: Boolean, default: false}
|
||||
});
|
||||
|
||||
const {data, chartOptions} = props.chart;
|
||||
@@ -64,7 +63,7 @@
|
||||
borderColor: "transparent",
|
||||
borderWidth: 2,
|
||||
plugins: {
|
||||
...(chartOptions.legend.enabled
|
||||
...(chartOptions?.legend?.enabled
|
||||
? {
|
||||
customBarLegend: {
|
||||
containerID,
|
||||
@@ -158,10 +157,11 @@
|
||||
});
|
||||
|
||||
const generated = ref();
|
||||
const generate = async () => {
|
||||
if (!props.isPreview) {
|
||||
const generate = async (id) => {
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (!props.showDefault) {
|
||||
let params = {
|
||||
id: dashboard.value.id,
|
||||
id,
|
||||
chartId: props.chart.id
|
||||
};
|
||||
if (route.query.namespace) {
|
||||
@@ -170,23 +170,18 @@
|
||||
if (route.query.labels) {
|
||||
params.labels = Object.fromEntries(route.query.labels.map(l => l.split(":")));
|
||||
}
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (decodedParams) {
|
||||
params = {...params, filters: decodedParams}
|
||||
}
|
||||
generated.value = await store.dispatch("dashboard/generate", params);
|
||||
}
|
||||
else {
|
||||
generated.value = await store.dispatch("dashboard/chartPreview", props.chart.content)
|
||||
generated.value = await store.dispatch("dashboard/chartPreview", {chart: props.chart.content, globalFilter: {filter: decodedParams}})
|
||||
}
|
||||
};
|
||||
|
||||
watch(route, async () => await generate());
|
||||
watch(
|
||||
() => props.identifier,
|
||||
() => generate(),
|
||||
);
|
||||
onMounted(() => generate());
|
||||
watch(route, async (route) => await generate(route.params?.id));
|
||||
onMounted(() => generate(route.params.id));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
84
ui/src/components/dashboard/components/charts/custom/KPI.vue
Normal file
84
ui/src/components/dashboard/components/charts/custom/KPI.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<template v-if="generated">
|
||||
<section id="kpi">
|
||||
<span class="pb-2">{{ label }}</span>
|
||||
<p class="m-0 fs-2 fw-bold">
|
||||
<span>{{ generated?.results[0]?.value }}</span>
|
||||
<span v-if="percentage">%</span>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<NoData v-else :text="t('custom_dashboard_empty')" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, computed, ref, watch} from "vue";
|
||||
|
||||
import NoData from "../../../../layout/NoData.vue";
|
||||
|
||||
import {useRoute} from "vue-router";
|
||||
const route = useRoute();
|
||||
|
||||
import {useStore} from "vuex";
|
||||
const store = useStore();
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
import {decodeSearchParams} from "../../../../filter/utils/helpers.ts";
|
||||
|
||||
const props = defineProps({
|
||||
chart: {type: Object, required: true},
|
||||
showDefault: {type: Boolean, default: false},
|
||||
});
|
||||
|
||||
const label = computed(
|
||||
() => props.chart?.chartOptions?.displayName || props.chart?.id,
|
||||
);
|
||||
const percentage = computed(
|
||||
() => props.chart?.chartOptions?.numberType === "PERCENTAGE"
|
||||
);
|
||||
|
||||
const generated = ref();
|
||||
const generate = async (id) => {
|
||||
// TODO: Tweak once the API is wrapped up
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (!props.showDefault) {
|
||||
let params = {id, chartId: props.chart.id};
|
||||
if (route.query.namespace) {
|
||||
params.namespace = route.query.namespace;
|
||||
}
|
||||
if (route.query.labels) {
|
||||
params.labels = Object.fromEntries(
|
||||
route.query.labels.map((l) => l.split(":")),
|
||||
);
|
||||
}
|
||||
if (decodedParams) {
|
||||
params = {...params, filters: decodedParams};
|
||||
}
|
||||
generated.value = await store.dispatch("dashboard/generate", params);
|
||||
} else {
|
||||
generated.value = await store.dispatch("dashboard/chartPreview", {
|
||||
chart: props.chart.content,
|
||||
globalFilter: {filter: decodedParams},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
watch(route, async (route) => await generate(route.params?.id));
|
||||
onMounted(() => generate(route.params.id));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||
|
||||
section#kpi {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -2,14 +2,14 @@
|
||||
<div
|
||||
class="d-flex flex-row align-items-center justify-content-center h-100"
|
||||
>
|
||||
<div class="w-75">
|
||||
<div>
|
||||
<component
|
||||
:is="chartOptions.graphStyle === 'PIE' ? Pie : Doughnut"
|
||||
v-if="generated !== undefined"
|
||||
:data="parsedData"
|
||||
:options="options"
|
||||
:plugins="
|
||||
chartOptions.legend.enabled
|
||||
chartOptions?.legend?.enabled
|
||||
? [isDuration ? totalsDurationLegend : totalsLegend, centerPlugin, thicknessPlugin]
|
||||
: [centerPlugin, thicknessPlugin]
|
||||
"
|
||||
@@ -43,13 +43,10 @@
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const dashboard = computed(() => store.state.dashboard.dashboard);
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
showDefault: {type: Boolean, default: false}
|
||||
});
|
||||
|
||||
const containerID = `${props.chart.id}__${Math.random()}`;
|
||||
@@ -63,7 +60,7 @@
|
||||
const options = computed(() => {
|
||||
return defaultConfig({
|
||||
plugins: {
|
||||
...(chartOptions.legend.enabled
|
||||
...(chartOptions?.legend?.enabled
|
||||
? {
|
||||
totalsLegend: {
|
||||
containerID,
|
||||
@@ -185,34 +182,28 @@
|
||||
});
|
||||
|
||||
const generated = ref();
|
||||
const generate = async () => {
|
||||
if (!props.isPreview) {
|
||||
let params = {
|
||||
id: dashboard.value.id,
|
||||
chartId: props.chart.id
|
||||
};
|
||||
const generate = async (id) => {
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
|
||||
if (!props.showDefault) {
|
||||
let params = {id, chartId: props.chart.id};
|
||||
if (route.query.namespace) {
|
||||
params.namespace = route.query.namespace;
|
||||
}
|
||||
if (route.query.labels) {
|
||||
params.labels = Object.fromEntries(route.query.labels.map(l => l.split(":")));
|
||||
}
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (decodedParams) {
|
||||
params = {...params, filters: decodedParams}
|
||||
}
|
||||
generated.value = await store.dispatch("dashboard/generate", params);
|
||||
} else {
|
||||
generated.value = await store.dispatch("dashboard/chartPreview", props.chart.content)
|
||||
generated.value = await store.dispatch("dashboard/chartPreview", {chart: props.chart.content, globalFilter: {filter: decodedParams}})
|
||||
}
|
||||
};
|
||||
|
||||
watch(route, async () => await generate());
|
||||
watch(
|
||||
() => props.identifier,
|
||||
() => generate(),
|
||||
);
|
||||
onMounted(() => generate());
|
||||
watch(route, async (route) => await generate(route.params?.id));
|
||||
onMounted(() => generate(route.params.id));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
v-if="generated !== undefined"
|
||||
:data="parsedData"
|
||||
:options
|
||||
:plugins="chartOptions.legend.enabled ? [customBarLegend] : []"
|
||||
:plugins="chartOptions?.legend?.enabled ? [customBarLegend] : []"
|
||||
class="chart"
|
||||
:class="chartOptions.legend.enabled ? 'with-legend' : ''"
|
||||
:class="chartOptions?.legend?.enabled ? 'with-legend' : ''"
|
||||
/>
|
||||
<NoData v-else />
|
||||
</template>
|
||||
@@ -31,16 +31,13 @@
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const dashboard = computed(() => store.state.dashboard.dashboard);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
showDefault: {type: Boolean, default: false}
|
||||
});
|
||||
|
||||
const containerID = `${props.chart.id}__${Math.random()}`;
|
||||
@@ -68,7 +65,7 @@
|
||||
borderColor: "transparent",
|
||||
borderWidth: 2,
|
||||
plugins: {
|
||||
...(chartOptions.legend.enabled
|
||||
...(chartOptions?.legend?.enabled
|
||||
? {
|
||||
customBarLegend: {
|
||||
containerID,
|
||||
@@ -241,10 +238,11 @@
|
||||
});
|
||||
|
||||
const generated = ref();
|
||||
const generate = async () => {
|
||||
if (!props.isPreview) {
|
||||
const generate = async (id) => {
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (!props.showDefault) {
|
||||
let params = {
|
||||
id: dashboard.value.id,
|
||||
id,
|
||||
chartId: props.chart.id
|
||||
};
|
||||
if (route.query.namespace) {
|
||||
@@ -253,22 +251,17 @@
|
||||
if (route.query.labels) {
|
||||
params.labels = Object.fromEntries(route.query.labels.map(l => l.split(":")));
|
||||
}
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (decodedParams) {
|
||||
params = {...params, filters: decodedParams}
|
||||
}
|
||||
generated.value = await store.dispatch("dashboard/generate", params);
|
||||
} else {
|
||||
generated.value = await store.dispatch("dashboard/chartPreview", props.chart.content)
|
||||
generated.value = await store.dispatch("dashboard/chartPreview", {chart: props.chart.content, globalFilter: {filter: decodedParams}})
|
||||
}
|
||||
};
|
||||
|
||||
watch(route, async () => await generate());
|
||||
watch(
|
||||
() => props.identifier,
|
||||
() => generate(),
|
||||
);
|
||||
onMounted(() => generate());
|
||||
watch(route, async (route) => await generate(route.params?.id));
|
||||
onMounted(() => generate(route.params.id));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,17 +2,17 @@ import Utils from "../../../../utils/utils";
|
||||
import {cssVariable} from "@kestra-io/ui-libs";
|
||||
import {getConsistentHEXColor} from "../../../../utils/charts.js";
|
||||
|
||||
const getOrCreateLegendList = (chart, id, direction = "row") => {
|
||||
const getOrCreateLegendList = (chart, id, direction = "row", width = "100%") => {
|
||||
const legendContainer = document.getElementById(id);
|
||||
|
||||
legendContainer.style.width = "100%";
|
||||
legendContainer.style.width = width;
|
||||
legendContainer.style.justifyItems = "end";
|
||||
|
||||
let listContainer = legendContainer?.querySelector("ul");
|
||||
|
||||
if (!listContainer) {
|
||||
listContainer = document.createElement("ul");
|
||||
listContainer.classList.add("w-100", "fw-light", "legend", direction === "row" ? "small" : "tall");
|
||||
listContainer.classList.add("mb-3", "fw-light", "legend", direction === "row" ? "small" : "tall");
|
||||
listContainer.style.display = "flex";
|
||||
listContainer.style.flexDirection = direction;
|
||||
listContainer.style.margin = 0;
|
||||
@@ -175,7 +175,7 @@ export const customBarLegend = {
|
||||
const generateTotalsLegend = (isDuration) => ({
|
||||
id: "totalsLegend",
|
||||
afterUpdate(chart, args, options) {
|
||||
const ul = getOrCreateLegendList(chart, options.containerID, "column");
|
||||
const ul = getOrCreateLegendList(chart, options.containerID, "column", "auto");
|
||||
|
||||
while (ul.firstChild) {
|
||||
ul.firstChild.remove();
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
<template>
|
||||
<template v-if="data !== undefined">
|
||||
<el-table :id="containerID" :data="data.results" :height="240">
|
||||
<el-table
|
||||
:id="containerID"
|
||||
:data="data.results"
|
||||
:height="240"
|
||||
size="small"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="(column, index) in Object.entries(props.chart.data.columns)"
|
||||
:key="index"
|
||||
:label="column[0]"
|
||||
v-for="key in Object.keys(props.chart.data.columns)"
|
||||
:label="key"
|
||||
:key
|
||||
>
|
||||
<template #default="scope">
|
||||
{{
|
||||
column[1].field === "DURATION" ? Utils.humanDuration(scope.row[column[0]]) : scope.row[column[0]]
|
||||
}}
|
||||
<template v-if="key === 'id'">
|
||||
<RouterLink
|
||||
v-if="scope.row.namespace && scope.row.flowId"
|
||||
:to="{
|
||||
name: 'executions/update',
|
||||
params: {
|
||||
namespace: scope.row.namespace,
|
||||
flowId: scope.row.flowId,
|
||||
id: scope.row.id,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<code>{{ scope.row.id.slice(0, 8) }}</code>
|
||||
</RouterLink>
|
||||
<code v-else>{{ scope.row.id }}</code>
|
||||
</template>
|
||||
<Status v-else-if="key === 'state'" size="small" :status="scope.row[key]" />
|
||||
<span v-else-if="key === 'duration'">{{ Utils.humanDuration(scope.row[key]) }}</span>
|
||||
<span v-else>{{ scope.row[key] }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -26,9 +47,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
import Status from "../../../../Status.vue";
|
||||
import NoData from "../../../../layout/NoData.vue";
|
||||
import Pagination from "../../../../layout/Pagination.vue";
|
||||
|
||||
@@ -46,56 +68,60 @@
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
showDefault: {type: Boolean, default: false},
|
||||
});
|
||||
|
||||
const containerID = `${props.chart.id}__${Math.random()}`;
|
||||
|
||||
const dashboard = computed(() => store.state.dashboard.dashboard);
|
||||
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
const handlePageChange = (options) => {
|
||||
currentPage.value = options.page;
|
||||
pageSize.value = options.size;
|
||||
generate();
|
||||
generate(route.params.id);
|
||||
};
|
||||
|
||||
const data = ref();
|
||||
const generate = async () => {
|
||||
if (!props.isPreview) {
|
||||
const generate = async (id) => {
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (!props.showDefault) {
|
||||
let params = {
|
||||
id: dashboard.value.id,
|
||||
chartId: props.chart.id
|
||||
id,
|
||||
chartId: props.chart.id,
|
||||
};
|
||||
if (route.query.namespace) {
|
||||
params.namespace = route.query.namespace;
|
||||
}
|
||||
if (route.query.labels) {
|
||||
params.labels = Object.fromEntries(route.query.labels.map(l => l.split(":")));
|
||||
params.labels = Object.fromEntries(
|
||||
route.query.labels.map((l) => l.split(":")),
|
||||
);
|
||||
}
|
||||
|
||||
if (props.chart.chartOptions?.pagination?.enabled) {
|
||||
params.pageNumber = currentPage.value;
|
||||
params.pageSize = pageSize.value;
|
||||
}
|
||||
let decodedParams = decodeSearchParams(route.query, undefined, []);
|
||||
if (decodedParams) {
|
||||
params = {...params, filters: decodedParams}
|
||||
params = {...params, filters: decodedParams};
|
||||
}
|
||||
data.value = await store.dispatch("dashboard/generate", params);
|
||||
} else {
|
||||
data.value = await store.dispatch("dashboard/chartPreview", props.chart.content)
|
||||
data.value = await store.dispatch("dashboard/chartPreview", {
|
||||
chart: props.chart.content,
|
||||
globalFilter: {filter: decodedParams},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
watch(route, async () => await generate());
|
||||
watch(
|
||||
() => props.identifier,
|
||||
() => generate(),
|
||||
);
|
||||
onMounted(() => generate());
|
||||
watch(route, async (route) => await generate(route.params?.id));
|
||||
onMounted(() => generate(route.params.id));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
code {
|
||||
color: var(--ks-content-id);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
placeholder: {type: String, default: undefined},
|
||||
searchCallback: {type: Function, default: undefined},
|
||||
isDefaultDashboard: {type: Boolean, default: false}
|
||||
isDefaultDashboard: {type: Boolean, default: false} // TODO: remove this when the default dashboard is removed
|
||||
});
|
||||
|
||||
const TEXT_PREFIX = `${t("filters.text_search")}: `;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
|
||||
<el-dropdown-item
|
||||
@click="selectDashboard(null)"
|
||||
@click="selectDashboard({id: 'default'})"
|
||||
:class="{'mt-3': filtered.length < 10}"
|
||||
>
|
||||
<small>{{ t("default_dashboard") }}</small>
|
||||
@@ -109,14 +109,16 @@
|
||||
|
||||
const selectedDashboard = ref(null)
|
||||
|
||||
const DASHBOARD_KEY = storageKeys.DASHBORD_SELECTED + (routeTenant.value ? `_${routeTenant.value}` : "")
|
||||
|
||||
const selectDashboard = (dashboard: any) => {
|
||||
selectedDashboard.value = dashboard?.title;
|
||||
if (dashboard?.id) {
|
||||
localStorage.setItem(storageKeys.DASHBORD_SELECTED + "_" + routeTenant.value, dashboard.id);
|
||||
localStorage.setItem(DASHBOARD_KEY, dashboard.id);
|
||||
} else {
|
||||
localStorage.removeItem(storageKeys.DASHBORD_SELECTED + "_" + routeTenant.value);
|
||||
localStorage.removeItem(DASHBOARD_KEY);
|
||||
}
|
||||
emits("dashboard", dashboard)
|
||||
emits("dashboard", dashboard.id)
|
||||
}
|
||||
|
||||
const editDashboard = (dashboard: any) => {
|
||||
@@ -135,13 +137,14 @@
|
||||
selectDashboard(dashboard);
|
||||
} else {
|
||||
selectedDashboard.value = null;
|
||||
emits("dashboard", "default")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetchLastDashboard = () => {
|
||||
return localStorage.getItem(storageKeys.DASHBORD_SELECTED + "_" + routeTenant.value)
|
||||
return localStorage.getItem(DASHBOARD_KEY)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<Dashboard
|
||||
v-if="loaded && total && flow"
|
||||
:restore-u-r-l="false"
|
||||
flow
|
||||
:flow-id="flow.id"
|
||||
:namespace="flow.namespace"
|
||||
embed
|
||||
:is-flow="true"
|
||||
/>
|
||||
<NoExecutions v-else-if="loaded && flow && !total" />
|
||||
</template>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function useHelpers() {
|
||||
name: "overview",
|
||||
title: t("overview"),
|
||||
component: Dashboard,
|
||||
props: {namespace, containerClass: "full-container flex-0"},
|
||||
props: {isNamespace: true},
|
||||
},
|
||||
{
|
||||
name: "flows",
|
||||
|
||||
@@ -12,7 +12,25 @@ export default [
|
||||
{name: "welcome", path: "/:tenant?/welcome", component: () => import("../components/onboarding/Welcome.vue")},
|
||||
|
||||
//Dashboards
|
||||
{name: "home", path: "/:tenant?/dashboards/:id?", component: () => import("../components/dashboard/Dashboard.vue")},
|
||||
{
|
||||
name: "home",
|
||||
path: "/:tenant?/dashboards/:id?",
|
||||
component: () => import("../components/dashboard/Dashboard.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (!to.params.id) {
|
||||
next({
|
||||
name: "home",
|
||||
params: {
|
||||
...to.params,
|
||||
id: "default",
|
||||
},
|
||||
query: to.query,
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
},
|
||||
{name: "dashboards/create", path: "/:tenant?/dashboards/new", component: () => import("../components/dashboard/components/DashboardCreate.vue")},
|
||||
{name: "dashboards/update", path: "/:tenant?/dashboards/:id/edit", component: () => import("override/components/dashboard/components/DashboardEdit.vue")},
|
||||
|
||||
|
||||
@@ -20,11 +20,21 @@ export default {
|
||||
}).then(response => response.data);
|
||||
},
|
||||
load({commit}, id) {
|
||||
return this.$http.get(`${apiUrl(this)}/dashboards/${id}`).then(response => {
|
||||
const dashboard = response.data;
|
||||
commit("setDashboard", dashboard);
|
||||
return dashboard;
|
||||
});
|
||||
return this.$http
|
||||
.get(`${apiUrl(this)}/dashboards/${id}`, {
|
||||
validateStatus: (status) => {
|
||||
return status === 200 || status === 404;
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
let dashboard;
|
||||
|
||||
if (response.status === 200) dashboard = response.data;
|
||||
else if (response.status === 404) dashboard = {title: "Default", id};
|
||||
|
||||
commit("setDashboard", dashboard);
|
||||
return dashboard;
|
||||
});
|
||||
},
|
||||
create(_, source) {
|
||||
return this.$http.post(`${apiUrl(this)}/dashboards`, source, yamlContentHeader).then(response => response.data);
|
||||
@@ -35,9 +45,19 @@ export default {
|
||||
delete(_, id) {
|
||||
return this.$http.delete(`${apiUrl(this)}/dashboards/${id}`).then(response => response.data);
|
||||
},
|
||||
generate(_, {id, chartId, ...filters}) {
|
||||
generate(_, {id, chartId, ...filters}) {
|
||||
const filtersObj = Object.keys(filters).length > 0 ? filters : null;
|
||||
return this.$http.post(`${apiUrl(this)}/dashboards/${id}/charts/${chartId}`, filtersObj).then(response => response.data);
|
||||
return this.$http
|
||||
.post(
|
||||
`${apiUrl(this)}/dashboards/${id}/charts/${chartId}`,
|
||||
filtersObj,
|
||||
{
|
||||
validateStatus: (status) => {
|
||||
return status === 200 || status === 404;
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((response) => response.data);
|
||||
},
|
||||
validate(_, source) {
|
||||
return this.$http.post(`${apiUrl(this)}/dashboards/validate`, source, yamlContentHeader).then(response => {
|
||||
@@ -54,7 +74,7 @@ export default {
|
||||
});
|
||||
},
|
||||
chartPreview(_, chart) {
|
||||
return this.$http.post(`${apiUrl(this)}/dashboards/charts/preview`, chart, yamlContentHeader)
|
||||
return this.$http.post(`${apiUrl(this)}/dashboards/charts/preview`, chart)
|
||||
.then(response => response.data);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {provide, ref} from "vue";
|
||||
import {TOPOLOGY_CLICK_INJECTION_KEY} from "../../../../src/components/code/injectionKeys";
|
||||
import {useStore} from "vuex";
|
||||
import {vueRouter} from "storybook-vue3-router";
|
||||
import LowCodeEditor from "../../../../src/components/inputs/LowCodeEditor.vue";
|
||||
@@ -17,6 +19,7 @@ export default {
|
||||
const Template= (args) => ({
|
||||
setup() {
|
||||
const store = useStore()
|
||||
provide(TOPOLOGY_CLICK_INJECTION_KEY, ref())
|
||||
store.$http = {
|
||||
get(){
|
||||
return Promise.resolve({data: {}})
|
||||
|
||||
@@ -2,18 +2,28 @@ package io.kestra.webserver.controllers.api;
|
||||
|
||||
import io.kestra.core.models.QueryFilter;
|
||||
import io.kestra.core.models.dashboards.Dashboard;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.models.dashboards.DataFilterKPI;
|
||||
import io.kestra.core.models.dashboards.charts.Chart;
|
||||
import io.kestra.core.models.dashboards.charts.DataChart;
|
||||
import io.kestra.core.models.dashboards.charts.DataChartKPI;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.models.validations.ValidateConstraintViolation;
|
||||
import io.kestra.core.repositories.ArrayListTotal;
|
||||
import io.kestra.core.repositories.DashboardRepositoryInterface;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.serializers.YamlParser;
|
||||
import io.kestra.core.tenant.TenantService;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.plugin.core.dashboard.chart.Markdown;
|
||||
import io.kestra.plugin.core.dashboard.chart.mardown.sources.FlowDescription;
|
||||
import io.kestra.webserver.models.GlobalFilter;
|
||||
import io.kestra.webserver.responses.PagedResults;
|
||||
import io.kestra.webserver.utils.PageableUtils;
|
||||
import io.kestra.webserver.utils.TimeLineSearch;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.http.HttpResponse;
|
||||
import io.micronaut.http.HttpStatus;
|
||||
import io.micronaut.http.MediaType;
|
||||
@@ -24,9 +34,9 @@ import io.micronaut.validation.Validated;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -43,10 +53,14 @@ import static io.kestra.core.utils.DateUtils.validateTimeline;
|
||||
@Controller("/api/v1/main/dashboards")
|
||||
@Slf4j
|
||||
public class DashboardController {
|
||||
protected static final YamlParser YAML_PARSER = new YamlParser();
|
||||
|
||||
@Inject
|
||||
private DashboardRepositoryInterface dashboardRepository;
|
||||
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepository;
|
||||
|
||||
@Inject
|
||||
protected TenantService tenantService;
|
||||
|
||||
@@ -196,31 +210,99 @@ public class DashboardController {
|
||||
return null;
|
||||
}
|
||||
|
||||
Integer pageNumber = globalFilter.getPageNumber();
|
||||
Integer pageSize = globalFilter.getPageSize();
|
||||
|
||||
if (chart instanceof DataChart dataChart) {
|
||||
Integer pageNumber = globalFilter.getPageNumber();
|
||||
Integer pageSize = globalFilter.getPageSize();
|
||||
|
||||
dataChart.getData().setGlobalFilter(filters, startDate, endDate);
|
||||
DataFilter<?, ?> dataChartDatas = dataChart.getData();
|
||||
dataChartDatas.updateWhereWithGlobalFilters(filters, startDate, endDate);
|
||||
|
||||
// StartDate & EndDate are only set in the globalFilter for JDBC
|
||||
// TODO: Check if we can remove them from generate() for ElasticSearch as they are already set in the where property
|
||||
return PagedResults.of(this.dashboardRepository.generate(tenantId, dataChart, startDate, endDate, pageNumber != null && pageSize != null ? PageableUtils.from(pageNumber, pageSize) : null));
|
||||
} else if (chart instanceof DataChartKPI dataChartKPI) {
|
||||
DataFilterKPI<?, ?> dataChartDatas = dataChartKPI.getData();
|
||||
dataChartDatas.updateWhereWithGlobalFilters(filters, startDate, endDate);
|
||||
|
||||
return PagedResults.of(new ArrayListTotal<>(this.dashboardRepository.generateKPI(tenantId, dataChartKPI, startDate, endDate), 1));
|
||||
} else if (chart instanceof Markdown markdownChart) {
|
||||
if (markdownChart.getSource() != null && markdownChart.getSource() instanceof FlowDescription flowDescription) {
|
||||
Optional<Flow> optionalFlow = flowRepository.findById(this.tenantService.resolveTenant(), flowDescription.getNamespace(), flowDescription.getFlowId());
|
||||
if (optionalFlow.isPresent()) {
|
||||
Flow flow = optionalFlow.get();
|
||||
Map<String, Object> descriptionMap = Map.of(
|
||||
"description", flow.getDescription() != null ? flow.getDescription() : ""
|
||||
);
|
||||
|
||||
return PagedResults.of(new ArrayListTotal<>(List.of(descriptionMap), 1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Flow not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Only data charts can be generated.");
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "charts/preview", consumes = MediaType.APPLICATION_YAML)
|
||||
@Post(uri = "charts/preview")
|
||||
@Operation(tags = {"Dashboards"}, summary = "Preview a chart data")
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public PagedResults<Map<String, Object>> previewChartData(
|
||||
@RequestBody(description = "The chart definition as YAML") @Body String chart
|
||||
public PagedResults<Map<String, Object>> previewChart(
|
||||
@Parameter(description = "The chart") @Body @Valid PreviewRequest previewRequest
|
||||
) throws IOException {
|
||||
Chart<?> parsed = YamlParser.parse(chart, Chart.class);
|
||||
Chart<?> chart = YAML_PARSER.parse(previewRequest.chart(), Chart.class);
|
||||
GlobalFilter globalFilter = previewRequest.globalFilter();
|
||||
|
||||
return PagedResults.of(this.dashboardRepository.generate(tenantService.resolveTenant(), (DataChart) parsed, ZonedDateTime.now().minusDays(8), ZonedDateTime.now(), null));
|
||||
List<QueryFilter> filters =
|
||||
globalFilter != null ? globalFilter.getFilters() : null;
|
||||
|
||||
ZonedDateTime endDate = null;
|
||||
ZonedDateTime startDate = null;
|
||||
if (filters != null) {
|
||||
TimeLineSearch timeLineSearch = TimeLineSearch.extractFrom(filters);
|
||||
validateTimeline(timeLineSearch.getStartDate(), timeLineSearch.getEndDate());
|
||||
|
||||
endDate = timeLineSearch.getEndDate();
|
||||
startDate = timeLineSearch.getStartDate();
|
||||
} else {
|
||||
endDate = ZonedDateTime.now();
|
||||
startDate = endDate.minusDays(8);
|
||||
}
|
||||
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new IllegalArgumentException("`endDate` must be after `startDate`.");
|
||||
}
|
||||
|
||||
if (chart instanceof DataChart dataChart) {
|
||||
DataFilter<?, ?> dataChartDatas = dataChart.getData();
|
||||
dataChartDatas.updateWhereWithGlobalFilters(filters, startDate, endDate);
|
||||
|
||||
// StartDate & EndDate are only set in the globalFilter for JDBC
|
||||
// TODO: Check if we can remove them from generate() for ElasticSearch as they are already set in the where property
|
||||
return PagedResults.of(this.dashboardRepository.generate(this.tenantService.resolveTenant(), dataChart, startDate, endDate, null));
|
||||
} else if (chart instanceof DataChartKPI dataChartKPI) {
|
||||
DataFilterKPI<?, ?> dataChartDatas = dataChartKPI.getData();
|
||||
dataChartDatas.updateWhereWithGlobalFilters(filters, startDate, endDate);
|
||||
|
||||
return PagedResults.of(new ArrayListTotal<>(this.dashboardRepository.generateKPI(this.tenantService.resolveTenant(), dataChartKPI, startDate, endDate),1));
|
||||
} else if (chart instanceof Markdown markdownChart) {
|
||||
if (markdownChart.getSource() != null && markdownChart.getSource() instanceof FlowDescription flowDescription) {
|
||||
Optional<Flow> optionalFlow = flowRepository.findById(this.tenantService.resolveTenant(), flowDescription.getNamespace(), flowDescription.getFlowId());
|
||||
if (optionalFlow.isPresent()) {
|
||||
Flow flow = optionalFlow.get();
|
||||
Map<String, Object> descriptionMap = Map.of(
|
||||
"description", flow.getDescription() != null ? flow.getDescription() : ""
|
||||
);
|
||||
|
||||
return PagedResults.of(new ArrayListTotal<>(List.of(descriptionMap), 1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Flow not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Chart is not an instance of DataChart.");
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@@ -249,5 +331,9 @@ public class DashboardController {
|
||||
return validateConstraintViolationBuilder.build();
|
||||
}
|
||||
|
||||
@Introspected
|
||||
public record PreviewRequest(
|
||||
@Parameter(description = "The chart") String chart,
|
||||
@Parameter(description = "The filters to apply, some can override chart definition like labels & namespace") @Nullable GlobalFilter globalFilter) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ public class StatsController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "summary")
|
||||
@Operation(tags = {"Stats"}, summary = "Get summary statistics")
|
||||
@Deprecated
|
||||
public SummaryStatistics summary(@Body @Valid SummaryRequest request) {
|
||||
return new SummaryStatistics(
|
||||
flowRepositoryInterface.countForNamespace(tenantService.resolveTenant(), request.namespace()),
|
||||
@@ -66,6 +67,7 @@ public class StatsController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "executions/daily")
|
||||
@Operation(tags = {"Stats"}, summary = "Get daily statistics for executions")
|
||||
@Deprecated
|
||||
public List<DailyExecutionStatistics> dailyStatistics(@Body @Valid StatisticRequest statisticRequest) {
|
||||
// @TODO: seems to be converted back to utc by micronaut
|
||||
return executionRepository.dailyStatistics(
|
||||
@@ -84,6 +86,7 @@ public class StatsController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "taskruns/daily")
|
||||
@Operation(tags = {"Stats"}, summary = "Get daily statistics for taskRuns")
|
||||
@Deprecated
|
||||
public List<DailyExecutionStatistics> taskRunsDailyStatistics(@Body @Valid StatisticRequest statisticRequest) {
|
||||
return executionRepository.dailyStatistics(
|
||||
statisticRequest.q(),
|
||||
@@ -101,6 +104,7 @@ public class StatsController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "executions/daily/group-by-flow")
|
||||
@Operation(tags = {"Stats"}, summary = "Get daily statistics for executions group by namespaces and flows")
|
||||
@Deprecated
|
||||
public Map<String, Map<String, List<DailyExecutionStatistics>>> dailyGroupByFlowStatistics(@Body @Valid ByFlowStatisticRequest statisticRequest) {
|
||||
return executionRepository.dailyGroupByFlowStatistics(
|
||||
statisticRequest.q(),
|
||||
@@ -117,6 +121,7 @@ public class StatsController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "executions/daily/group-by-namespace")
|
||||
@Operation(tags = {"Stats"}, summary = "Get daily statistics for executions grouped by namespace")
|
||||
@Deprecated
|
||||
public Map<String, ExecutionCountStatistics> dailyStatisticsGroupByNamespace(@Body @Valid ByNamespaceStatisticRequest request) {
|
||||
return executionRepository.executionCountsGroupedByNamespace(
|
||||
tenantService.resolveTenant(),
|
||||
@@ -129,6 +134,7 @@ public class StatsController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "executions/latest/group-by-flow")
|
||||
@Operation(tags = {"Stats"}, summary = "Get latest execution by flows")
|
||||
@Deprecated
|
||||
public List<Execution> lastExecutions(
|
||||
@Parameter(description = "A list of flows filter") @Body @Valid LastExecutionsRequest lastExecutionsRequest
|
||||
) {
|
||||
@@ -141,6 +147,7 @@ public class StatsController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "logs/daily")
|
||||
@Operation(tags = {"Stats"}, summary = "Get daily statistics for logs")
|
||||
@Deprecated
|
||||
public List<LogStatistics> logsDailyStatistics(@Body @Valid LogStatisticRequest logStatisticRequest) {
|
||||
return logRepositoryInterface.statistics(
|
||||
logStatisticRequest.q(),
|
||||
|
||||
@@ -20,6 +20,9 @@ public class TimeLineSearch {
|
||||
Duration timeRange = null;
|
||||
|
||||
for (QueryFilter filter : filters) {
|
||||
if (filter.field() == null) {
|
||||
continue;
|
||||
}
|
||||
switch (filter.field()) {
|
||||
case START_DATE -> startDate = ZonedDateTime.parse(filter.value().toString());
|
||||
case END_DATE -> endDate = ZonedDateTime.parse(filter.value().toString());
|
||||
|
||||
Reference in New Issue
Block a user