Skip to content

Commit

Permalink
feat(ui): add autocompletion & dropdown for subflows
Browse files Browse the repository at this point in the history
part of #2473
  • Loading branch information
brian-mulier-p committed Apr 18, 2024
1 parent d00be2d commit 871a0ef
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ List<FlowWithSource> findWithSource(

List<String> findDistinctNamespace(String tenantId);

default List<String> findDistinctNamespace(String tenantId, String prefix) {
List<String> distinctNamespaces = this.findDistinctNamespace(tenantId);

if (prefix == null) {
return distinctNamespaces;
}

return distinctNamespaces.stream()
.filter(n -> n.startsWith(prefix))
.toList();
}

FlowWithSource create(Flow flow, String flowSource, Flow flowWithDefaults);

FlowWithSource update(Flow flow, Flow previous, String flowSource, Flow flowWithDefaults) throws ConstraintViolationException;
Expand Down
3 changes: 1 addition & 2 deletions ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,7 @@
surveyVisible = false;
})
window.addEventListener("KestraRouterAfterEach", (event) => {
console.log(event)
window.addEventListener("KestraRouterAfterEach", () => {
if (surveyVisible) {
window.dispatchEvent(new Event("PHSurveyClosed"))
surveyVisible = false;
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/flows/FlowRun.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
v-model="inputs[input.id]"
>
<el-option
v-for="item in input.values"
v-for="item in input.value"
:key="item"
:label="item"
:value="item"
Expand Down
1 change: 0 additions & 1 deletion ui/src/components/flows/TriggerVars.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
},
computed: {
variables() {
console.log(Utils.executionVars(this.data))
return Utils.executionVars(this.data);
},
},
Expand Down
10 changes: 9 additions & 1 deletion ui/src/components/flows/tasks/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export default {
type: Boolean,
default: false
},
task: {
type: Object,
default: undefined
},
root: {
type: String,
default: undefined
Expand All @@ -28,7 +32,7 @@ export default {
isRequired(key) {
return this.schema.required && this.schema.required.includes(key);
},
getType(property) {
getType(property, key) {
if (property.enum !== undefined) {
return "enum";
}
Expand Down Expand Up @@ -57,6 +61,10 @@ export default {
return "anyOf";
}

if (key === "namespace" || key === "flowId") {
return key;
}

return property.type || "dynamic";
},
// eslint-disable-next-line no-unused-vars
Expand Down
43 changes: 43 additions & 0 deletions ui/src/components/flows/tasks/TaskFlowId.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<el-select
:model-value="values"
@update:model-value="onInput"
filterable
clearable
:persistent="false"
allow-create
>
<el-option
v-for="item in flowIds"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
<script>
import Task from "./Task";
import {mapState} from "vuex";
export default {
mixins: [Task],
data() {
return {
flowIds: []
}
},
watch: {
async namespace() {
this.flowIds = (await this.$store.dispatch("flow/flowsByNamespace", this.namespace))
.map(flow => flow.id)
.filter(id => id !== this.flow.id);
}
},
computed: {
...mapState("flow", ["flow"]),
namespace() {
return this.task?.namespace ?? this.flow?.namespace;
},
}
};
</script>
26 changes: 26 additions & 0 deletions ui/src/components/flows/tasks/TaskNamespace.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<namespace-select
data-type="flow"
:value="modelValue"
allow-create
@update:model-value="onInput"
/>
</template>
<script>
import Task from "./Task";
import NamespaceSelect from "../../namespace/NamespaceSelect.vue";
import {mapState} from "vuex";
export default {
components: {NamespaceSelect},
mixins: [Task],
created() {
const flowNamespace = this.flow?.namespace;
if (!this.modelValue && flowNamespace) {
this.onInput(flowNamespace)
}
},
computed: {
...mapState("flow", ["flow"])
}
};
</script>
5 changes: 3 additions & 2 deletions ui/src/components/flows/tasks/TaskObject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
</span>
<span>
<el-tag disable-transitions type="info" size="small">
{{ getType(schema, key) }}
{{ getType(schema) }}
</el-tag>
</span>
</span>
</template>
<component
:is="`task-${getType(schema)}`"
:is="`task-${getType(schema, key)}`"
:model-value="getPropertiesValue(key)"
:task="modelValue"
@update:model-value="onObjectInput(key, $event)"
:root="getKey(key)"
:schema="schema"
Expand Down
99 changes: 91 additions & 8 deletions ui/src/components/inputs/MonacoEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import Utils from "../../utils/utils";
import YamlUtils from "../../utils/yamlUtils";
import {uniqBy} from "lodash";
import {mapState} from "vuex";
window.MonacoEnvironment = {
getWorker(moduleId, label) {
Expand All @@ -44,6 +45,9 @@
});
export default defineComponent({
computed: {
...mapState("namespace", ["datatypeNamespaces"])
},
props: {
original: {
type: String,
Expand Down Expand Up @@ -136,15 +140,85 @@
schemas: yamlSchemas(this.$store)
});
this.autocompletionProvider = this.monaco.languages.registerCompletionItemProvider("yaml", {
this.subflowAutocompletionProvider = this.monaco.languages.registerCompletionItemProvider("yaml", {
triggerCharacters: [":"],
async provideCompletionItems(model, position) {
const lineContent = _this.lineContent(model, position);
const tillCursorContent = lineContent.substring(0, position.column - 1);
let match = tillCursorContent.match(/^( *namespace:( *))(.*)$/);
let indexOfFieldToComplete;
if (match) {
indexOfFieldToComplete = match.index + match[1].length;
if (!_this.datatypeNamespaces) {
await _this.$store.dispatch("namespace/loadNamespacesForDatatype", {dataType: "flow"})
}
let filteredNamespaces = _this.datatypeNamespaces;
if (match[3].length > 0) {
filteredNamespaces = filteredNamespaces.filter(n => n.startsWith(match[3]));
}
return {
suggestions: filteredNamespaces.map(namespace => ({
kind: monaco.languages.CompletionItemKind.Value,
label: namespace,
insertText: (match[2].length > 0 ? "" : " ") + namespace,
range: {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: indexOfFieldToComplete + 1,
endColumn: YamlUtils.nextDelimiterIndex(lineContent, position.column - 1)
}
}))
};
}
match = tillCursorContent.match(/^( *flowId:( *))(.*)$/);
if (!match) {
return {suggestions: []};
}
indexOfFieldToComplete = match.index + match[1].length;
const source = model.getValue();
const namespacesWithRange = YamlUtils.extractFieldFromMaps(source, "namespace").reverse();
const namespace = namespacesWithRange.find(namespaceWithRange => {
const range = namespaceWithRange.range;
return range[0] < position.offset < range[2];
})?.namespace;
if (namespace === undefined) {
return {suggestions: []};
}
const flowAsJs = YamlUtils.parse(source);
let flowIds = (await _this.$store.dispatch("flow/flowsByNamespace", namespace))
.map(flow => flow.id)
if (match[3].length > 0) {
flowIds = flowIds.filter(flowId => flowId.startsWith(match[3]));
}
if (flowAsJs?.id) {
flowIds = flowIds.filter(flowId => flowId !== flowAsJs?.id);
}
return {
suggestions: flowIds.map(flowId => ({
kind: monaco.languages.CompletionItemKind.Value,
label: flowId,
insertText: (match[2].length > 0 ? "" : " ") + flowId,
range: {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: indexOfFieldToComplete + 1,
endColumn: YamlUtils.nextDelimiterIndex(lineContent, position.column - 1)
}
}))
};
}
})
this.nestedFieldAutocompletionProvider = this.monaco.languages.registerCompletionItemProvider("yaml", {
triggerCharacters: ["."],
async provideCompletionItems(model, position) {
const lineContent = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: model.getLineMaxColumn(position.lineNumber)
});
const lineContent = _this.lineContent(model, position);
const tillCursorContent = lineContent.substring(0, position.column - 1);
const match = tillCursorContent.match(/( *([^{ ]*)\.)([^.} ]*)$/);
if (!match) {
Expand Down Expand Up @@ -183,6 +257,14 @@
this.destroy();
},
methods: {
lineContent(model, position) {
return model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: model.getLineMaxColumn(position.lineNumber)
});
},
async autocompletion(source, lineContent, field, rest, lineNumber, fieldToCompleteIndexes) {
const flowAsJs = YamlUtils.parse(source);
let autocompletions;
Expand Down Expand Up @@ -324,7 +406,8 @@
},
destroy: function () {
this.monacoYaml?.dispose();
this.autocompletionProvider?.dispose();
this.subflowAutocompletionProvider?.dispose();
this.nestedFieldAutocompletionProvider?.dispose();
this.editor?.dispose();
},
needReload: function (newValue, oldValue) {
Expand Down
6 changes: 3 additions & 3 deletions ui/src/components/namespace/NamespaceSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@
emits: ["update:modelValue"],
created() {
this.$store
.dispatch("namespace/loadNamespaces", {dataType: this.dataType})
.dispatch("namespace/loadNamespacesForDatatype", {dataType: this.dataType})
.then(() => {
this.groupedNamespaces = this.groupNamespaces(this.namespaces);
this.groupedNamespaces = this.groupNamespaces(this.datatypeNamespaces);
});
},
computed: {
...mapState("namespace", ["namespaces"])
...mapState("namespace", ["datatypeNamespaces"])
},
data() {
return {
Expand Down
5 changes: 5 additions & 0 deletions ui/src/stores/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export default {
return response.data;
})
},
flowsByNamespace(_, namespace) {
return this.$http.get(`${apiUrl(this)}/flows/${namespace}`).then(response => {
return response.data;
})
},
loadFlow({commit}, options) {
return this.$http.get(`${apiUrl(this)}/flows/${options.namespace}/${options.id}?source=true`,
{
Expand Down
10 changes: 5 additions & 5 deletions ui/src/stores/namespaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import Utils from "../utils/utils";
export default {
namespaced: true,
state: {
namespaces: undefined,
datatypeNamespaces: undefined,
},

actions: {
loadNamespaces({commit}, options) {
loadNamespacesForDatatype({commit}, options) {
return this.$http.get(`${apiUrl(this)}/${options.dataType}s/distinct-namespaces`).then(response => {
commit("setNamespaces", response.data)
commit("setDatatypeNamespaces", response.data)
})
},
importFile({_commit}, options) {
Expand Down Expand Up @@ -38,8 +38,8 @@ export default {
}
},
mutations: {
setNamespaces(state, namespaces) {
state.namespaces = namespaces
setDatatypeNamespaces(state, datatypeNamespaces) {
state.datatypeNamespaces = datatypeNamespaces
}
},
getters: {}
Expand Down
4 changes: 4 additions & 0 deletions ui/src/utils/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import TaskObject from "../components/flows/tasks/TaskObject.vue";
import TaskString from "../components/flows/tasks/TaskString.vue";
import TaskTask from "../components/flows/tasks/TaskTask.vue";
import TaskAnyOf from "../components/flows/tasks/TaskAnyOf.vue";
import TaskNamespace from "../components/flows/tasks/TaskNamespace.vue";
import TaskFlowId from "../components/flows/tasks/TaskFlowId.vue";

export default (app, routes, stores, translations) => {
// charts
Expand Down Expand Up @@ -142,6 +144,8 @@ export default (app, routes, stores, translations) => {
app.component("TaskString", TaskString)
app.component("TaskTask", TaskTask)
app.component("TaskAnyOf", TaskAnyOf)
app.component("TaskNamespace", TaskNamespace)
app.component("TaskFlowId", TaskFlowId)

return {store, router};
}
Loading

0 comments on commit 871a0ef

Please sign in to comment.