Skip to content

Commit

Permalink
feat(core): Implement bulk delete/disable for flows & templates (#1008)
Browse files Browse the repository at this point in the history
  • Loading branch information
Skraye authored Feb 27, 2023
1 parent efbd006 commit bb6a425
Show file tree
Hide file tree
Showing 15 changed files with 522 additions and 28 deletions.
1 change: 1 addition & 0 deletions core/src/main/java/io/kestra/core/models/flows/Flow.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import javax.validation.Valid;
import javax.validation.constraints.*;


@SuperBuilder(toBuilder = true)
@Getter
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void run() {
});

triggerQueue.receive(trigger -> {
if (trigger.getExecutionId() != null) {
if (trigger != null && trigger.getExecutionId() != null) {
this.watchingTrigger.put(trigger.getExecutionId(), trigger);
}
});
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/io/kestra/core/services/FlowService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.kestra.core.utils.ListUtils;

import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.inject.Inject;
Expand Down Expand Up @@ -215,6 +216,15 @@ public static String cleanupSource(String source) {
return source.replaceFirst("(?m)^revision: \\d+\n?","");
}

public static String injectDisabledTrue(String source) {
Pattern p = Pattern.compile("^disabled\\s*:\\s*false\\s*", Pattern.MULTILINE);
if (p.matcher(source).find()) {
return p.matcher(source).replaceAll("disabled: true\n");
}

return source + "\ndisabled: true";
}

@AllArgsConstructor
@Getter
private static class FlowWithTrigger {
Expand Down
66 changes: 66 additions & 0 deletions ui/src/components/flows/Flows.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@
<el-button v-if="canRead" :icon="Download" size="large" @click="exportFlows()">
{{ $t('export') }}
</el-button>
<el-button v-if="canDelete" @click="deleteFlows" size="large" :icon="TrashCan">
{{ $t('delete') }}
</el-button>
<el-button v-if="canDisable" @click="disableFlows" size="large" :icon="FileDocumentRemoveOutline">
{{ $t('disable') }}
</el-button>
</bottom-line-counter>
</ul>
<li class="spacer" />
Expand Down Expand Up @@ -152,6 +158,8 @@
import Plus from "vue-material-design-icons/Plus.vue";
import TextBoxSearch from "vue-material-design-icons/TextBoxSearch.vue";
import Download from "vue-material-design-icons/Download.vue";
import TrashCan from "vue-material-design-icons/TrashCan.vue";
import FileDocumentRemoveOutline from "vue-material-design-icons/FileDocumentRemoveOutline.vue";
</script>

<script>
Expand Down Expand Up @@ -224,6 +232,12 @@
canRead() {
return this.user && this.user.isAllowed(permission.FLOW, action.READ);
},
canDelete() {
return this.user && this.user.isAllowed(permission.FLOW, action.DELETE);
},
canDisable() {
return this.user && this.user.isAllowed(permission.FLOW, action.UPDATE);
},
},
methods: {
handleSelectionChange(val) {
Expand Down Expand Up @@ -266,6 +280,58 @@
() => {}
)
},
disableFlows(){
this.$toast().confirm(
this.$t("flow disable", {"flowCount": this.queryBulkAction ? this.total : this.flowsSelection.length}),
() => {
if (this.queryBulkAction) {
return this.$store
.dispatch("flow/disableFlowByQuery", this.loadQuery({
namespace: this.$route.query.namespace ? [this.$route.query.namespace] : undefined,
q: this.$route.query.q ? [this.$route.query.q] : undefined,
}, false))
.then(r => {
this.$toast().success(this.$t("flows disabled", {count: r.data.count}));
this.loadData(() => {})
})
} else {
return this.$store
.dispatch("flow/disableFlowByIds", {ids: this.flowsSelection})
.then(r => {
this.$toast().success(this.$t("flows disabled", {count: r.data.count}));
this.loadData(() => {})
})
}
},
() => {}
)
},
deleteFlows(){
this.$toast().confirm(
this.$t("flow delete", {"flowCount": this.queryBulkAction ? this.total : this.flowsSelection.length}),
() => {
if (this.queryBulkAction) {
return this.$store
.dispatch("flow/deleteFlowByQuery", this.loadQuery({
namespace: this.$route.query.namespace ? [this.$route.query.namespace] : undefined,
q: this.$route.query.q ? [this.$route.query.q] : undefined,
}, false))
.then(r => {
this.$toast().success(this.$t("flows deleted", {count: r.data.count}));
this.loadData(() => {})
})
} else {
return this.$store
.dispatch("flow/deleteFlowByIds", {ids: this.flowsSelection})
.then(r => {
this.$toast().success(this.$t("flows deleted", {count: r.data.count}));
this.loadData(() => {})
})
}
},
() => {}
)
},
importFlows() {
const formData = new FormData();
formData.append("fileUpload", this.$refs.file.files[0]);
Expand Down
37 changes: 35 additions & 2 deletions ui/src/components/templates/Templates.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,22 @@
<el-button v-if="canRead" :icon="Download" size="large" @click="exportTemplates()">
{{ $t('export') }}
</el-button>
<el-button v-if="canDelete" @click="deleteTemplates" size="large" :icon="TrashCan">
{{ $t('delete') }}
</el-button>
</bottom-line-counter>
</ul>

<li class="spacer" />
<li>
<div class="el-input el-input-file el-input--large custom-upload">
<div class="el-input__wrapper">
<label for="importFlows">
<label for="importTemplates">
<Upload />
{{ $t('import') }}
</label>
<input
id="importFlows"
id="importTemplates"
class="el-input__inner"
type="file"
@change="importTemplates()"
Expand All @@ -105,6 +108,7 @@
<script setup>
import Plus from "vue-material-design-icons/Plus.vue";
import Download from "vue-material-design-icons/Download.vue";
import TrashCan from "vue-material-design-icons/TrashCan.vue";
</script>

<script>
Expand Down Expand Up @@ -159,6 +163,9 @@
canRead() {
return this.user && this.user.isAllowed(permission.FLOW, action.READ);
},
canDelete() {
return this.user && this.user.isAllowed(permission.FLOW, action.DELETE);
},
},
methods: {
loadQuery(base) {
Expand Down Expand Up @@ -227,6 +234,32 @@
this.loadData(() => {})
})
},
deleteTemplates(){
this.$toast().confirm(
this.$t("template delete", {"templateCount": this.queryBulkAction ? this.total : this.templatesSelection.length}),
() => {
if (this.queryBulkAction) {
return this.$store
.dispatch("template/deleteTemplateByQuery", this.loadQuery({
namespace: this.$route.query.namespace ? [this.$route.query.namespace] : undefined,
q: this.$route.query.q ? [this.$route.query.q] : undefined,
}, false))
.then(r => {
this.$toast().success(this.$t("templates deleted", {count: r.data.count}));
this.loadData(() => {})
})
} else {
return this.$store
.dispatch("template/deleteTemplateByIds", {ids: this.templatesSelection})
.then(r => {
this.$toast().success(this.$t("templates deleted", {count: r.data.count}));
this.loadData(() => {})
})
}
},
() => {}
)
},
},
};
</script>
Expand Down
12 changes: 12 additions & 0 deletions ui/src/stores/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ export default {
},
importFlows(_, options) {
return this.$http.post("/api/v1/flows/import", options, {headers: {"Content-Type": "multipart/form-data"}})
},
disableFlowByIds(_, options) {
return this.$http.post("/api/v1/flows/disable/by-ids", options.ids)
},
disableFlowByQuery(_, options) {
return this.$http.post("/api/v1/flows/disable/by-query", options, {params: options})
},
deleteFlowByIds(_, options) {
return this.$http.delete("/api/v1/flows/delete/by-ids", {data: options.ids})
},
deleteFlowByQuery(_, options) {
return this.$http.delete("/api/v1/flows/delete/by-query", options, {params: options})
}
},
mutations: {
Expand Down
6 changes: 6 additions & 0 deletions ui/src/stores/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export default {
},
importTemplates(_, options) {
return this.$http.post("/api/v1/templates/import", options, {headers: {"Content-Type": "multipart/form-data"}})
},
deleteTemplateByIds(_, options) {
return this.$http.delete("/api/v1/templates/delete/by-ids", {data: options.ids})
},
deleteTemplateByQuery(_, options) {
return this.$http.delete("/api/v1/templates/delete/by-query", options, {params: options})
}
},
mutations: {
Expand Down
22 changes: 22 additions & 0 deletions ui/src/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@
"flows exported": "Flows exported",
"export all flows": "Export all flows",
"import": "Import",
"disable": "Disable",
"template delete": "Are you sure you want to delete <code>{templateCount}</code> template(s)?",
"flow delete": "Are you sure you want to delete <code>{flowCount}</code> flow(s)?",
"templates deleted": "<code>{count}</code> Template(s) deleted",
"flows deleted": "<code>{count}</code> Flow(s) deleted",
"flow disable": "Are you sure you want to disable <code>{flowCount}</code> flow(s)?",
"flows disabled": "<code>{count}</code> Flow(s) disabled",
"dependencies": "Dependencies",
"see dependencies": "See dependencies",
"dependencies missing acls": "No permissions on this flow",
Expand Down Expand Up @@ -571,6 +578,13 @@
"flows exported": "Flows exportés",
"export all flows": "Exporter tous les flows",
"import": "Importer",
"disable": "Désactiver",
"template delete": "Êtes vous sûr de vouloir supprimer <code>{templateCount}</code> template(s)?",
"flow delete": "Êtes vous sûr de vouloir supprimer <code>{flowCount}</code> flow(s)?",
"templates deleted": "<code>{count}</code> Template(s) supprimé(s)",
"flows deleted": "<code>{count}</code> Flow(s) supprimé(s)",
"flow disable": "Êtes vous sûr de vouloir désactiver <code>{flowCount}</code> flow(s)?",
"flows disabled": "<code>{count}</code> Flow(s) désactivé(s)",
"dependencies": "Dépendances",
"see dependencies": "Voir les dépendances",
"dependencies missing acls": "Aucune permission sur ce flow",
Expand Down Expand Up @@ -859,6 +873,14 @@
"flows exported": "Flows exportiert",
"export all flows": "Exportieren Sie alle Flows",
"import": "Importieren",
"disable": "Deaktivieren",
"template delete": "Sind Sie sicher, dass Sie <code>{templateCount}</code> Template(n) löschen möchten?",
"flow delete": "Sind Sie sicher, dass Sie <code>{flowCount}</code> Flows löschen möchten?",
"templates deleted": "Template(s) gelöscht",
"flows deleted": "Flow(s) gelöscht",
"flow disable": "Sind Sie sicher, dass Sie <code>{flowCount}</code> Flows deaktivieren möchten?",
"flows disabled": "Flow(s) deaktiviert",
"import": "Importieren",
"dependencies": "Abhängigkeiten",
"see dependencies": "Siehe Abhängigkeiten",
"dependencies missing acls": "Keinen Zugriff für diesen Flow",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.runners.RunContext;
import io.kestra.core.runners.RunContextFactory;
import io.kestra.webserver.responses.BulkErrorResponse;
import io.kestra.webserver.responses.BulkResponse;
import io.micronaut.context.annotation.Value;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.core.convert.format.Format;
Expand Down Expand Up @@ -109,21 +111,6 @@ public class ExecutionController {
@Inject
private RunContextFactory runContextFactory;

@SuperBuilder
@Getter
@NoArgsConstructor
public static class BulkResponse {
Integer count;
}

@SuperBuilder
@Getter
@NoArgsConstructor
public static class BulkErrorResponse {
String message;
Set<ManualConstraintViolation<String>> invalids;
}

@ExecuteOn(TaskExecutors.IO)
@Get(uri = "executions/search", produces = MediaType.TEXT_JSON)
@Operation(tags = {"Executions"}, summary = "Search for executions")
Expand Down
Loading

0 comments on commit bb6a425

Please sign in to comment.