fix(ui) add a better linter configuration

This commit is contained in:
Ludovic DEHON
2020-11-04 11:52:18 +01:00
committed by tchiotludo
parent 367f951900
commit cd99681712
89 changed files with 2781 additions and 2633 deletions

25
.editorconfig Normal file
View File

@@ -0,0 +1,25 @@
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
trim_trailing_whitespace=true
indent_style=space
indent_size=4
continuation_indent_size=4
[*.yml]
indent_size=2
[*.md]
indent_size=2
[*.yaml]
indent_size=2
[*.json]
indent_size=2
[*.css]
indent_size=2

View File

@@ -1,5 +1,3 @@
module.exports = {
presets: [
'@vue/app'
]
}
presets: ["@vue/app"]
};

View File

@@ -65,10 +65,37 @@
"node": true
},
"extends": [
"plugin:vue/essential",
"plugin:vue/strongly-recommended",
"eslint:recommended"
],
"rules": {},
"rules": {
"vue/html-indent": [
"error",
4,
{
"baseIndent": 1
}
],
"vue/script-indent": [
"error",
4,
{
"baseIndent": 1
}
],
"vue/max-attributes-per-line": [
"error",
{
"singleline": 7
}
],
"quotes": [
"error",
"double"
],
"vue/object-curly-spacing": ["error", "never"],
"object-curly-spacing": ["error", "never"]
},
"parserOptions": {
"parser": "babel-eslint"
}

View File

@@ -1,59 +1,59 @@
<template>
<div>
<nprogress-container></nprogress-container>
<top-nav-bar :menuCollapsed="menuCollapsed" />
<nprogress-container />
<top-nav-bar :menu-collapsed="menuCollapsed" />
<Menu @onMenuCollapse="onMenuCollapse" />
<custom-toast v-if="errorMessage" :noAutoHide="true" toastId="errorToast" :content="errorMessage" :title="$t('error')" />
<custom-toast v-if="errorMessage" :no-auto-hide="true" toast-id="errorToast" :content="errorMessage" :title="$t('error')" />
<div id="app" class="container-fluid">
<div class="content-wrapper" :class="menuCollapsed">
<router-view></router-view>
<router-view />
</div>
</div>
</div>
</template>
<script>
import Menu from "Override/components/Menu.vue";
import TopNavBar from "./components/layout/TopNavBar";
import CustomToast from "./components/customToast";
import NprogressContainer from "vue-nprogress/src/NprogressContainer";
import { mapState } from "vuex";
import Menu from "Override/components/Menu.vue";
import TopNavBar from "./components/layout/TopNavBar";
import CustomToast from "./components/customToast";
import NprogressContainer from "vue-nprogress/src/NprogressContainer";
import {mapState} from "vuex";
export default {
name: "app",
components: {
Menu,
TopNavBar,
CustomToast,
NprogressContainer
},
data() {
return {
menuCollapsed: "",
};
},
computed: {
...mapState('core', ['errorMessage'])
},
created() {
if (this.$route.path === "/") {
this.$router.push({ name: "flowsList" });
}
this.displayApp()
this.onMenuCollapse(localStorage.getItem("menuCollapsed") === "true");
},
methods: {
onMenuCollapse(collapse) {
this.menuCollapsed = collapse ? "menu-collapsed" : "menu-not-collapsed";
export default {
name: "App",
components: {
Menu,
TopNavBar,
CustomToast,
NprogressContainer
},
displayApp() {
document.getElementById("loader-wrapper").style.display = "none";
document.getElementById("app-container").style.display = "block";
data() {
return {
menuCollapsed: "",
};
},
computed: {
...mapState("core", ["errorMessage"])
},
created() {
if (this.$route.path === "/") {
this.$router.push({name: "flowsList"});
}
this.displayApp()
this.onMenuCollapse(localStorage.getItem("menuCollapsed") === "true");
},
methods: {
onMenuCollapse(collapse) {
this.menuCollapsed = collapse ? "menu-collapsed" : "menu-not-collapsed";
},
displayApp() {
document.getElementById("loader-wrapper").style.display = "none";
document.getElementById("app-container").style.display = "block";
}
}
}
};
};
</script>

View File

@@ -1,45 +1,45 @@
<template>
<b-button class="status text-white rounded-lg" :class="'btn-' + cls">
<component :is="icon"></component>
{{status | lower | cap }}
<component :is="icon" />
{{ status | lower | cap }}
</b-button>
</template>
<script>
import State from "../utils/state";
import PauseCircleOutline from "vue-material-design-icons/PauseCircleOutline";
import CheckCircleOutline from "vue-material-design-icons/CheckCircleOutline";
import PlayCircleOutline from "vue-material-design-icons/PlayCircleOutline";
import CloseCircleOutline from "vue-material-design-icons/CloseCircleOutline";
import StopCircleOutline from "vue-material-design-icons/StopCircleOutline";
import State from "../utils/state";
import PauseCircleOutline from "vue-material-design-icons/PauseCircleOutline";
import CheckCircleOutline from "vue-material-design-icons/CheckCircleOutline";
import PlayCircleOutline from "vue-material-design-icons/PlayCircleOutline";
import CloseCircleOutline from "vue-material-design-icons/CloseCircleOutline";
import StopCircleOutline from "vue-material-design-icons/StopCircleOutline";
export default {
components: {
PauseCircleOutline,
CheckCircleOutline,
PlayCircleOutline,
CloseCircleOutline,
StopCircleOutline
},
props: {
status: {
type: String,
required: true
export default {
components: {
PauseCircleOutline,
CheckCircleOutline,
PlayCircleOutline,
CloseCircleOutline,
StopCircleOutline
},
size: {
type: String,
default: ""
}
},
computed: {
cls() {
return State.colorClass()[this.status] + (this.size ? " btn-" + this.size : "");
props: {
status: {
type: String,
required: true
},
size: {
type: String,
default: ""
}
},
icon () {
return State.icon()[this.status];
computed: {
cls() {
return State.colorClass()[this.status] + (this.size ? " btn-" + this.size : "");
},
icon () {
return State.icon()[this.status];
}
}
}
};
};
</script>
<style scoped>
button.status {

View File

@@ -2,55 +2,62 @@
<b-toast @hide="onHide" :id="toastId" :variant="variant" solid :no-auto-hide="noAutoHide">
<template v-slot:toast-title>
<div class="d-flex flex-grow-1 align-items-baseline">
<strong class="mr-auto">{{title}}</strong>
<strong class="mr-auto">{{ title }}</strong>
</div>
</template>
<span>{{content.message || content}}</span>
<b-table class="mt-2 mb-0" small bordered v-if="items && items.length > 0" striped hover
:items="items"></b-table>
<span>{{ content.message || content }}</span>
<b-table
class="mt-2 mb-0"
small
bordered
v-if="items && items.length > 0"
striped
hover
:items="items"
/>
</b-toast>
</template>
<script>
export default {
props: {
variant: {
type: String,
default: "danger"
export default {
props: {
variant: {
type: String,
default: "danger"
},
title: {
type: String,
required: true
},
toastId: {
type: String,
required: true
},
content: {
type: Object,
required: true
},
noAutoHide: {
type: Boolean,
default: false
}
},
title: {
type: String,
required: true
mounted() {
this.$bvToast.show(this.toastId);
},
toastId: {
type: String,
required: true
computed: {
items() {
const messages = this.content && this.content._embedded && this.content._embedded.errors ? this.content._embedded.errors : []
return Array.isArray(messages) ? messages : [messages]
}
},
content: {
type: Object,
required: true
},
noAutoHide: {
type: Boolean,
default: false
methods: {
onHide() {
setTimeout(() => {
this.$store.commit("core/setErrorMessage", undefined);
}, 1000);
}
}
},
mounted() {
this.$bvToast.show(this.toastId);
},
computed: {
items() {
const messages = this.content && this.content._embedded && this.content._embedded.errors ? this.content._embedded.errors : []
return Array.isArray(messages) ? messages : [messages]
}
},
methods: {
onHide() {
setTimeout(() => {
this.$store.commit("core/setErrorMessage", undefined);
}, 1000);
}
}
};
};
</script>
<style lang="scss">
@import "../styles/variable";

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="execution && outputs">
<b-navbar toggleable="lg" type="light" variant="light">
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-navbar-toggle target="nav-collapse" />
<b-collapse id="nav-collapse" is-nav>
<b-nav-form>
<v-select
@@ -10,7 +10,7 @@
@input="onSearch"
:options="selectOptions"
:placeholder="$t('display output for specific task') + '...'"
></v-select>
/>
</b-nav-form>
</b-collapse>
</b-navbar>
@@ -39,96 +39,96 @@
</div>
</template>
<script>
import { mapState } from "vuex";
import md5 from "md5";
import VarValue from "./VarValue";
import Utils from "../../utils/utils";
import {mapState} from "vuex";
import md5 from "md5";
import VarValue from "./VarValue";
import Utils from "../../utils/utils";
export default {
components: {
VarValue,
},
data() {
return {
filter: ""
};
},
created() {
if (this.$route.query.search) {
this.filter = this.$route.query.search || ""
}
},
watch: {
$route() {
if (this.$route.query.search !== this.filter) {
this.filter = this.$route.query.search || "";
}
}
},
methods: {
onSearch() {
if (this.filter && this.$route.query.search !== this.filter) {
const newRoute = { query: { ...this.$route.query } };
newRoute.query.search = this.filter;
this.$router.push(newRoute);
export default {
components: {
VarValue,
},
data() {
return {
filter: ""
};
},
created() {
if (this.$route.query.search) {
this.filter = this.$route.query.search || ""
}
},
taskRunOutputToken(taskRun) {
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ''));
}
},
computed: {
...mapState("execution", ["execution"]),
selectOptions() {
const options = {};
for (const taskRun of this.execution.taskRunList || []) {
options[this.taskRunOutputToken(taskRun)] = {
label: taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ''),
value: this.taskRunOutputToken(taskRun)
watch: {
$route() {
if (this.$route.query.search !== this.filter) {
this.filter = this.$route.query.search || "";
}
}
return Object.values(options);
},
fields() {
return [
{
key: "task",
label: this.$t("task")
},
{
key: "value",
label: this.$t("value")
},
{
key: "key",
label: this.$t("name")
},
{
key: "output",
label: this.$t("output")
}
];
},
outputs() {
const outputs = [];
for (const taskRun of this.execution.taskRunList || []) {
const token = this.taskRunOutputToken(taskRun)
if (!this.filter || token === this.filter) {
Utils.executionVars(taskRun.outputs).forEach(output => {
const item = {
key: output.key,
output: output.value,
task: taskRun.taskId,
value: taskRun.value
};
outputs.push(item);
})
methods: {
onSearch() {
if (this.filter && this.$route.query.search !== this.filter) {
const newRoute = {query: {...this.$route.query}};
newRoute.query.search = this.filter;
this.$router.push(newRoute);
}
},
taskRunOutputToken(taskRun) {
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""));
}
},
computed: {
...mapState("execution", ["execution"]),
selectOptions() {
const options = {};
for (const taskRun of this.execution.taskRunList || []) {
options[this.taskRunOutputToken(taskRun)] = {
label: taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""),
value: this.taskRunOutputToken(taskRun)
}
}
return Object.values(options);
},
fields() {
return [
{
key: "task",
label: this.$t("task")
},
{
key: "value",
label: this.$t("value")
},
{
key: "key",
label: this.$t("name")
},
{
key: "output",
label: this.$t("output")
}
];
},
outputs() {
const outputs = [];
for (const taskRun of this.execution.taskRunList || []) {
const token = this.taskRunOutputToken(taskRun)
if (!this.filter || token === this.filter) {
Utils.executionVars(taskRun.outputs).forEach(output => {
const item = {
key: output.key,
output: output.value,
task: taskRun.taskId,
value: taskRun.value
};
outputs.push(item);
})
}
}
return outputs;
}
return outputs;
}
}
};
};
</script>

View File

@@ -17,148 +17,148 @@
</b-card>
</template>
<script>
import Gantt from "./Gantt";
import Overview from "./Overview";
import Logs from "../logs/Logs";
import Topology from "./Topology";
import ExecutionOutput from "./ExecutionOutput";
import Trigger from "vue-material-design-icons/Cogs";
import BottomLine from "../layout/BottomLine";
import FlowActions from "../flows/FlowActions";
import RouteContext from "../../mixins/routeContext";
import { mapState } from "vuex";
import Gantt from "./Gantt";
import Overview from "./Overview";
import Logs from "../logs/Logs";
import Topology from "./Topology";
import ExecutionOutput from "./ExecutionOutput";
import Trigger from "vue-material-design-icons/Cogs";
import BottomLine from "../layout/BottomLine";
import FlowActions from "../flows/FlowActions";
import RouteContext from "../../mixins/routeContext";
import {mapState} from "vuex";
export default {
mixins: [RouteContext],
components: {
Overview,
BottomLine,
Trigger,
Gantt,
Logs,
Topology,
FlowActions,
ExecutionOutput
},
data() {
return {
sse: undefined
};
},
created() {
this.follow();
},
methods: {
follow() {
this.closeSSE();
this.$store
.dispatch("execution/followExecution", this.$route.params)
.then(sse => {
this.sse = sse;
sse.subscribe("", (data, event) => {
this.$store.commit("execution/setExecution", data);
if (this.$route.query.tab === 'topology') {
this.$store.dispatch('execution/loadTree', data)
}
if (event && event.lastEventId === "end") {
this.closeSSE();
}
});
});
export default {
mixins: [RouteContext],
components: {
Overview,
BottomLine,
Trigger,
Gantt,
Logs,
Topology,
FlowActions,
ExecutionOutput
},
closeSSE() {
if (this.sse) {
this.sse.close();
this.sse = undefined;
}
},
setTab(tab) {
this.$store.commit("execution/setTask", undefined);
this.$router.push({
name: "executionEdit",
params: this.$route.params,
query: { tab }
});
}
},
computed: {
...mapState("execution", ["execution"]),
routeInfo() {
const ns = this.$route.params.namespace;
data() {
return {
title: this.$t("execution"),
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList",
query: {
namespace: ns
}
}
},
{
label: `${ns}.${this.$route.params.flowId}`,
link: {
name: "flowEdit",
params: {
namespace: ns,
id: this.$route.params.flowId
}
}
},
{
label: this.$t("executions"),
link: {
name: 'flowEdit',
params: {
namespace: ns,
id: this.$route.params.flowId
},
query: {
tab: 'executions'
}
}
},
{
label: this.$route.params.id,
link: {
name: "executionEdit"
}
}
]
sse: undefined
};
},
tabs() {
const title = title => this.$t(title);
return [
{
tab: "overview",
title: title("overview")
},
{
tab: "gantt",
title: title("gantt")
},
{
tab: "logs",
title: title("logs")
},
{
tab: "topology",
title: title("topology")
},
{
tab: "execution-output",
title: title("output")
created() {
this.follow();
},
methods: {
follow() {
this.closeSSE();
this.$store
.dispatch("execution/followExecution", this.$route.params)
.then(sse => {
this.sse = sse;
sse.subscribe("", (data, event) => {
this.$store.commit("execution/setExecution", data);
if (this.$route.query.tab === "topology") {
this.$store.dispatch("execution/loadTree", data)
}
if (event && event.lastEventId === "end") {
this.closeSSE();
}
});
});
},
closeSSE() {
if (this.sse) {
this.sse.close();
this.sse = undefined;
}
];
},
setTab(tab) {
this.$store.commit("execution/setTask", undefined);
this.$router.push({
name: "executionEdit",
params: this.$route.params,
query: {tab}
});
}
},
computed: {
...mapState("execution", ["execution"]),
routeInfo() {
const ns = this.$route.params.namespace;
return {
title: this.$t("execution"),
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList",
query: {
namespace: ns
}
}
},
{
label: `${ns}.${this.$route.params.flowId}`,
link: {
name: "flowEdit",
params: {
namespace: ns,
id: this.$route.params.flowId
}
}
},
{
label: this.$t("executions"),
link: {
name: "flowEdit",
params: {
namespace: ns,
id: this.$route.params.flowId
},
query: {
tab: "executions"
}
}
},
{
label: this.$route.params.id,
link: {
name: "executionEdit"
}
}
]
};
},
tabs() {
const title = title => this.$t(title);
return [
{
tab: "overview",
title: title("overview")
},
{
tab: "gantt",
title: title("gantt")
},
{
tab: "logs",
title: title("logs")
},
{
tab: "topology",
title: title("topology")
},
{
tab: "execution-output",
title: title("output")
}
];
}
},
beforeDestroy() {
this.closeSSE();
this.$store.commit("execution/setExecution", undefined);
}
},
beforeDestroy() {
this.closeSSE();
this.$store.commit("execution/setExecution", undefined);
}
};
};
</script>

View File

@@ -3,10 +3,10 @@
<data-table @onPageChanged="onPageChanged" ref="dataTable" :total="total">
<template v-slot:navbar>
<search-field ref="searchField" @onSearch="onSearch" :fields="searchableFields" />
<namespace-select data-type="flow" v-if="$route.name !== 'flowEdit'" @onNamespaceSelect="onNamespaceSelect" />
<status-filter-buttons @onRefresh="onStatusChange"/>
<namespace-select data-type="flow" v-if="$route.name !== 'flowEdit'" @onNamespaceSelect="onNamespaceSelect" />
<status-filter-buttons @onRefresh="onStatusChange" />
<date-range @onDate="onSearch" />
<refresh-button class="float-right" @onRefresh="loadData"/>
<refresh-button class="float-right" @onRefresh="loadData" />
</template>
<template v-slot:top>
@@ -30,7 +30,7 @@
@row-dblclicked="onRowDoubleClick"
>
<template #empty>
<span class="text-black-50">{{$t('no result')}}</span>
<span class="text-black-50">{{ $t('no result') }}</span>
</template>
<template v-slot:cell(details)="row">
@@ -40,13 +40,15 @@
</template>
<template
v-slot:cell(state.startDate)="row"
>{{row.item.state.startDate | date('LLLL')}}</template>
>
{{ row.item.state.startDate | date('LLLL') }}
</template>
<template
v-slot:cell(state.endDate)="row"
>
<span v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)">
{{row.item.state.endDate | date('LLLL')}}
</span>
<span v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)">
{{ row.item.state.endDate | date('LLLL') }}
</span>
</template>
<template v-slot:cell(state.current)="row">
<status
@@ -55,197 +57,197 @@
size="sm"
/>
</template>
<template v-slot:cell(state.duration)="row">
<span v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)">{{durationFrom(row.item) | humanizeDuration}}</span>
<span v-else>{{row.item.state.duration | humanizeDuration}}</span>
<template v-slot:cell(state.duration)="row">
<span v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)">{{ durationFrom(row.item) | humanizeDuration }}</span>
<span v-else>{{ row.item.state.duration | humanizeDuration }}</span>
</template>
<template v-slot:cell(flowId)="row">
<router-link
:to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.flowId}}"
>{{row.item.flowId}}</router-link>
>
{{ row.item.flowId }}
</router-link>
</template>
<template v-slot:cell(id)="row">
<code>{{row.item.id | id}}</code>
<code>{{ row.item.id | id }}</code>
</template>
<template v-slot:cell(trigger)="row">
<trigger-avatar @showTriggerDetails="showTriggerDetails" :execution="row.item"/>
<trigger-avatar @showTriggerDetails="showTriggerDetails" :execution="row.item" />
</template>
</b-table>
</template>
</data-table>
<flow-trigger-details-modal v-if="flowTriggerDetails" :trigger="flowTriggerDetails"/>
<flow-trigger-details-modal v-if="flowTriggerDetails" :trigger="flowTriggerDetails" />
</div>
</template>
<script>
import { mapState } from "vuex";
import DataTable from "../layout/DataTable";
import Eye from "vue-material-design-icons/Eye";
import Status from "../Status";
import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions";
import SearchField from "../layout/SearchField";
import NamespaceSelect from "../namespace/NamespaceSelect";
import DateRange from "../layout/DateRange";
import RefreshButton from '../layout/RefreshButton'
import StatusFilterButtons from '../layout/StatusFilterButtons'
import StateGlobalChart from "../../components/stats/StateGlobalChart";
import FlowTriggerDetailsModal from "../../components/flows/TriggerDetailsModal";
import TriggerAvatar from "../../components/flows/TriggerAvatar";
import {mapState} from "vuex";
import DataTable from "../layout/DataTable";
import Eye from "vue-material-design-icons/Eye";
import Status from "../Status";
import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions";
import SearchField from "../layout/SearchField";
import NamespaceSelect from "../namespace/NamespaceSelect";
import DateRange from "../layout/DateRange";
import RefreshButton from "../layout/RefreshButton"
import StatusFilterButtons from "../layout/StatusFilterButtons"
import StateGlobalChart from "../../components/stats/StateGlobalChart";
import FlowTriggerDetailsModal from "../../components/flows/TriggerDetailsModal";
import TriggerAvatar from "../../components/flows/TriggerAvatar";
export default {
mixins: [RouteContext, DataTableActions],
components: {
Status,
Eye,
DataTable,
SearchField,
NamespaceSelect,
DateRange,
RefreshButton,
StatusFilterButtons,
StateGlobalChart,
FlowTriggerDetailsModal,
TriggerAvatar
},
data() {
return {
dataType: "execution",
dailyReady: false,
flowTriggerDetails: undefined
};
},
beforeCreate() {
const q = JSON.parse(localStorage.getItem('executionQueries') || '{}')
q.sort = q.sort ? q.sort : 'state.startDate:desc'
q.status = q.status ? q.status : 'ALL'
localStorage.setItem('executionQueries', JSON.stringify(q))
},
computed: {
...mapState("execution", ["executions", "total"]),
...mapState("stat", ["daily"]),
fields() {
const title = title => {
return this.$t(title);
export default {
mixins: [RouteContext, DataTableActions],
components: {
Status,
Eye,
DataTable,
SearchField,
NamespaceSelect,
DateRange,
RefreshButton,
StatusFilterButtons,
StateGlobalChart,
FlowTriggerDetailsModal,
TriggerAvatar
},
data() {
return {
dataType: "execution",
dailyReady: false,
flowTriggerDetails: undefined
};
return [
{
key: "id",
label: title("id")
},
{
key: "state.startDate",
label: title("start date"),
sortable: true
},
{
key: "state.endDate",
label: title("end date"),
sortable: true
},
{
key: "state.duration",
label: title("duration"),
sortable: true
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "flowId",
label: title("flow"),
sortable: true
},
{
key: "state.current",
label: title("state"),
class: "text-center",
sortable: true
},
{
key: "trigger",
label: title("trigger"),
class: "shrink"
},
{
key: "details",
label: "",
class: "row-action"
},
beforeCreate() {
const q = JSON.parse(localStorage.getItem("executionQueries") || "{}")
q.sort = q.sort ? q.sort : "state.startDate:desc"
q.status = q.status ? q.status : "ALL"
localStorage.setItem("executionQueries", JSON.stringify(q))
},
computed: {
...mapState("execution", ["executions", "total"]),
...mapState("stat", ["daily"]),
fields() {
const title = title => {
return this.$t(title);
};
return [
{
key: "id",
label: title("id")
},
{
key: "state.startDate",
label: title("start date"),
sortable: true
},
{
key: "state.endDate",
label: title("end date"),
sortable: true
},
{
key: "state.duration",
label: title("duration"),
sortable: true
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "flowId",
label: title("flow"),
sortable: true
},
{
key: "state.current",
label: title("state"),
class: "text-center",
sortable: true
},
{
key: "trigger",
label: title("trigger"),
class: "shrink"
},
{
key: "details",
label: "",
class: "row-action"
}
];
},
executionQuery() {
let filter;
if (this.$route.name === "flowEdit") {
filter = `namespace:${this.$route.params.namespace} AND flowId:${this.$route.params.id}`;
}
];
},
executionQuery() {
let filter;
if (this.$route.name === "flowEdit") {
filter = `namespace:${this.$route.params.namespace} AND flowId:${this.$route.params.id}`;
return this.query + (filter ? " " + filter : "");
},
endDate() {
return new Date();
},
startDate() {
return this.$moment(this.endDate)
.add(-30, "days")
.toDate();
}
},
methods: {
onStatusChange() {
this.saveFilters()
this.loadData()
},
showTriggerDetails(trigger) {
this.flowTriggerDetails = trigger
this.$bvModal.show("modal-triggers-details")
},
triggerExecution() {
this.$store
.dispatch("execution/triggerExecution", this.$route.params)
.then(response => {
this.$router.push({
name: "execution",
params: response.data
});
return this.query + (filter ? " " + filter : "");
},
endDate() {
return new Date();
},
startDate() {
return this.$moment(this.endDate)
.add(-30, "days")
.toDate();
}
},
methods: {
onStatusChange() {
this.saveFilters()
this.loadData()
},
showTriggerDetails(trigger) {
this.flowTriggerDetails = trigger
this.$bvModal.show('modal-triggers-details')
},
triggerExecution() {
this.$store
.dispatch("execution/triggerExecution", this.$route.params)
.then(response => {
this.$router.push({
name: "execution",
params: response.data
return response.data
})
.then((execution) => {
this.$toast().success(this.$t("triggered done", {name: execution.id}));
})
},
loadData(callback) {
this.dailyReady = false;
this.$store
.dispatch("stat/daily", {
q: this.executionQuery,
startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
})
.then(() => {
this.dailyReady = true;
});
return response.data
})
.then((execution) => {
this.$toast().success(this.$t('triggered done', {name: execution.id}));
})
},
loadData(callback) {
this.dailyReady = false;
this.$store
.dispatch("stat/daily", {
this.$store.dispatch("execution/findExecutions", {
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
q: this.executionQuery,
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'),
endDate: this.$moment(this.endDate).format('YYYY-MM-DD')
})
.then(() => {
this.dailyReady = true;
});
this.$store.dispatch("execution/findExecutions", {
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
q: this.executionQuery,
sort: this.$route.query.sort,
state: this.$route.query.status
}).finally(callback);
},
durationFrom(item) {
return (+new Date() - new Date(item.state.startDate).getTime()) / 1000
},
}
};
sort: this.$route.query.sort,
state: this.$route.query.status
}).finally(callback);
},
durationFrom(item) {
return (+new Date() - new Date(item.state.startDate).getTime()) / 1000
},
}
};
</script>

View File

@@ -5,7 +5,9 @@
<thead>
<tr class="bg-light">
<th>{{ duration }}</th>
<td v-for="(date, i) in dates" :key="i">{{ date }}</td>
<td v-for="(date, i) in dates" :key="i">
{{ date }}
</td>
</tr>
</thead>
<tbody v-for="taskItem in series" :key="taskItem.id">
@@ -18,13 +20,15 @@
:target="`task-title-wrapper-${taskItem.id}`"
>
<code>{{ taskItem.name }}</code>
<span v-if="taskItem.task && taskItem.task.value"><br/>{{ taskItem.task.value }}</span>
<span v-if="taskItem.task && taskItem.task.value"><br>{{ taskItem.task.value }}</span>
</b-tooltip>
</th>
<td :colspan="dates.length">
<b-tooltip
:target="`task-progress-${taskItem.id}`"
placement="left"><span v-html="taskItem.tooltip"></span>
placement="left"
>
<span v-html="taskItem.tooltip" />
</b-tooltip>
<div
:style="{left: Math.max(1, (taskItem.start - 1)) + '%', width: taskItem.width - 1 + '%'}"
@@ -33,18 +37,19 @@
:id="`task-progress-${taskItem.id}`"
>
<div class="progress">
<div class="progress-bar"
:style="{left: taskItem.left + '%', width: (100-taskItem.left) + '%'}"
:class="'bg-' + taskItem.color + (taskItem.running ? ' progress-bar-striped' : '')"
role="progressbar">
</div>
<div
class="progress-bar"
:style="{left: taskItem.left + '%', width: (100-taskItem.left) + '%'}"
:class="'bg-' + taskItem.color + (taskItem.running ? ' progress-bar-striped' : '')"
role="progressbar"
/>
</div>
</div>
</td>
</tr>
<tr v-if="task && task.id === taskItem.id">
<td :colspan="dates.length + 1">
<log-list :task-run-id="task.id" level="TRACE"/>
<log-list :task-run-id="task.id" level="TRACE" />
</td>
</tr>
</tbody>

View File

@@ -2,9 +2,9 @@
<div v-if="execution">
<b-row class="mb-3 text-right">
<b-col>
<restart :execution="execution" @restart="restart"/>
<restart :execution="execution" @restart="restart" />
<kill :execution="execution" />
<status :status="execution.state.current"/>
<status :status="execution.state.current" />
</b-col>
</b-row>
<b-table responsive="xl" striped hover bordered :items="items" class="mb-0">
@@ -12,7 +12,8 @@
<router-link
v-if="row.item.link"
:to="{name: 'executionEdit', params: row.item.link}"
>{{ row.item.value }}
>
{{ row.item.value }}
</router-link>
<span v-else>
<span v-if="row.item.key === $t('revision')">
@@ -27,17 +28,17 @@
<div v-if="execution.trigger" class="mt-4">
<h5>{{ $t('trigger') }}</h5>
<vars :execution="execution" :data="execution.trigger"/>
<vars :execution="execution" :data="execution.trigger" />
</div>
<div v-if="execution.inputs" class="mt-4">
<h5>{{ $t('inputs') }}</h5>
<vars :execution="execution" :data="inputs"/>
<vars :execution="execution" :data="inputs" />
</div>
<div v-if="execution.variables" class="mt-4">
<h5>{{ $t('variables') }}</h5>
<vars :execution="execution" :data="execution.variables"/>
<vars :execution="execution" :data="execution.variables" />
</div>
</div>
</template>
@@ -78,8 +79,8 @@
items() {
const startTs = this.execution.state.histories[0].date;
const stopTs = this.execution.state.histories[
this.execution.state.histories.length - 1
].date;
this.execution.state.histories.length - 1
].date;
const delta = ts(stopTs) - ts(startTs);
const duration = this.$moment.duration(delta);
const humanDuration = humanizeDuration(duration);
@@ -94,8 +95,8 @@
key: this.$t("revision"),
value: this.execution.flowRevision
},
{key: this.$t("created date"), value: this.$moment(startTs).format('LLLL')},
{key: this.$t("updated date"), value: this.$moment(stopTs).format('LLLL')},
{key: this.$t("created date"), value: this.$moment(startTs).format("LLLL")},
{key: this.$t("updated date"), value: this.$moment(stopTs).format("LLLL")},
{key: this.$t("duration"), value: humanDuration},
{key: this.$t("steps"), value: stepCount}
];

View File

@@ -1,15 +1,15 @@
<template>
<span>
<b-button
@click="restart"
v-if="enabled"
:class="!isButtonGroup ? 'rounded-lg btn-info restart mr-1' : ''"
:title="$t('restart')"
>
<restart-icon/>
{{ (isButtonGroup ? '' : $t("restart")) }}
</b-button>
</span>
<span>
<b-button
@click="restart"
v-if="enabled"
:class="!isButtonGroup ? 'rounded-lg btn-info restart mr-1' : ''"
:title="$t('restart')"
>
<restart-icon />
{{ (isButtonGroup ? '' : $t("restart")) }}
</b-button>
</span>
</template>
<script>
import RestartIcon from "vue-material-design-icons/Restart";
@@ -30,7 +30,8 @@
},
task: {
type: Object,
required: false
required: false,
default: undefined
}
},
methods: {
@@ -43,9 +44,9 @@
taskId: this.task ? this.task.taskId : null
})
.then(response => {
this.$store.commit('execution/setExecution', response.data);
this.$router.push({name: 'executionEdit', params: response.data});
this.$emit('restart')
this.$store.commit("execution/setExecution", response.data);
this.$router.push({name: "executionEdit", params: response.data});
this.$emit("restart")
})
.then(() => {
this.$toast().success(this.$t("restarted"));

View File

@@ -4,36 +4,36 @@
<topology-tree
ref="topology"
v-if="execution && dataTree"
:dataTree="dataTree"
:data-tree="dataTree"
:label="getLabel"
/>
</b-col>
</b-row>
</template>
<script>
import TopologyTree from "../graph/TopologyTree";
import { mapState } from "vuex";
export default {
components: {
TopologyTree
},
computed: {
...mapState("execution", ["execution", "dataTree"])
},
created() {
if (!this.dataTree && this.execution) {
this.$store.dispatch('execution/loadTree', this.execution)
}
},
methods: {
getLabel(node) {
return node.data.taskId;
import TopologyTree from "../graph/TopologyTree";
import {mapState} from "vuex";
export default {
components: {
TopologyTree
},
update() {
if (this.$refs.topology) {
this.$refs.topology.update();
computed: {
...mapState("execution", ["execution", "dataTree"])
},
created() {
if (!this.dataTree && this.execution) {
this.$store.dispatch("execution/loadTree", this.execution)
}
},
methods: {
getLabel(node) {
return node.data.taskId;
},
update() {
if (this.$refs.topology) {
this.$refs.topology.update();
}
}
}
}
};
};
</script>

View File

@@ -3,9 +3,10 @@
v-if="isFile(value)"
target="_blank"
:href="itemUrl(value)"
><download /> {{ $t('download') }}
>
<download /> {{ $t('download') }}
</b-link>
<span v-else v-html="value"></span>
<span v-else v-html="value" />
</template>
<script>
@@ -26,11 +27,13 @@
},
props: {
value: {
type: Object,
required: true
},
execution: {
type: Object,
required: false
required: false,
default: undefined
}
}
};

View File

@@ -15,13 +15,15 @@
<template v-slot:thead-top v-if="title">
<b-tr class="top">
<b-th colspan="2">{{ title }}</b-th>
<b-th colspan="2">
{{ title }}
</b-th>
</b-tr>
</template>
<template v-slot:empty>
<div class="alert alert-info mb-0" role="alert">
{{ $t("no data current task")}}
{{ $t("no data current task") }}
</div>
</template>
@@ -46,11 +48,13 @@
},
title: {
type: String,
required: false
required: false,
default: undefined
},
execution: {
type: Object,
required: false
required: false,
default: undefined
}
},
computed: {

View File

@@ -1,17 +1,17 @@
<template>
<b-row>
<b-col>
<flow-edit v-if="flow" :prevent-route-info="true"/>
<flow-edit v-if="flow" :prevent-route-info="true" />
</b-col>
</b-row>
</template>
<script>
import { mapGetters } from "vuex";
import FlowEdit from "./FlowEdit";
export default {
components: { FlowEdit },
computed: {
...mapGetters("flow", ["flow"])
}
};
import {mapGetters} from "vuex";
import FlowEdit from "./FlowEdit";
export default {
components: {FlowEdit},
computed: {
...mapGetters("flow", ["flow"])
}
};
</script>

View File

@@ -1,64 +1,65 @@
<template>
<div class="container" v-if="flow">
<b-form v-hotkey="keymap" @submit.prevent="onSubmit">
<b-alert v-if="flow.triggers" variant="warning" show>{{$t('warning flow with triggers')}}</b-alert>
<b-alert v-if="flow.triggers" variant="warning" show>
{{ $t('warning flow with triggers') }}
</b-alert>
<b-form-group
v-for="input in flow.inputs"
:key="input.id"
:label="input.name"
label-cols-sm="2"
label-align-sm="right"
label-size="sm"
v-for="input in flow.inputs"
:key="input.id"
:label="input.name"
label-cols-sm="2"
label-align-sm="right"
label-size="sm"
>
<b-form-input
v-if="input.type === 'STRING'"
v-model="input.value"
type="text"
:required="input.required"
:placeholder="`${placeholder} ${input.name}`"
></b-form-input>
v-if="input.type === 'STRING'"
v-model="input.value"
type="text"
:required="input.required"
:placeholder="`${placeholder} ${input.name}`"
/>
<b-form-input
v-if="input.type === 'INT'"
v-model="input.value"
type="number"
step="1"
:required="input.required"
:placeholder="`${placeholder} ${input.name}`"
></b-form-input>
v-if="input.type === 'INT'"
v-model="input.value"
type="number"
step="1"
:required="input.required"
:placeholder="`${placeholder} ${input.name}`"
/>
<b-form-input
v-if="input.type === 'FLOAT'"
v-model="input.value"
type="number"
step="0.001"
:required="input.required"
:placeholder="`${placeholder} ${input.name}`"
></b-form-input>
v-if="input.type === 'FLOAT'"
v-model="input.value"
type="number"
step="0.001"
:required="input.required"
:placeholder="`${placeholder} ${input.name}`"
/>
<date-picker
v-if="input.type === 'DATETIME'"
v-model="input.value"
:required="input.required"
type="datetime"
class="w-100"
:placeholder="$t('select datetime')"
></date-picker>
v-if="input.type === 'DATETIME'"
v-model="input.value"
:required="input.required"
type="datetime"
class="w-100"
:placeholder="$t('select datetime')"
/>
<b-form-file
v-if="input.type === 'FILE'"
v-model="input.value"
:required="input.required"
:state="Boolean(input.value)"
:placeholder="$t('choose file')"
></b-form-file>
v-if="input.type === 'FILE'"
v-model="input.value"
:required="input.required"
:state="Boolean(input.value)"
:placeholder="$t('choose file')"
/>
</b-form-group>
<b-form-group class="text-right mb-0">
<b-button type="submit" variant="primary">
{{$t('launch execution')}}
<trigger title/>
{{ $t('launch execution') }}
<trigger title />
</b-button>
</b-form-group>
</b-form>
<br/>
<br>
<b-card :header="$t('triggers')" v-if="flow && flow.triggers">
<triggers />
</b-card>
@@ -79,7 +80,7 @@
},
keymap () {
return {
'ctrl+enter': this.onSubmit,
"ctrl+enter": this.onSubmit,
}
}
},
@@ -110,13 +111,13 @@
formData
})
.then(response => {
this.$store.commit('execution/setExecution', response.data)
this.$router.push({name: 'executionEdit', params: response.data})
this.$store.commit("execution/setExecution", response.data)
this.$router.push({name: "executionEdit", params: response.data})
return response.data;
})
.then((execution) => {
this.$toast().success(this.$t('triggered done', {name: execution.id}));
this.$toast().success(this.$t("triggered done", {name: execution.id}));
})
}
}

View File

@@ -3,45 +3,45 @@
<b-dropdown-item>
<router-link :to="`/flows/edit/${actionFlow.namespace}/${actionFlow.id}`">
<edit />
{{$t('edit flow')}} {{actionFlow.id}}
{{ $t('edit flow') }} {{ actionFlow.id }}
</router-link>
</b-dropdown-item>
<b-dropdown-item>
<router-link :to="`/executions/${actionFlow.namespace}/${actionFlow.id}`">
<search />
{{$tc('display flow {id} executions', null, flow)}}
{{ $tc('display flow {id} executions', null, flow) }}
</router-link>
</b-dropdown-item>
<b-dropdown-item>
<router-link :to="{name: 'flowTopology', params: actionFlow}">
<graph />
{{$t('display topology for flow')}} {{actionFlow.id}}
{{ $t('display topology for flow') }} {{ actionFlow.id }}
</router-link>
</b-dropdown-item>
</b-dropdown>
</template>
<script>
import Search from "vue-material-design-icons/Magnify";
import Edit from "vue-material-design-icons/Pencil";
import Graph from "vue-material-design-icons/Graph";
import { mapState } from "vuex";
export default {
components: {
Search,
Edit,
Graph
},
props: {
flowItem: {
type: Object,
required: false
import Search from "vue-material-design-icons/Magnify";
import Edit from "vue-material-design-icons/Pencil";
import Graph from "vue-material-design-icons/Graph";
import {mapState} from "vuex";
export default {
components: {
Search,
Edit,
Graph
},
props: {
flowItem: {
type: Object,
default: undefined,
}
},
computed: {
...mapState("flow", ["flow"]),
actionFlow() {
return this.flow || this.flowItem;
}
}
},
computed: {
...mapState("flow", ["flow"]),
actionFlow() {
return this.flow || this.flowItem;
}
}
};
};
</script>

View File

@@ -1,17 +1,17 @@
<template>
<div>
<editor @onSave="save" v-model="content" lang="yaml"></editor>
<editor @onSave="save" v-model="content" lang="yaml" />
<bottom-line v-if="canSave || canDelete">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<b-button class="btn-danger" v-if="canDelete" @click="deleteFile">
<delete />
<span>{{$t('delete')}}</span>
<span>{{ $t('delete') }}</span>
</b-button>
<b-button @click="save" v-if="canSave">
<content-save />
<span>{{$t('save')}}</span>
<span>{{ $t('save') }}</span>
</b-button>
</li>
</ul>
@@ -20,21 +20,21 @@
</template>
<script>
import flowTemplateEdit from "../../mixins/flowTemplateEdit";
import { mapGetters } from "vuex";
import flowTemplateEdit from "../../mixins/flowTemplateEdit";
import {mapGetters} from "vuex";
export default {
mixins: [flowTemplateEdit],
data() {
return {
dataType: "flow",
};
},
computed: {
...mapGetters("flow", ["flow"]),
},
created() {
this.loadFile();
},
};
export default {
mixins: [flowTemplateEdit],
data() {
return {
dataType: "flow",
};
},
computed: {
...mapGetters("flow", ["flow"]),
},
created() {
this.loadFile();
},
};
</script>

View File

@@ -20,139 +20,139 @@
</div>
</template>
<script>
import Overview from "./Overview";
import Schedule from "./Schedule";
import DataSource from "./DataSource";
import Revisions from "./Revisions";
import ExecutionConfiguration from "./ExecutionConfiguration";
import BottomLine from "../layout/BottomLine";
import FlowActions from "./FlowActions";
import Logs from "../logs/LogsWrapper";
import Executions from "../executions/Executions";
import RouteContext from "../../mixins/routeContext";
import { mapState } from "vuex";
import permission from "../../models/permission";
import action from "../../models/action";
import Overview from "./Overview";
import Schedule from "./Schedule";
import DataSource from "./DataSource";
import Revisions from "./Revisions";
import ExecutionConfiguration from "./ExecutionConfiguration";
import BottomLine from "../layout/BottomLine";
import FlowActions from "./FlowActions";
import Logs from "../logs/LogsWrapper";
import Executions from "../executions/Executions";
import RouteContext from "../../mixins/routeContext";
import {mapState} from "vuex";
import permission from "../../models/permission";
import action from "../../models/action";
export default {
mixins: [RouteContext],
components: {
Overview,
Schedule,
BottomLine,
DataSource,
FlowActions,
Executions,
ExecutionConfiguration,
Revisions,
Logs
},
created() {
this.$store.dispatch("flow/loadFlow", this.$route.params).then(() => {
if (this.flow) {
this.$store.dispatch("flow/loadTree", this.flow);
}
});
},
methods: {
setTab(tab) {
this.$router.push({
name: "flowEdit",
params: this.$route.params,
query: { tab }
});
}
},
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
routeInfo() {
return {
title: this.$route.params.id,
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList"
}
},
{
label: this.$route.params.namespace,
link: {
name: "flowsList",
query: {
namespace: this.$route.params.namespace
}
}
},
{
label: this.$route.params.id,
link: {
name: "flowEdit",
params: {
namespace: this.$route.params.namespace,
id: this.$route.params.id
}
}
}
]
};
export default {
mixins: [RouteContext],
components: {
Overview,
Schedule,
BottomLine,
DataSource,
FlowActions,
Executions,
ExecutionConfiguration,
Revisions,
Logs
},
tabs() {
const title = title => this.$t(title);
const tabs = [
{
tab: "overview",
title: title("overview")
},
];
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.READ, this.flow.namespace)) {
tabs.push({
tab: "executions",
title: title("executions")
created() {
this.$store.dispatch("flow/loadFlow", this.$route.params).then(() => {
if (this.flow) {
this.$store.dispatch("flow/loadTree", this.flow);
}
});
},
methods: {
setTab(tab) {
this.$router.push({
name: "flowEdit",
params: this.$route.params,
query: {tab}
});
}
},
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
routeInfo() {
return {
title: this.$route.params.id,
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList"
}
},
{
label: this.$route.params.namespace,
link: {
name: "flowsList",
query: {
namespace: this.$route.params.namespace
}
}
},
{
label: this.$route.params.id,
link: {
name: "flowEdit",
params: {
namespace: this.$route.params.namespace,
id: this.$route.params.id
}
}
}
]
};
},
tabs() {
const title = title => this.$t(title);
const tabs = [
{
tab: "overview",
title: title("overview")
},
];
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.CREATE, this.flow.namespace)) {
tabs.push({
tab: "execution-configuration",
title: title("launch execution")
});
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.READ, this.flow.namespace)) {
tabs.push({
tab: "executions",
title: title("executions")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.CREATE, this.flow.namespace)) {
tabs.push({
tab: "execution-configuration",
title: title("launch execution")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.UPDATE, this.flow.namespace)) {
tabs.push({
tab: "data-source",
title: title("source"),
class: "p-0"
});
tabs.push({
tab: "schedule",
title: title("schedule"),
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "revisions",
title: title("revisions")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "logs",
title: title("logs")
});
}
return tabs;
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.UPDATE, this.flow.namespace)) {
tabs.push({
tab: "data-source",
title: title("source"),
class: "p-0"
});
tabs.push({
tab: "schedule",
title: title("schedule"),
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "revisions",
title: title("revisions")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "logs",
title: title("logs")
});
}
return tabs;
},
destroyed () {
this.$store.commit("flow/setFlow", undefined)
}
},
destroyed () {
this.$store.commit('flow/setFlow', undefined)
}
};
};
</script>

View File

@@ -37,7 +37,7 @@
show-empty
>
<template #empty>
<span class="text-black-50">{{$t('no result')}}</span>
<span class="text-black-50">{{ $t('no result') }}</span>
</template>
<template v-slot:cell(actions)="row">
@@ -63,20 +63,21 @@
<template v-slot:cell(id)="row">
<router-link
:to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.id}, query:{tab: 'executions'}}"
>{{row.item.id}}</router-link>
>
{{ row.item.id }}
</router-link>
&nbsp;<markdown-tooltip :id="row.item.namespace + '-' + row.item.id" :description="row.item.description" />
</template>
<template v-slot:cell(triggers)="row">
<trigger-avatar @showTriggerDetails="showTriggerDetails" :flow="row.item"/>
<trigger-avatar @showTriggerDetails="showTriggerDetails" :flow="row.item" />
</template>
</b-table>
</template>
</data-table>
</div>
<trigger-details-modal :trigger="flowTriggerDetails"/>
<trigger-details-modal :trigger="flowTriggerDetails" />
<bottom-line v-if="user && user.hasAnyAction(permission.FLOW, action.CREATE)">
<ul class="navbar-nav ml-auto">
@@ -84,7 +85,7 @@
<router-link :to="{name: 'flowsAdd'}">
<b-button variant="primary">
<plus />
{{$t('create')}}
{{ $t('create') }}
</b-button>
</router-link>
</li>
@@ -94,156 +95,156 @@
</template>
<script>
import { mapState } from "vuex";
import permission from "../../models/permission";
import action from "../../models/action";
import NamespaceSelect from "../namespace/NamespaceSelect";
import Plus from "vue-material-design-icons/Plus";
import Eye from "vue-material-design-icons/Eye";
import BottomLine from "../layout/BottomLine";
import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions";
import DataTable from "../layout/DataTable";
import SearchField from "../layout/SearchField";
import StateChart from "../stats/StateChart";
import DurationChart from "../stats/DurationChart";
import StateGlobalChart from "../stats/StateGlobalChart";
import TriggerDetailsModal from "./TriggerDetailsModal";
import TriggerAvatar from "./TriggerAvatar";
import MarkdownTooltip from "../layout/MarkdownTooltip"
import {mapState} from "vuex";
import permission from "../../models/permission";
import action from "../../models/action";
import NamespaceSelect from "../namespace/NamespaceSelect";
import Plus from "vue-material-design-icons/Plus";
import Eye from "vue-material-design-icons/Eye";
import BottomLine from "../layout/BottomLine";
import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions";
import DataTable from "../layout/DataTable";
import SearchField from "../layout/SearchField";
import StateChart from "../stats/StateChart";
import DurationChart from "../stats/DurationChart";
import StateGlobalChart from "../stats/StateGlobalChart";
import TriggerDetailsModal from "./TriggerDetailsModal";
import TriggerAvatar from "./TriggerAvatar";
import MarkdownTooltip from "../layout/MarkdownTooltip"
export default {
mixins: [RouteContext, DataTableActions],
components: {
NamespaceSelect,
BottomLine,
Plus,
Eye,
DataTable,
SearchField,
StateChart,
DurationChart,
StateGlobalChart,
TriggerDetailsModal,
TriggerAvatar,
MarkdownTooltip
},
data() {
return {
dataType: "flow",
permission: permission,
action: action,
dailyGroupByFlowReady: false,
dailyReady: false,
flowTriggerDetails: undefined
};
},
computed: {
...mapState("flow", ["flows", "total"]),
...mapState("stat", ["dailyGroupByFlow", "daily"]),
...mapState("auth", ["user"]),
fields() {
const title = title => {
return this.$t(title);
export default {
mixins: [RouteContext, DataTableActions],
components: {
NamespaceSelect,
BottomLine,
Plus,
Eye,
DataTable,
SearchField,
StateChart,
DurationChart,
StateGlobalChart,
TriggerDetailsModal,
TriggerAvatar,
MarkdownTooltip
},
data() {
return {
dataType: "flow",
permission: permission,
action: action,
dailyGroupByFlowReady: false,
dailyReady: false,
flowTriggerDetails: undefined
};
return [
{
key: "id",
label: title("flow"),
sortable: true
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "state",
label: title("execution statistics"),
sortable: false,
class: "row-graph"
},
{
key: "duration",
label: title("duration"),
sortable: false,
class: "row-graph"
},
{
key: "triggers",
label: title("triggers"),
class: "shrink"
},
{
key: "actions",
label: "",
class: "row-action"
}
];
},
endDate() {
return new Date();
},
startDate() {
return this.$moment(this.endDate)
.add(-30, "days")
.toDate();
}
},
methods: {
showTriggerDetails(trigger) {
this.flowTriggerDetails = trigger
this.$bvModal.show('modal-triggers-details')
},
chartData(row) {
if (this.dailyGroupByFlow && this.dailyGroupByFlow[row.item.namespace] && this.dailyGroupByFlow[row.item.namespace][row.item.id]) {
return this.dailyGroupByFlow[row.item.namespace][row.item.id];
} else {
return [];
computed: {
...mapState("flow", ["flows", "total"]),
...mapState("stat", ["dailyGroupByFlow", "daily"]),
...mapState("auth", ["user"]),
fields() {
const title = title => {
return this.$t(title);
};
return [
{
key: "id",
label: title("flow"),
sortable: true
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "state",
label: title("execution statistics"),
sortable: false,
class: "row-graph"
},
{
key: "duration",
label: title("duration"),
sortable: false,
class: "row-graph"
},
{
key: "triggers",
label: title("triggers"),
class: "shrink"
},
{
key: "actions",
label: "",
class: "row-action"
}
];
},
endDate() {
return new Date();
},
startDate() {
return this.$moment(this.endDate)
.add(-30, "days")
.toDate();
}
},
loadData(callback) {
this.dailyReady = false;
this.$store
.dispatch("stat/daily", {
q: this.query.replace("id:" , "flowId:"),
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'),
endDate: this.$moment(this.endDate).format('YYYY-MM-DD')
})
.then(() => {
this.dailyReady = true;
});
methods: {
showTriggerDetails(trigger) {
this.flowTriggerDetails = trigger
this.$bvModal.show("modal-triggers-details")
},
chartData(row) {
if (this.dailyGroupByFlow && this.dailyGroupByFlow[row.item.namespace] && this.dailyGroupByFlow[row.item.namespace][row.item.id]) {
return this.dailyGroupByFlow[row.item.namespace][row.item.id];
} else {
return [];
}
},
loadData(callback) {
this.dailyReady = false;
this.$store
.dispatch("stat/daily", {
q: this.query.replace("id:" , "flowId:"),
startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
})
.then(() => {
this.dailyReady = true;
});
this.$store
.dispatch("flow/findFlows", {
q: this.query,
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
sort: this.$route.query.sort
})
.then(flows => {
this.dailyGroupByFlowReady = false;
callback();
this.$store
.dispatch("flow/findFlows", {
q: this.query,
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
sort: this.$route.query.sort
})
.then(flows => {
this.dailyGroupByFlowReady = false;
callback();
if (flows.results && flows.results.length > 0) {
let query = "((" + flows.results
.map(flow => "flowId:" + flow.id + " AND namespace:" + flow.namespace)
.join(") OR (") + "))"
if (flows.results && flows.results.length > 0) {
let query = "((" + flows.results
.map(flow => "flowId:" + flow.id + " AND namespace:" + flow.namespace)
.join(") OR (") + "))"
this.$store
.dispatch("stat/dailyGroupByFlow", {
q: query,
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'),
endDate: this.$moment(this.endDate).format('YYYY-MM-DD')
})
.then(() => {
this.dailyGroupByFlowReady = true
})
}
})
this.$store
.dispatch("stat/dailyGroupByFlow", {
q: query,
startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
})
.then(() => {
this.dailyGroupByFlowReady = true
})
}
})
}
}
}
};
};
</script>
<style lang="scss" scoped>
@import "../../styles/_variable.scss";

View File

@@ -2,36 +2,36 @@
<div>
<b-row class="topology-wrapper">
<b-col>
<topology-tree :isFlow="true" v-if="flow && dataTree" :dataTree="dataTree" :label="getLabel"/>
<topology-tree :is-flow="true" v-if="flow && dataTree" :data-tree="dataTree" :label="getLabel" />
</b-col>
</b-row>
<b-row>
<b-col>
<task-details/>
<task-details />
</b-col>
</b-row>
</div>
</template>
<script>
import { mapState } from "vuex";
import TopologyTree from "../graph/TopologyTree";
import TaskDetails from "./TaskDetails";
import {mapState} from "vuex";
import TopologyTree from "../graph/TopologyTree";
import TaskDetails from "./TaskDetails";
export default {
components: {
TopologyTree,
TaskDetails
},
computed: {
...mapState("flow", ["flow", "dataTree"]),
},
methods: {
getLabel (node) {
const id = node.data.id;
return `${id.substr(0, 25)}${id.length > 25 ? "..." : ""}`;
export default {
components: {
TopologyTree,
TaskDetails
},
computed: {
...mapState("flow", ["flow", "dataTree"]),
},
methods: {
getLabel (node) {
const id = node.data.id;
return `${id.substr(0, 25)}${id.length > 25 ? "..." : ""}`;
}
}
}
};
};
</script>
<style lang="scss" scoped>
.topology-wrapper {

View File

@@ -2,20 +2,20 @@
<div v-if="revisions && revisions.length > 1">
<b-row>
<b-col md="12">
<b-form-select v-model="displayType" :options="displayTypes"></b-form-select>
<hr />
<b-form-select v-model="displayType" :options="displayTypes" />
<hr>
</b-col>
<b-col md="6">
<b-form-select v-model="revisionLeft" :options="options"></b-form-select>
<b-form-select v-model="revisionLeft" :options="options" />
</b-col>
<b-col md="6">
<b-form-select v-model="revisionRight" :options="options"></b-form-select>
<b-form-select v-model="revisionRight" :options="options" />
</b-col>
<b-col md="12">
<br />
<br>
<code-diff
:outputFormat="displayType"
:output-format="displayType"
:old-string="revisionLeftText"
:new-string="revisionRightText"
:context="10"
@@ -24,98 +24,100 @@
</b-row>
</div>
<div v-else>
<b-alert class="mb-0" show>{{$t('no revisions found')}}</b-alert>
<b-alert class="mb-0" show>
{{ $t('no revisions found') }}
</b-alert>
</div>
</template>
<script>
import { mapState } from "vuex";
import YamlUtils from "../../utils/yamlUtils";
import CodeDiff from "vue-code-diff";
import {mapState} from "vuex";
import YamlUtils from "../../utils/yamlUtils";
import CodeDiff from "vue-code-diff";
export default {
components: { CodeDiff },
created() {
this.$store
.dispatch("flow/loadRevisions", this.$route.params)
.then(() => {
const revisionLength = this.revisions.length;
if (revisionLength > 0) {
this.revisionRight = revisionLength - 1;
}
if (revisionLength > 1) {
this.revisionLeft = revisionLength - 2;
}
if (this.$route.query.revisionRight) {
this.revisionRight = this.revisionIndex(
this.$route.query.revisionRight
);
if (
!this.$route.query.revisionLeft &&
this.revisionRight > 0
) {
this.revisionLeft = this.revisions.length - 1;
export default {
components: {CodeDiff},
created() {
this.$store
.dispatch("flow/loadRevisions", this.$route.params)
.then(() => {
const revisionLength = this.revisions.length;
if (revisionLength > 0) {
this.revisionRight = revisionLength - 1;
}
if (revisionLength > 1) {
this.revisionLeft = revisionLength - 2;
}
if (this.$route.query.revisionRight) {
this.revisionRight = this.revisionIndex(
this.$route.query.revisionRight
);
if (
!this.$route.query.revisionLeft &&
this.revisionRight > 0
) {
this.revisionLeft = this.revisions.length - 1;
}
}
if (this.$route.query.revisionLeft) {
this.revisionLeft = this.revisionIndex(
this.$route.query.revisionLeft
);
}
});
},
methods: {
revisionIndex(revision) {
const rev = parseInt(revision);
for (let i = 0; i < this.revisions.length; i++) {
if (rev === this.revisions[i].revision) {
return i;
}
}
if (this.$route.query.revisionLeft) {
this.revisionLeft = this.revisionIndex(
this.$route.query.revisionLeft
);
},
},
computed: {
...mapState("flow", ["revisions"]),
options() {
return (this.revisions || []).map((revision, x) => {
return {
value: x,
text: revision.revision,
};
});
},
revisionLeftText() {
if (this.revisionLeft === undefined) {
return "";
}
});
},
methods: {
revisionIndex(revision) {
const rev = parseInt(revision);
for (let i = 0; i < this.revisions.length; i++) {
if (rev === this.revisions[i].revision) {
return i;
return YamlUtils.stringify(this.revisions[this.revisionLeft]);
},
revisionRightText() {
if (this.revisionRight === undefined) {
return "";
}
}
return YamlUtils.stringify(this.revisions[this.revisionRight]);
},
diff() {
const linesLeft = this.revisionLeftText.split("\n");
const linesRight = this.revisionRightText.split("\n");
const minLength = Math.min(linesLeft.length, linesRight.length);
const diff = [];
for (let i = 0; i < minLength; i++) {
diff.push(linesLeft[i] === linesRight[i] ? "" : "≠");
}
return diff.join("\n");
},
},
},
computed: {
...mapState("flow", ["revisions"]),
options() {
return (this.revisions || []).map((revision, x) => {
return {
value: x,
text: revision.revision,
};
});
data() {
return {
revisionLeft: 0,
revisionRight: 0,
displayType: "side-by-side",
displayTypes: [
{value: "side-by-side", text: "side-by-side"},
{value: "line-by-line", text: "line-by-line"},
],
};
},
revisionLeftText() {
if (this.revisionLeft === undefined) {
return "";
}
return YamlUtils.stringify(this.revisions[this.revisionLeft]);
},
revisionRightText() {
if (this.revisionRight === undefined) {
return "";
}
return YamlUtils.stringify(this.revisions[this.revisionRight]);
},
diff() {
const linesLeft = this.revisionLeftText.split("\n");
const linesRight = this.revisionRightText.split("\n");
const minLength = Math.min(linesLeft.length, linesRight.length);
const diff = [];
for (let i = 0; i < minLength; i++) {
diff.push(linesLeft[i] === linesRight[i] ? "" : "≠");
}
return diff.join("\n");
},
},
data() {
return {
revisionLeft: 0,
revisionRight: 0,
displayType: "side-by-side",
displayTypes: [
{ value: "side-by-side", text: "side-by-side" },
{ value: "line-by-line", text: "line-by-line" },
],
};
},
};
};
</script>

View File

@@ -6,21 +6,29 @@
@set="set"
:schedule="schedule"
:index="x"
v-for="(schedule, x) in (flow.triggers || []).filter(r => r.type === 'org.kestra.core.models.triggers.types.Schedule')"
v-for="(schedule, x) in (flow.triggers || []).filter(
(r) =>
r.type ===
'org.kestra.core.models.triggers.types.Schedule'
)"
:key="x"
/>
</b-list-group>
<bottom-line v-if="canSave">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<b-button variant="primary" @click="addSchedule" v-if="canSave">
<b-button
variant="primary"
@click="addSchedule"
v-if="canSave"
>
<plus />
{{ $t('add schedule') }}
{{ $t("add schedule") }}
</b-button>
<b-button @click="save" v-if="canSave">
<content-save />
<span>{{$t('save')}}</span>
<span>{{ $t("save") }}</span>
</b-button>
</li>
</ul>
@@ -28,46 +36,49 @@
</div>
</template>
<script>
import { mapState } from "vuex";
import ContentSave from "vue-material-design-icons/ContentSave";
import Plus from "vue-material-design-icons/Plus";
import ScheduleItem from "./ScheduleItem";
import BottomLine from "../layout/BottomLine";
import { canSaveFlowTemplate, saveFlowTemplate } from "../../utils/flowTemplate";
import {mapState} from "vuex";
import ContentSave from "vue-material-design-icons/ContentSave";
import Plus from "vue-material-design-icons/Plus";
import ScheduleItem from "./ScheduleItem";
import BottomLine from "../layout/BottomLine";
import {
canSaveFlowTemplate,
saveFlowTemplate,
} from "../../utils/flowTemplate";
export default {
components: {
Plus,
ContentSave,
ScheduleItem,
BottomLine
},
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
canSave() {
return canSaveFlowTemplate(true, this.user, this.flow, "flow");
}
},
methods: {
save() {
saveFlowTemplate(this, this.flow, "flow");
export default {
components: {
Plus,
ContentSave,
ScheduleItem,
BottomLine,
},
set(index, schedule) {
this.$store.commit("flow/setTrigger", {index, trigger: schedule});
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
canSave() {
return canSaveFlowTemplate(true, this.user, this.flow, "flow");
},
},
remove(index) {
this.$store.commit("flow/removeTrigger", index);
methods: {
save() {
saveFlowTemplate(this, this.flow, "flow");
},
set(index, schedule) {
this.$store.commit("flow/setTrigger", {index, trigger: schedule});
},
remove(index) {
this.$store.commit("flow/removeTrigger", index);
},
addSchedule() {
this.$store.commit("flow/addTrigger", {
id: "schedule",
cron: "0 4 * * 1,4",
type: "org.kestra.core.models.triggers.types.Schedule",
});
},
},
addSchedule() {
this.$store.commit("flow/addTrigger", {
id: "schedule",
cron: "0 4 * * 1,4",
type: "org.kestra.core.models.triggers.types.Schedule",
});
}
}
};
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,24 +1,29 @@
<template>
<b-list-group-item>
<b-form-group label-cols-sm="3" label-cols-lg="2" :label="$t('id')" :label-for="'input-id-' + index">
<b-form-input required :id="'input-id-' + index" v-model="schedule.id"></b-form-input>
<b-form-input required :id="'input-id-' + index" v-model="schedule.id" />
</b-form-group>
<b-form-group label-cols-sm="3" label-cols-lg="2"
:label-for="'input-cron-' + index"
:state="isValid">
<b-form-group
label-cols-sm="3"
label-cols-lg="2"
:label-for="'input-cron-' + index"
:state="isValid"
>
<template v-slot:label>
{{ $t('schedules.cron.expression')}}
{{ $t('schedules.cron.expression') }}
<b-link class="text-body" :id="'tooltip-' + index">
<help />
</b-link>
<b-tooltip :target="'tooltip-' + index" placement="bottom">
<div v-if="isValid">
<p class="font-weight-bold">3 Next occurences</p>
<p class="font-weight-bold">
3 Next occurences
</p>
<span v-if="occurences.length">
<span v-for="(occurence, x) in occurences" :key="x">{{occurence | date('LLL:ss')}}<br /></span>
<span v-for="(occurence, x) in occurences" :key="x">{{ occurence | date('LLL:ss') }}<br></span>
</span>
</div>
<span v-else>
@@ -27,7 +32,7 @@
</b-tooltip>
</template>
<b-form-input required :id="'input-cron-' + index" v-model="schedule.cron"></b-form-input>
<b-form-input required :id="'input-cron-' + index" v-model="schedule.cron" />
<b-form-invalid-feedback>
Enter at least 3 letters
@@ -37,95 +42,98 @@
</b-form-group>
<b-form-group label-cols-sm="3" label-cols-lg="2" :label="$t('schedules.cron.backfilll')"
:label-for="'input-' + index">
<b-form-group
label-cols-sm="3"
label-cols-lg="2"
:label="$t('schedules.cron.backfilll')"
:label-for="'input-' + index"
>
<date-picker
v-model="backfillStart"
:required="false"
type="datetime"
:id="'input-' + index"
></date-picker>
/>
</b-form-group>
<b-form-group class="mb-0 text-right">
<b-btn variant="danger" @click="remove">
<delete/>
<delete />
Delete
</b-btn>
</b-form-group>
</b-list-group-item>
</template>
<script>
const cronstrue = require("cronstrue/i18n");
const cronParser = require("cron-parser");
import Delete from "vue-material-design-icons/Delete";
import Help from "vue-material-design-icons/HelpBox";
import DatePicker from "vue2-datepicker";
const cronstrue = require("cronstrue/i18n");
const cronParser = require("cron-parser");
import Delete from "vue-material-design-icons/Delete";
import Help from "vue-material-design-icons/HelpBox";
import DatePicker from "vue2-datepicker";
export default {
components: {
Delete,
Help,
DatePicker
},
props: {
schedule: {
type: Object,
required: true
export default {
components: {
Delete,
Help,
DatePicker
},
index: {
type: Number,
required: true
}
},
computed: {
backfillStart: {
get: function () {
return this.schedule.backfill && this.schedule.backfill.start !== undefined ?
this.$moment(this.schedule.backfill.start).toDate() :
undefined;
props: {
schedule: {
type: Object,
required: true
},
set: function (val) {
let current = this.schedule;
index: {
type: Number,
required: true
}
},
if (val) {
current.backfill = {"start": this.$moment(val).format()};
} else {
delete current.backfill;
computed: {
backfillStart: {
get: function () {
return this.schedule.backfill && this.schedule.backfill.start !== undefined ?
this.$moment(this.schedule.backfill.start).toDate() :
undefined;
},
set: function (val) {
let current = this.schedule;
if (val) {
current.backfill = {"start": this.$moment(val).format()};
} else {
delete current.backfill;
}
this.$emit("set", this.index, current);
}
},
occurences() {
const occurences = [];
if (!this.isValid) {
return occurences;
}
const interval = cronParser.parseExpression(this.schedule.cron);
for (let i = 0; i < 3; i++) {
occurences.push(interval.next().toDate());
}
this.$emit("set", this.index, current);
}
},
occurences() {
const occurences = [];
if (!this.isValid) {
return occurences;
}
const interval = cronParser.parseExpression(this.schedule.cron);
for (let i = 0; i < 3; i++) {
occurences.push(interval.next().toDate());
}
return occurences;
},
cronHumanReadable() {
const locale = localStorage.getItem("lang") || "en";
try {
return cronstrue.toString(this.schedule.cron, { locale });
} catch {
return this.$t("schedules.cron.invalid");
},
cronHumanReadable() {
const locale = localStorage.getItem("lang") || "en";
try {
return cronstrue.toString(this.schedule.cron, {locale});
} catch {
return this.$t("schedules.cron.invalid");
}
},
isValid() {
return require("cron-validator").isValidCron(this.schedule.cron);
}
},
isValid() {
return require("cron-validator").isValidCron(this.schedule.cron);
methods: {
remove() {
this.$emit("remove", this.index);
}
}
},
methods: {
remove() {
this.$emit("remove", this.index);
}
}
};
};
</script>

View File

@@ -10,26 +10,26 @@
</b-card>
</template>
<script>
import YamlUtils from "../../utils/yamlUtils";
import { mapState } from "vuex";
export default {
computed: {
...mapState("graph", ["node"]),
task() {
return this.node && this.node.task;
},
items() {
const items = [];
for (const property in this.task) {
const v = this.task[property];
const value =
typeof v === "object"
? `<pre>${YamlUtils.stringify(v)}</pre>`
: v;
items.push({ property, value });
import YamlUtils from "../../utils/yamlUtils";
import {mapState} from "vuex";
export default {
computed: {
...mapState("graph", ["node"]),
task() {
return this.node && this.node.task;
},
items() {
const items = [];
for (const property in this.task) {
const v = this.task[property];
const value =
typeof v === "object"
? `<pre>${YamlUtils.stringify(v)}</pre>`
: v;
items.push({property, value});
}
return items;
}
return items;
}
}
};
};
</script>

View File

@@ -7,51 +7,50 @@
:text="name(trigger)"
button
@click="showTriggerDetails(trigger)"
>
</b-avatar>
/>
</span>
</div>
</template>
<script>
export default {
props: {
flow: {
type: Object,
default: () => undefined,
export default {
props: {
flow: {
type: Object,
default: () => undefined,
},
execution: {
type: Object,
default: () => undefined,
},
},
execution: {
type: Object,
default: () => undefined,
},
},
components: {},
methods: {
showTriggerDetails(trigger) {
this.$emit("showTriggerDetails", trigger);
},
uid(trigger) {
return (this.flow ? this.flow.namespace + "-" + this.flow.id : this.execution.namespace + "-" + this.execution.flowId) + '-' + trigger.id
},
name(trigger) {
let split = trigger.id.split(".");
components: {},
methods: {
showTriggerDetails(trigger) {
this.$emit("showTriggerDetails", trigger);
},
uid(trigger) {
return (this.flow ? this.flow.namespace + "-" + this.flow.id : this.execution.namespace + "-" + this.execution.flowId) + "-" + trigger.id
},
name(trigger) {
let split = trigger.id.split(".");
return split[split.length - 1].substr(0, 1).toUpperCase();
return split[split.length - 1].substr(0, 1).toUpperCase();
},
},
},
computed: {
triggers() {
if (this.flow && this.flow.triggers) {
return this.flow.triggers
} else if (this.execution && this.execution.trigger) {
return [this.execution.trigger]
} else {
return []
computed: {
triggers() {
if (this.flow && this.flow.triggers) {
return this.flow.triggers
} else if (this.execution && this.execution.trigger) {
return [this.execution.trigger]
} else {
return []
}
}
}
}
};
};
</script>
<style lang="scss" scoped>

View File

@@ -11,25 +11,25 @@
</b-modal>
</template>
<script>
import Vars from "../executions/Vars";
import Markdown from "../../utils/markdown";
import Vars from "../executions/Vars";
import Markdown from "../../utils/markdown";
export default {
components: { Vars },
props: {
trigger: {
type: Object,
default: () => undefined
export default {
components: {Vars},
props: {
trigger: {
type: Object,
default: () => undefined
},
},
},
computed: {
triggerData() {
if (this.trigger.description) {
return {...this.trigger, description: Markdown.render(this.trigger.description)}
}
computed: {
triggerData() {
if (this.trigger.description) {
return {...this.trigger, description: Markdown.render(this.trigger.description)}
}
return this.trigger
return this.trigger
}
}
}
};
};
</script>

View File

@@ -21,19 +21,19 @@
role="tabpanel"
>
<b-card-body>
<vars :data="trigger"/>
<vars :data="trigger" />
</b-card-body>
</b-collapse>
</b-card>
</div>
</template>
<script>
import { mapState } from "vuex";
import Vars from '../executions/Vars'
export default {
components: { Vars },
computed: {
...mapState("flow", ["flow"]),
}
};
import {mapState} from "vuex";
import Vars from "../executions/Vars"
export default {
components: {Vars},
computed: {
...mapState("flow", ["flow"]),
}
};
</script>

View File

@@ -16,7 +16,9 @@
/>
</b-breadcrumb>
<div>
<b-tooltip placement="left" target="graph-orientation">{{$t('graph orientation')}}</b-tooltip>
<b-tooltip placement="left" target="graph-orientation">
{{ $t('graph orientation') }}
</b-tooltip>
<b-btn size="sm" @click="toggleOrientation" id="graph-orientation">
<arrow-collapse-down v-if="orientation" />
<arrow-collapse-right v-else />
@@ -24,7 +26,7 @@
</div>
</div>
<div :class="{hide: !ready}" class="wrapper" ref="wrapper"></div>
<div :class="{hide: !ready}" class="wrapper" ref="wrapper" />
<div class="hidden">
<tree-node
@onFilterGroup="onFilterGroup"
@@ -32,13 +34,13 @@
v-for="node in filteredDataTree"
:key="slug(node)"
:n="node"
:isFlow="isFlow"
:is-flow="isFlow"
/>
<tree-node
:ref="`node-${slug(virtualRootNode)}`"
v-if="virtualRootNode"
:n="virtualRootNode"
:isFlow="isFlow"
:is-flow="isFlow"
/>
</div>
@@ -48,290 +50,290 @@
</div>
</template>
<script>
const dagreD3 = require("dagre-d3");
import TreeNode from "./TreeNode";
import * as d3 from "d3";
import ArrowCollapseRight from "vue-material-design-icons/ArrowCollapseRight";
import ArrowCollapseDown from "vue-material-design-icons/ArrowCollapseDown";
import VectorCircle from "vue-material-design-icons/Circle";
const parentHash = node => {
if (node.parent) {
const parent = node.parent[0];
return nodeHash(parent)
} else {
return undefined;
const dagreD3 = require("dagre-d3");
import TreeNode from "./TreeNode";
import * as d3 from "d3";
import ArrowCollapseRight from "vue-material-design-icons/ArrowCollapseRight";
import ArrowCollapseDown from "vue-material-design-icons/ArrowCollapseDown";
import VectorCircle from "vue-material-design-icons/Circle";
const parentHash = node => {
if (node.parent) {
const parent = node.parent[0];
return nodeHash(parent)
} else {
return undefined;
}
};
const nodeHash = node => {
return (node.id + (node.value ? "-" + node.value : "")).hashCode();
}
};
const nodeHash = node => {
return (node.id + (node.value ? "-" + node.value : "")).hashCode();
}
export default {
components: {
TreeNode,
ArrowCollapseDown,
ArrowCollapseRight,
VectorCircle
},
props: {
dataTree: {
type: Array,
required: true
export default {
components: {
TreeNode,
ArrowCollapseDown,
ArrowCollapseRight,
VectorCircle
},
label: {
type: Function,
required: true
props: {
dataTree: {
type: Array,
required: true
},
label: {
type: Function,
required: true
},
isFlow: {
type: Boolean,
default: false
}
},
isFlow: {
type: Boolean,
default: false
}
},
data() {
return {
ready: false,
filterGroup: undefined,
orientation: true,
zoom: undefined,
filteredDataTree: undefined,
virtualRootNode: undefined
};
},
watch: {
dataTree() {
this.generateGraph();
data() {
return {
ready: false,
filterGroup: undefined,
orientation: true,
zoom: undefined,
filteredDataTree: undefined,
virtualRootNode: undefined
};
},
$route() {
if (this.$route.query.filter != this.filterGroup) {
this.filterGroup = this.$route.query.filter;
watch: {
dataTree() {
this.generateGraph();
},
$route() {
if (this.$route.query.filter != this.filterGroup) {
this.filterGroup = this.$route.query.filter;
this.generateGraph();
}
}
}
},
created() {
this.orientation = localStorage.getItem("topology-orientation") === "1";
if (this.$route.query.filter) {
this.filterGroup = this.$route.query.filter;
}
},
mounted() {
this.generateGraph();
},
methods: {
toggleOrientation() {
this.orientation = !this.orientation;
localStorage.setItem(
"topology-orientation",
this.orientation ? 1 : 0
);
},
created() {
this.orientation = localStorage.getItem("topology-orientation") === "1";
if (this.$route.query.filter) {
this.filterGroup = this.$route.query.filter;
}
},
mounted() {
this.generateGraph();
},
getVirtualRootNode() {
return this.filterGroup
? {
task: {
id: this.filterGroup
}
}
: undefined;
},
generateGraph() {
this.filteredDataTree = this.getFilteredDataTree();
this.virtualRootNode = this.getVirtualRootNode();
// Create the input graph
const arrowColor = "#ccc";
if (this.zoom) {
this.zoom.on("zoom", null);
}
this.$refs.wrapper.innerHTML =
'<svg id="svg-canvas" width="100%" style="min-height:800px"/>';
const g = new dagreD3.graphlib.Graph({
compound: true,
multigraph: true
})
.setGraph({})
.setDefaultEdgeLabel(function() {
return {};
});
methods: {
toggleOrientation() {
this.orientation = !this.orientation;
localStorage.setItem(
"topology-orientation",
this.orientation ? 1 : 0
);
this.generateGraph();
},
getVirtualRootNode() {
return this.filterGroup
? {
task: {
id: this.filterGroup
}
}
: undefined;
},
generateGraph() {
this.filteredDataTree = this.getFilteredDataTree();
this.virtualRootNode = this.getVirtualRootNode();
// Create the input graph
const arrowColor = "#ccc";
if (this.zoom) {
this.zoom.on("zoom", null);
}
this.$refs.wrapper.innerHTML =
"<svg id=\"svg-canvas\" width=\"100%\" style=\"min-height:800px\"/>";
const g = new dagreD3.graphlib.Graph({
compound: true,
multigraph: true
})
.setGraph({})
.setDefaultEdgeLabel(function() {
return {};
});
const getOptions = node => {
const edgeOption = {};
if (node.relation !== "SEQUENTIAL") {
edgeOption.label = node.relation.toLowerCase();
if (node.taskRun && node.taskRun.value) {
edgeOption.label += ` : ${node.taskRun.value}`;
const getOptions = node => {
const edgeOption = {};
if (node.relation !== "SEQUENTIAL") {
edgeOption.label = node.relation.toLowerCase();
if (node.taskRun && node.taskRun.value) {
edgeOption.label += ` : ${node.taskRun.value}`;
}
}
edgeOption.class =
{
ERROR: "error-edge",
DYNAMIC: "dynamic-edge",
CHOICE: "choice-edge",
PARALLEL: "choice-edge"
}[node.relation] || "";
return edgeOption;
};
const ancestorsHashes = new Set(
this.filteredDataTree.filter(e => e.parent).map(e => nodeHash(e.parent[0]))
);
const virtualRootId = this.getVirtualRootNode() && this.getVirtualRootNode().task.id
for (const node of this.filteredDataTree) {
const slug = this.slug(node);
const hash = parentHash(node);
g.setNode(slug, {
labelType: "html",
label: `<div class="node-binder" id="node-${slug}"/>`
});
const options = getOptions(node);
const parentId = node.parent && node.parent[0] && node.parent[0].id
if (ancestorsHashes.has(hash) && ["SEQUENTIAL", "ERROR"].includes(node.relation) && virtualRootId !== parentId) {
g.setEdge(parentHash(node), slug, options);
} else {
g.setEdge("parent node", slug, options)
}
}
edgeOption.class =
{
ERROR: "error-edge",
DYNAMIC: "dynamic-edge",
CHOICE: "choice-edge",
PARALLEL: "choice-edge"
}[node.relation] || "";
return edgeOption;
};
const ancestorsHashes = new Set(
this.filteredDataTree.filter(e => e.parent).map(e => nodeHash(e.parent[0]))
);
const virtualRootId = this.getVirtualRootNode() && this.getVirtualRootNode().task.id
for (const node of this.filteredDataTree) {
const slug = this.slug(node);
const hash = parentHash(node);
g.setNode(slug, {
const rootNode = {
labelType: "html",
label: `<div class="node-binder" id="node-${slug}"/>`
});
const options = getOptions(node);
const parentId = node.parent && node.parent[0] && node.parent[0].id
if (ancestorsHashes.has(hash) && ['SEQUENTIAL', 'ERROR'].includes(node.relation) && virtualRootId !== parentId) {
g.setEdge(parentHash(node), slug, options);
clusterLabelPos: "bottom"
};
if (this.filterGroup) {
rootNode.label = `<div class="node-binder root-node-virtual" id="node-${this.slug(
this.virtualRootNode
)}"/>`;
} else {
g.setEdge("parent node", slug, options)
rootNode.class = "root-node";
rootNode.label = "<div class=\"vector-circle-wrapper\"/>";
rootNode.height = 30;
rootNode.width = 30;
}
}
const rootNode = {
labelType: "html",
clusterLabelPos: "bottom"
};
if (this.filterGroup) {
rootNode.label = `<div class="node-binder root-node-virtual" id="node-${this.slug(
this.virtualRootNode
)}"/>`;
} else {
rootNode.class = "root-node";
rootNode.label = '<div class="vector-circle-wrapper"/>';
rootNode.height = 30;
rootNode.width = 30;
}
g.setNode("parent node", rootNode);
g.nodes().forEach(v => {
const node = g.node(v);
if (node) {
node.paddingLeft = node.paddingRight = node.paddingTop = node.paddingBottom = 0;
g.setNode("parent node", rootNode);
g.nodes().forEach(v => {
const node = g.node(v);
if (node) {
node.paddingLeft = node.paddingRight = node.paddingTop = node.paddingBottom = 0;
}
});
if (!this.orientation) {
g.graph().rankDir = "LR";
}
});
if (!this.orientation) {
g.graph().rankDir = "LR";
}
const render = new dagreD3.render();
// Set up an SVG group so that we can translate the final graph.
const svgWrapper = d3.select("#svg-canvas"),
svgGroup = svgWrapper.append("g");
// Run the renderer. This is what draws the final graph.
this.zoom = d3
.zoom()
.on("zoom", () => {
const t = d3.event.transform;
svgGroup.attr(
"transform",
`translate(${t.x},${t.y}) scale(${t.k})`
);
})
.scaleExtent([1, 1]);
svgWrapper.call(this.zoom);
svgWrapper.on("dblclick.zoom", null);
render(d3.select("#svg-canvas g"), g);
d3.selectAll("#svg-canvas g path").style("stroke", arrowColor);
d3.selectAll("#svg-canvas .edgePath marker").style(
"fill",
arrowColor
);
const transform = d3.zoomIdentity.translate(50, 50).translate(0, 0);
svgWrapper.call(this.zoom.transform, transform);
this.bindNodes();
},
virtalNodeReady() {
if (this.virtualRootNode) {
const vueNode = this.$refs[
`node-${this.slug(this.virtualRootNode)}`
];
return vueNode && vueNode.$el;
} else {
return true;
}
},
bindNodes() {
let ready = true;
for (const node of this.filteredDataTree) {
if (
!this.virtalNodeReady() ||
!this.$refs[this.slug(node)] ||
!this.$refs[this.slug(node)].length ||
!this.$refs["vector-circle"]
) {
ready = false;
}
}
if (ready) {
for (const node of this.filteredDataTree) {
this.$el
.querySelector(`#node-${this.slug(node)}`)
.appendChild(this.$refs[this.slug(node)][0].$el);
}
if (this.virtualRootNode) {
this.$el
.querySelector(
`#node-${this.slug(this.virtualRootNode)}`
)
.appendChild(
this.$refs[
`node-${this.slug(this.virtualRootNode)}`
].$el
const render = new dagreD3.render();
// Set up an SVG group so that we can translate the final graph.
const svgWrapper = d3.select("#svg-canvas"),
svgGroup = svgWrapper.append("g");
// Run the renderer. This is what draws the final graph.
this.zoom = d3
.zoom()
.on("zoom", () => {
const t = d3.event.transform;
svgGroup.attr(
"transform",
`translate(${t.x},${t.y}) scale(${t.k})`
);
} else {
this.$el
.querySelector(".vector-circle-wrapper")
.appendChild(this.$refs["vector-circle"]);
}
this.ready = true;
} else {
setTimeout(this.bindNodes, 30);
}
},
onFilterGroup(group) {
if (this.$route.query.filter != group) {
this.filterGroup = group;
this.$router.push({
query: { ...this.$route.query, filter: group }
});
this.generateGraph();
}
},
slug(node) {
const hash =
node.task.id +
(node.taskRun && node.taskRun.value
? "-" + node.taskRun.value
: "");
return hash.hashCode();
},
getFilteredDataTree() {
if (this.filterGroup) {
return this.dataTree.filter(
node =>
node.groups &&
node.groups[node.groups.length - 1] === this.filterGroup
})
.scaleExtent([1, 1]);
svgWrapper.call(this.zoom);
svgWrapper.on("dblclick.zoom", null);
render(d3.select("#svg-canvas g"), g);
d3.selectAll("#svg-canvas g path").style("stroke", arrowColor);
d3.selectAll("#svg-canvas .edgePath marker").style(
"fill",
arrowColor
);
} else {
return this.dataTree.filter(node => !node.groups);
const transform = d3.zoomIdentity.translate(50, 50).translate(0, 0);
svgWrapper.call(this.zoom.transform, transform);
this.bindNodes();
},
virtalNodeReady() {
if (this.virtualRootNode) {
const vueNode = this.$refs[
`node-${this.slug(this.virtualRootNode)}`
];
return vueNode && vueNode.$el;
} else {
return true;
}
},
bindNodes() {
let ready = true;
for (const node of this.filteredDataTree) {
if (
!this.virtalNodeReady() ||
!this.$refs[this.slug(node)] ||
!this.$refs[this.slug(node)].length ||
!this.$refs["vector-circle"]
) {
ready = false;
}
}
if (ready) {
for (const node of this.filteredDataTree) {
this.$el
.querySelector(`#node-${this.slug(node)}`)
.appendChild(this.$refs[this.slug(node)][0].$el);
}
if (this.virtualRootNode) {
this.$el
.querySelector(
`#node-${this.slug(this.virtualRootNode)}`
)
.appendChild(
this.$refs[
`node-${this.slug(this.virtualRootNode)}`
].$el
);
} else {
this.$el
.querySelector(".vector-circle-wrapper")
.appendChild(this.$refs["vector-circle"]);
}
this.ready = true;
} else {
setTimeout(this.bindNodes, 30);
}
},
onFilterGroup(group) {
if (this.$route.query.filter != group) {
this.filterGroup = group;
this.$router.push({
query: {...this.$route.query, filter: group}
});
this.generateGraph();
}
},
slug(node) {
const hash =
node.task.id +
(node.taskRun && node.taskRun.value
? "-" + node.taskRun.value
: "");
return hash.hashCode();
},
getFilteredDataTree() {
if (this.filterGroup) {
return this.dataTree.filter(
node =>
node.groups &&
node.groups[node.groups.length - 1] === this.filterGroup
);
} else {
return this.dataTree.filter(node => !node.groups);
}
}
},
computed: {
groups() {
const groups = new Set();
this.dataTree.forEach(node =>
(node.groups || []).forEach(group => groups.add(group))
);
return groups;
}
},
destroyed() {
this.ready = false;
}
},
computed: {
groups() {
const groups = new Set();
this.dataTree.forEach(node =>
(node.groups || []).forEach(group => groups.add(group))
);
return groups;
}
},
destroyed() {
this.ready = false;
}
};
};
</script>
<style lang="scss">
@import "../../styles/variable";

View File

@@ -1,15 +1,15 @@
<template>
<div class="node-wrapper">
<div class="status-color" v-if="n.taskRun" :class="contentCls"></div>
<div class="status-color" v-if="n.taskRun" :class="contentCls" />
<div class="task-content">
<div class="card-header">
<div class="icon-wrapper">
<!-- <img src=""/> -->
<!-- <img src=""/> -->
</div>
<div class="task-title">
<span>{{task.id | ellipsis(18)}}</span>
<span>{{ task.id | ellipsis(18) }}</span>
</div>
<!-- <menu-open class="node-action" @click="onSettings" /> -->
<!-- <menu-open class="node-action" @click="onSettings" /> -->
</div>
<div v-if="task.state" class="status-wrapper">
<status :status="state" />
@@ -82,104 +82,104 @@
</div>
<b-modal size="xl" :hide-footer="true" :id="`modal-${hash}`" :title="`Task ${task.id}`">
<pre>{{taskYaml}}</pre>
<pre>{{ taskYaml }}</pre>
</b-modal>
</div>
</template>
<script>
import Console from "vue-material-design-icons/Console";
import Graph from "vue-material-design-icons/Graph";
import CodeTags from "vue-material-design-icons/CodeTags";
import FormatListChecks from "vue-material-design-icons/FormatListChecks";
import LocationExit from "vue-material-design-icons/LocationExit";
import CurrentAc from "vue-material-design-icons/CurrentAc";
import { mapState } from "vuex";
import Status from "../Status";
import md5 from "md5";
import YamlUtils from "../../utils/yamlUtils";
import MarkdownTooltip from "../../components/layout/MarkdownTooltip";
import Console from "vue-material-design-icons/Console";
import Graph from "vue-material-design-icons/Graph";
import CodeTags from "vue-material-design-icons/CodeTags";
import FormatListChecks from "vue-material-design-icons/FormatListChecks";
import LocationExit from "vue-material-design-icons/LocationExit";
import CurrentAc from "vue-material-design-icons/CurrentAc";
import {mapState} from "vuex";
import Status from "../Status";
import md5 from "md5";
import YamlUtils from "../../utils/yamlUtils";
import MarkdownTooltip from "../../components/layout/MarkdownTooltip";
export default {
components: {
MarkdownTooltip,
Status,
Console,
Graph,
CodeTags,
FormatListChecks,
LocationExit,
CurrentAc
},
props: {
n: {
type: Object,
default: undefined
export default {
components: {
MarkdownTooltip,
Status,
Console,
Graph,
CodeTags,
FormatListChecks,
LocationExit,
CurrentAc
},
isFlow: {
type: Boolean,
default: false
}
},
methods: {
taskRunOutputToken(taskRun) {
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ''));
},
onFilterGroup() {
this.$emit("onFilterGroup", this.task.id);
},
onSettings() {
if (this.node) {
this.$store.dispatch("graph/setNode", undefined);
} else {
this.$store.dispatch("graph/setNode", this.n);
this.$emit("onSettings");
props: {
n: {
type: Object,
default: undefined
},
isFlow: {
type: Boolean,
default: false
}
}
},
computed: {
...mapState("graph", ["node"]),
hasLogs() {
// @TODO
return true;
},
methods: {
taskRunOutputToken(taskRun) {
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""));
},
onFilterGroup() {
this.$emit("onFilterGroup", this.task.id);
},
onSettings() {
if (this.node) {
this.$store.dispatch("graph/setNode", undefined);
} else {
this.$store.dispatch("graph/setNode", this.n);
this.$emit("onSettings");
}
}
},
computed: {
...mapState("graph", ["node"]),
hasLogs() {
// @TODO
return true;
// return (
// this.attempts.filter(attempt => attempt.logs.length).length > 0
// );
},
hasOutputs() {
return this.n.taskRun && this.n.taskRun.outputs;
},
attempts() {
return this.n.taskRun && this.n.taskRun.attempts
? this.n.taskRun.attempts
: [];
},
hash() {
return this.task.id.hashCode();
},
childrenCount() {
return this.task.tasks ? this.task.tasks.length : 0;
},
taskYaml() {
return YamlUtils.stringify(this.n);
},
state() {
return this.n.taskRun ? this.n.taskRun.state.current : "SUCCESS";
},
contentCls() {
return {
"is-success": !["RUNNING", "FAILED"].includes(this.state),
"is-running": this.state === "RUNNING",
"is-failed": this.state === "FAILED"
};
},
task() {
return this.n.task;
},
value () {
return this.n.taskRun && this.n.taskRun.value
},
hasOutputs() {
return this.n.taskRun && this.n.taskRun.outputs;
},
attempts() {
return this.n.taskRun && this.n.taskRun.attempts
? this.n.taskRun.attempts
: [];
},
hash() {
return this.task.id.hashCode();
},
childrenCount() {
return this.task.tasks ? this.task.tasks.length : 0;
},
taskYaml() {
return YamlUtils.stringify(this.n);
},
state() {
return this.n.taskRun ? this.n.taskRun.state.current : "SUCCESS";
},
contentCls() {
return {
"is-success": !["RUNNING", "FAILED"].includes(this.state),
"is-running": this.state === "RUNNING",
"is-failed": this.state === "FAILED"
};
},
task() {
return this.n.task;
},
value () {
return this.n.taskRun && this.n.taskRun.value
}
}
}
};
};
</script>
<style scoped lang="scss">
@import "../../styles/_variable.scss";

View File

@@ -3,11 +3,15 @@
<b-navbar type="dark" variant="dark">
<b-btn-group>
<b-button @click="autoFold(true)" size="sm" variant="light" v-b-tooltip.hover.top="$t('Fold content lines')">
<unfold-less-horizontal/>
<unfold-less-horizontal />
</b-button>
<b-button @click="unfoldAll" size="sm" variant="light"
v-b-tooltip.hover.top="$t('Unfold content lines')">
<unfold-more-horizontal/>
<b-button
@click="unfoldAll"
size="sm"
variant="light"
v-b-tooltip.hover.top="$t('Unfold content lines')"
>
<unfold-more-horizontal />
</b-button>
</b-btn-group>
</b-navbar>
@@ -19,9 +23,9 @@
:lang="lang"
theme="merbivore_soft"
:width="width"
minLines="5"
min-lines="5"
:height="height"
></editor>
/>
</div>
</template>
@@ -29,7 +33,7 @@
import UnfoldLessHorizontal from "vue-material-design-icons/UnfoldLessHorizontal";
import UnfoldMoreHorizontal from "vue-material-design-icons/UnfoldMoreHorizontal";
import YamlUtils from '../../utils/yamlUtils';
import YamlUtils from "../../utils/yamlUtils";
export default {
props: {
@@ -60,7 +64,7 @@
editor.setOptions({
minLines: 5,
maxLines: Infinity,
fontFamily: '"Source Code Pro", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontFamily: "\"Source Code Pro\", SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace",
showPrintMargin: false,
tabSize: 2,
wrap: false,
@@ -73,15 +77,15 @@
name: "save",
bindKey: {win: "Ctrl-S", mac: "Cmd-S"},
exec: (editor) => {
this.$emit('onSave', editor.session.getValue())
this.$emit("onSave", editor.session.getValue())
},
});
setTimeout(() => {
this.autoFold(localStorage.getItem('autofoldTextEditor') === "1")
this.autoFold(localStorage.getItem("autofoldTextEditor") === "1")
})
},
trimContent(text) {
return text.split('\n').map(line => line.trim()).join('\n')
return text.split("\n").map(line => line.trim()).join("\n")
},
autoFold(autoFold) {
//we may add try in case content is not serializable a json
@@ -92,8 +96,8 @@
for (const foldableToken of foldableTokens) {
const search = this.trimContent(foldableToken)
const index = trimmedContent.indexOf(search)
const line = trimmedContent.slice(0, index).split('\n').length + lineDiff
lineDiff = line + search.split('\n').length - 2
const line = trimmedContent.slice(0, index).split("\n").length + lineDiff
lineDiff = line + search.split("\n").length - 2
trimmedContent = trimmedContent.slice(index + search.length)
this.ed.getSession().$toggleFoldWidget(line - 2, {})
}
@@ -103,12 +107,12 @@
for (const n of node) {
this.getFoldLines(n, foldableTokens, autoFold)
}
} else if (typeof (node) === 'object') {
} else if (typeof (node) === "object") {
for (const key in node) {
this.getFoldLines(node[key], foldableTokens, autoFold)
}
} else if (typeof (node) === 'string') {
if (node.split('\n').length > 1 && autoFold) {
} else if (typeof (node) === "string") {
if (node.split("\n").length > 1 && autoFold) {
foldableTokens.push(node)
}
}
@@ -117,7 +121,7 @@
this.ed.getSession().expandFolds(this.ed.getSession().getAllFolds())
},
onInput(value) {
this.$emit('input', value);
this.$emit("input", value);
}
},
};

View File

@@ -1,6 +1,6 @@
<template>
<b-navbar class="bottom-line" fixed="bottom" toggleable="lg">
<slot/>
<slot />
</b-navbar>
</template>
<script>

View File

@@ -1,40 +1,39 @@
<template>
<div>
<b-navbar toggleable="lg" type="light" variant="light" v-if="hasNavBar">
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-navbar-toggle target="nav-collapse" />
<b-collapse id="nav-collapse" is-nav>
<b-nav-form>
<slot name="navbar"></slot>
<slot name="navbar" />
</b-nav-form>
</b-collapse>
</b-navbar>
<slot name="top"></slot>
<slot name="top" />
<slot name="table"></slot>
<slot name="table" />
<pagination :total="total" :max="max" @onPageChanged="onPageChanged" />
</div>
</template>
<script>
import Pagination from "./Pagination";
export default {
components: { Pagination },
computed: {
hasNavBar() {
return !!this.$slots["navbar"];
import Pagination from "./Pagination";
export default {
components: {Pagination},
computed: {
hasNavBar() {
return !!this.$slots["navbar"];
},
},
},
props: {
total: { type: Number, required: true },
max: {type: Number, required:false}
},
methods: {
onPageChanged(pagination) {
this.$emit("onPageChanged", pagination);
props: {
total: {type: Number, required: true},
max: {type: Number, required:false, default: undefined}},
methods: {
onPageChanged(pagination) {
this.$emit("onPageChanged", pagination);
},
},
},
};
};
</script>
<style scoped lang="scss">

View File

@@ -7,7 +7,7 @@
type="datetime"
class="sm"
:placeholder="$t('start datetime')"
></date-picker>
/>
<date-picker
@input="onDate"
v-model="end"
@@ -15,43 +15,43 @@
type="datetime"
class="sm"
:placeholder="$t('end datetime')"
></date-picker>
/>
</div>
</template>
<script>
import DatePicker from "vue2-datepicker";
export default {
components: { DatePicker },
data() {
return {
start: null,
end: null
};
},
created() {
if (this.$route.query.start) {
this.start = new Date(parseInt(this.$route.query.start));
}
if (this.$route.query.end) {
this.end = new Date(parseInt(this.$route.query.end));
}
},
methods: {
onDate() {
const start = this.start,
end = this.end;
const dateRange = {
start: start ? start.toISOString() : null,
end: end ? end.toISOString() : null
import DatePicker from "vue2-datepicker";
export default {
components: {DatePicker},
data() {
return {
start: null,
end: null
};
const query = { ...this.$route.query };
query.start = start ? start.getTime() : undefined;
query.end = end ? end.getTime() : undefined;
this.$router.push({ query });
this.$emit("onDate", dateRange);
},
created() {
if (this.$route.query.start) {
this.start = new Date(parseInt(this.$route.query.start));
}
if (this.$route.query.end) {
this.end = new Date(parseInt(this.$route.query.end));
}
},
methods: {
onDate() {
const start = this.start,
end = this.end;
const dateRange = {
start: start ? start.toISOString() : null,
end: end ? end.toISOString() : null
};
const query = {...this.$route.query};
query.start = start ? start.getTime() : undefined;
query.end = end ? end.getTime() : undefined;
this.$router.push({query});
this.$emit("onDate", dateRange);
}
}
}
};
};
</script>
<style scoped lang="scss">
.time-line {

View File

@@ -1,36 +1,38 @@
<template>
<div>
<span v-html="markdownRenderer"></span>
<span v-html="markdownRenderer" />
</div>
</template>
<script>
import Prism from "prismjs";
import "prismjs/themes/prism-okaidia.css";
import 'prismjs/components/prism-yaml.min';
import "prismjs/components/prism-yaml.min";
import Markdown from "../../utils/markdown";
export default {
props: {
watches: {
type: Array,
default: () => ['source', 'show', 'toc'],
default: () => ["source", "show", "toc"],
},
source: {
type: String,
default: ``,
default: "",
},
permalink: {
type: Boolean,
default: false,
}
},
},
computed: {
markdownRenderer() {
const outHtml = Markdown.render(this.source, {permalink: this.permalink});
const outHtml = Markdown.render(this.source, {
permalink: this.permalink,
});
this.$emit('rendered', outHtml)
this.$emit("rendered", outHtml);
this.$nextTick(() => {
Prism.highlightAll();
@@ -39,7 +41,6 @@
return outHtml;
},
},
};
</script>
@@ -52,5 +53,4 @@
font-weight: normal;
}
}
</style>

View File

@@ -2,7 +2,7 @@
<span v-if="description">
<help-circle title="" :id="'tooltip-desc-' + id" />
<b-popover triggers="hover" :target="'tooltip-desc-' + id" placement="bottom">
<markdown :source="description"></markdown>
<markdown :source="description" />
</b-popover>
</span>
</template>
@@ -22,7 +22,7 @@
},
description: {
type: String,
default: ``,
default: "",
}
},
computed: {

View File

@@ -6,7 +6,7 @@
@change="pageSizeChange"
size="sm"
:options="pageOptions"
></b-form-select>
/>
</div>
<div>
<b-pagination
@@ -18,49 +18,50 @@
size="sm"
class="my-0"
align="right"
></b-pagination>
/>
</div>
<small v-if="max" class="btn btn-sm btn-outline-light text-muted"
>{{ $t('Max displayed') }}: {{ max }}</small
>
<small class="btn btn-sm btn-outline-light text-muted"
>{{ $t('Total') }}: {{ total }}</small
>
<small v-if="max" class="btn btn-sm btn-outline-light text-muted">
{{ $t('Max displayed') }}: {{ max }}
</small>
<small class="btn btn-sm btn-outline-light text-muted">
{{ $t('Total') }}: {{ total }}
</small>
</div>
</template>
<script>
export default {
props: {
total: { type: Number, required: true },
max: {type: Number, required:false}
},
data() {
return {
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
pageOptions: [
{ value: 10, text: `10 ${this.$t("Per page")}` },
{ value: 25, text: `25 ${this.$t("Per page")}` },
{ value: 50, text: `50 ${this.$t("Per page")}` },
{ value: 100, text: `100 ${this.$t("Per page")}` },
],
};
},
methods: {
pageSizeChange() {
this.$emit("onPageChanged", {
page: 1,
size: this.size,
});
export default {
props: {
total: {type: Number, required: true},
max: {type: Number, required:false, default: undefined}},
data() {
return {
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
pageOptions: [
{value: 10, text: `10 ${this.$t("Per page")}`},
{value: 25, text: `25 ${this.$t("Per page")}`},
{value: 50, text: `50 ${this.$t("Per page")}`},
{value: 100, text: `100 ${this.$t("Per page")}`},
],
};
},
pageChanged(page) {
this.$emit("onPageChanged", {
page: page,
size: this.size,
});
methods: {
pageSizeChange() {
this.$emit("onPageChanged", {
page: 1,
size: this.size,
});
},
pageChanged(page) {
this.$emit("onPageChanged", {
page: page,
size: this.size,
});
},
},
},
};
};
</script>
<style scoped>
select {

View File

@@ -2,56 +2,60 @@
<b-button-group size="sm">
<b-tooltip
target="toggle-automatic-refresh-action"
>{{$t('toggle periodic refresh each 10 seconds')}}</b-tooltip>
>
{{ $t('toggle periodic refresh each 10 seconds') }}
</b-tooltip>
<b-button @click="toggleAutoRefresh" :pressed="autoRefresh" id="toggle-automatic-refresh-action">
<clock/> <span class="label">{{$t('automatic refresh')}}</span>
<clock /> <span class="label">{{ $t('automatic refresh') }}</span>
</b-button>
<b-tooltip target="trigger-refresh-action">{{ $t('trigger refresh') }}</b-tooltip>
<b-tooltip target="trigger-refresh-action">
{{ $t('trigger refresh') }}
</b-tooltip>
<b-button @click="triggerRefresh" id="trigger-refresh-action">
<refresh/> <span class="label">{{ $t('trigger refresh') }}</span>
<refresh /> <span class="label">{{ $t('trigger refresh') }}</span>
</b-button>
</b-button-group>
</template>
<script>
import Refresh from "vue-material-design-icons/Refresh";
import Clock from "vue-material-design-icons/Clock";
import Refresh from "vue-material-design-icons/Refresh";
import Clock from "vue-material-design-icons/Clock";
export default {
components: { Refresh, Clock },
data() {
return {
autoRefresh: false,
refreshHandler: undefined
};
},
created() {
this.autoRefresh = localStorage.getItem("autoRefresh") === "1";
},
methods: {
toggleAutoRefresh() {
this.autoRefresh = !this.autoRefresh;
localStorage.setItem("autoRefresh", this.autoRefresh ? "1" : "0");
if (this.autoRefresh) {
this.refreshHandler = setInterval(this.triggerRefresh, 10000);
this.triggerRefresh()
} else {
this.stopRefresh();
export default {
components: {Refresh, Clock},
data() {
return {
autoRefresh: false,
refreshHandler: undefined
};
},
created() {
this.autoRefresh = localStorage.getItem("autoRefresh") === "1";
},
methods: {
toggleAutoRefresh() {
this.autoRefresh = !this.autoRefresh;
localStorage.setItem("autoRefresh", this.autoRefresh ? "1" : "0");
if (this.autoRefresh) {
this.refreshHandler = setInterval(this.triggerRefresh, 10000);
this.triggerRefresh()
} else {
this.stopRefresh();
}
},
triggerRefresh() {
this.$emit("onRefresh");
},
stopRefresh() {
if (this.refreshHandler) {
clearInterval(this.refreshHandler);
this.refreshHandler = undefined
}
}
},
triggerRefresh() {
this.$emit("onRefresh");
},
stopRefresh() {
if (this.refreshHandler) {
clearInterval(this.refreshHandler);
this.refreshHandler = undefined
}
beforeDestroy() {
this.stopRefresh();
}
},
beforeDestroy() {
this.stopRefresh();
}
};
};
</script>
<style lang="scss">

View File

@@ -6,37 +6,37 @@
@input="onSearch"
v-model="search"
:placeholder="$t('search')"
></b-form-input>
/>
</b-nav-form>
</template>
<script>
import { debounce } from "throttle-debounce";
export default {
created() {
if (this.$route.query.q) {
this.search = this.$route.query.q;
}
this.searchDebounce = debounce(300, () => {
this.$emit("onSearch", this.search);
});
},
data() {
return {
search: ""
};
},
methods: {
onSearch() {
const query = { ...this.$route.query, q: this.search, page: 1 };
if (!this.search) {
delete query.q;
import {debounce} from "throttle-debounce";
export default {
created() {
if (this.$route.query.q) {
this.search = this.$route.query.q;
}
this.$router.push({ query });
this.searchDebounce();
this.searchDebounce = debounce(300, () => {
this.$emit("onSearch", this.search);
});
},
},
destroyed() {
this.searchDebounce.cancel();
}
};
data() {
return {
search: ""
};
},
methods: {
onSearch() {
const query = {...this.$route.query, q: this.search, page: 1};
if (!this.search) {
delete query.q;
}
this.$router.push({query});
this.searchDebounce();
},
},
destroyed() {
this.searchDebounce.cancel();
}
};
</script>

View File

@@ -1,27 +1,27 @@
<template>
<b-form-select @input="searchStatus" v-model="selected" size="sm" :options="statuses"></b-form-select>
<b-form-select @input="searchStatus" v-model="selected" size="sm" :options="statuses" />
</template>
<script>
export default {
data() {
return {
statuses: ["all", "running", "success", "failed"],
selected: "all"
};
},
created() {
if (this.$route.query.status) {
this.selected = this.$route.query.status.toLowerCase();
}
},
methods: {
searchStatus() {
const status = this.selected.toUpperCase();
if (this.$route.query.status !== status) {
this.$router.push({ query: { ...this.$route.query, status } });
this.$emit("onRefresh");
export default {
data() {
return {
statuses: ["all", "running", "success", "failed"],
selected: "all"
};
},
created() {
if (this.$route.query.status) {
this.selected = this.$route.query.status.toLowerCase();
}
},
methods: {
searchStatus() {
const status = this.selected.toUpperCase();
if (this.$route.query.status !== status) {
this.$router.push({query: {...this.$route.query, status}});
this.$emit("onRefresh");
}
}
}
}
};
};
</script>

View File

@@ -1,11 +1,15 @@
<template>
<b-navbar v-if="topNavbar" :class="menuCollapsed" class="top-line" type="dark" variant="dark">
<b-navbar-nav>
<b-nav-text >
<h1>{{title}}</h1>
<b-nav-text>
<h1>{{ title }}</h1>
<b-breadcrumb>
<b-breadcrumb-item><router-link :to="{ name: 'home'}"><home /> {{$t('home')}}</router-link></b-breadcrumb-item>
<b-breadcrumb-item>
<router-link :to="{name: 'home'}">
<home /> {{ $t('home') }}
</router-link>
</b-breadcrumb-item>
<b-breadcrumb-item v-for="(item, x) in topNavbar.breadcrumb" :to="item.link" :text="item.label" :key="x" />
</b-breadcrumb>
</b-nav-text>
@@ -14,31 +18,31 @@
</b-navbar>
</template>
<script>
import { mapState } from "vuex";
import Home from "vue-material-design-icons/Home";
import Auth from "Override/components/auth/Auth";
import {mapState} from "vuex";
import Home from "vue-material-design-icons/Home";
import Auth from "Override/components/auth/Auth";
export default {
components: {
Home,
Auth,
},
props: {
menuCollapsed : {
type: String,
required: true
}
},
computed: {
...mapState("layout", ["topNavbar"]),
title() {
return this.topNavbar.title;
export default {
components: {
Home,
Auth,
},
breadcrumb() {
return this.topNavbar.breadcrumb.join(" > ");
props: {
menuCollapsed : {
type: String,
required: true
}
},
computed: {
...mapState("layout", ["topNavbar"]),
title() {
return this.topNavbar.title;
},
breadcrumb() {
return this.topNavbar.breadcrumb.join(" > ");
}
}
}
};
};
</script>
<style lang="scss" scoped>
@import "../../styles/variable";

View File

@@ -12,7 +12,7 @@
size="sm"
@input="onChange"
:placeholder="$t('search') + '...'"
></b-form-input>
/>
</b-form-group>
</b-col>
<b-col md="6">
@@ -26,20 +26,20 @@
</b-row>
</template>
<script>
import LogLevelSelector from "./LogLevelSelector";
export default {
components: { LogLevelSelector },
data() {
return {
filter: "",
};
},
methods: {
onChange() {
const query = { ...this.$route.query, q: this.filter, page: 1 };
this.$router.push({ query });
this.$emit("onChange");
import LogLevelSelector from "./LogLevelSelector";
export default {
components: {LogLevelSelector},
data() {
return {
filter: "",
};
},
},
};
methods: {
onChange() {
const query = {...this.$route.query, q: this.filter, page: 1};
this.$router.push({query});
this.$emit("onChange");
},
},
};
</script>

View File

@@ -6,29 +6,29 @@
size="sm"
@input="onChange"
:options="levelOptions"
></b-form-select>
/>
</template>
<script>
export default {
data() {
return {
level: "INFO",
levelOptions: [
"TRACE",
"DEBUG",
"INFO",
"WARN",
"ERROR",
"CRITICAL",
],
};
},
methods: {
onChange() {
const query = { ...this.$route.query, level: this.level };
this.$router.push({ query });
this.$emit("onChange");
export default {
data() {
return {
level: "INFO",
levelOptions: [
"TRACE",
"DEBUG",
"INFO",
"WARN",
"ERROR",
"CRITICAL",
],
};
},
},
};
methods: {
onChange() {
const query = {...this.$route.query, level: this.level};
this.$router.push({query});
this.$emit("onChange");
},
},
};
</script>

View File

@@ -16,60 +16,61 @@
</div>
</template>
<script>
export default {
props: {
log: {
type: Object,
required: true,
export default {
props: {
log: {
type: Object,
required: true,
},
filter: {
type: String,
default: "",
},
level: {
type: String,
required: true,
},
excludeMetas: {
type: Array,
default: () => [],
},
},
filter: {
type: String,
default: "",
},
level: {
type: String,
},
excludeMetas: {
type: Array,
default: () => [],
},
},
computed: {
metaWithValue() {
const metaWithValue = [];
const excludes = [
"message",
"timestamp",
"thread",
"taskRunId",
"level",
];
excludes.push.apply(excludes, this.excludeMetas);
for (const key in this.log) {
if (this.log[key] && !excludes.includes(key)) {
metaWithValue.push({ key, value: this.log[key] });
computed: {
metaWithValue() {
const metaWithValue = [];
const excludes = [
"message",
"timestamp",
"thread",
"taskRunId",
"level",
];
excludes.push.apply(excludes, this.excludeMetas);
for (const key in this.log) {
if (this.log[key] && !excludes.includes(key)) {
metaWithValue.push({key, value: this.log[key]});
}
}
}
return metaWithValue;
return metaWithValue;
},
levelClass() {
return {
TRACE: "badge-info",
DEBUG: "badge-secondary",
INFO: "badge-primary",
WARN: "badge-warning",
ERROR: "badge-danger",
CRITICAL: "badge-danger font-weight-bold",
}[this.log.level];
},
filtered() {
return (
this.log.message &&
this.log.message.toLowerCase().includes(this.filter)
);
},
},
levelClass() {
return {
TRACE: "badge-info",
DEBUG: "badge-secondary",
INFO: "badge-primary",
WARN: "badge-warning",
ERROR: "badge-danger",
CRITICAL: "badge-danger font-weight-bold",
}[this.log.level];
},
filtered() {
return (
this.log.message &&
this.log.message.toLowerCase().includes(this.filter)
);
},
},
};
};
</script>
<style scoped lang="scss">
@import "../../styles/_variable.scss";

View File

@@ -1,7 +1,9 @@
<template>
<div v-if="execution" class="log-wrapper text-white">
<div v-for="taskItem in execution.taskRunList" :key="taskItem.id">
<template v-if="(!task || task.id === taskItem.id) && taskItem.attempts">
<template
v-if="(!task || task.id === taskItem.id) && taskItem.attempts"
>
<div class="bg-dark attempt-wrapper">
<template v-for="(attempt, index) in taskItem.attempts">
<div
@@ -13,41 +15,55 @@
:target="`attempt-${index}-${taskItem.id}`"
triggers="hover"
>
{{ $t('from') }} : {{ attempt.state.startDate | date('LLL:ss') }}
<br/>
{{ $t('to') }} : {{ attempt.state.endDate | date('LLL:ss') }}
<br/>
<br/>
<clock/>
{{ $t('duration') }} : {{ attempt.state.duration | humanizeDuration }}
{{ $t("from") }} :
{{ attempt.state.startDate | date("LLL:ss") }}
<br>
{{ $t("to") }} :
{{ attempt.state.endDate | date("LLL:ss") }}
<br>
<br>
<clock />
{{ $t("duration") }} :
{{ attempt.state.duration | humanizeDuration }}
</b-tooltip>
<div class="attempt-header">
<div class="attempt-number mr-1">
{{ $t('attempt') }} {{ index + 1 }}
{{ $t("attempt") }} {{ index + 1 }}
</div>
<div class="task-id flex-grow-1">
<code>{{ taskItem.taskId }}</code>
<small v-if="taskItem.value"> {{ taskItem.value }}</small>
<small v-if="taskItem.value">
{{ taskItem.value }}</small>
</div>
<b-button-group>
<b-button
v-if="taskItem.outputs"
:title="$t('toggle metrics')"
@click="toggleShowMetric(taskItem, index)"
><chart-areaspline :title="$t('toggle metrics')" /></b-button>
@click="
toggleShowMetric(taskItem, index)
"
>
<chart-areaspline
:title="$t('toggle metrics')"
/>
</b-button>
<b-button
v-if="taskItem.outputs"
:title="$t('toggle output')"
@click="toggleShowOutput(taskItem)"
><location-exit :title="$t('toggle output')" /></b-button>
>
<location-exit
:title="$t('toggle output')"
/>
</b-button>
<restart
:key="`restart-${index}-${attempt.state.startDate}`"
:isButtonGroup="true"
:is-button-group="true"
:execution="execution"
:task="taskItem"
/>
@@ -57,7 +73,9 @@
<!-- Log lines -->
<template>
<template v-for="(log, i) in findLogs(taskItem.id, index)">
<template
v-for="(log, i) in findLogs(taskItem.id, index)"
>
<log-line
:level="level"
:filter="filter"
@@ -74,8 +92,8 @@
:title="$t('metrics')"
:execution="execution"
:key="`metrics-${index}-${taskItem.id}`"
:data="convertMetric(attempt.metrics)" />
:data="convertMetric(attempt.metrics)"
/>
</template>
<!-- Outputs -->
<vars
@@ -83,13 +101,10 @@
:title="$t('outputs')"
:execution="execution"
:key="taskItem.id"
:data="taskItem.outputs" />
:data="taskItem.outputs"
/>
</div>
</template>
</div>
</div>
</template>
@@ -104,36 +119,44 @@
import ChartAreaspline from "vue-material-design-icons/ChartAreaspline";
export default {
components: {LogLine, Restart, Clock, LocationExit, Vars, ChartAreaspline},
components: {
LogLine,
Restart,
Clock,
LocationExit,
Vars,
ChartAreaspline,
},
props: {
level: {
type: String,
default: "INFO"
default: "INFO",
},
filter: {
type: String,
default: ""
default: "",
},
taskRunId: {
type: String,
default: undefined,
},
},
data() {
return {
showOutputs: {},
showMetrics: {}
showMetrics: {},
};
},
watch: {
level: function () {
this.loadLogs()
}
this.loadLogs();
},
},
created() {
this.loadLogs();
},
computed: {
...mapState("execution", ["execution", "task", "logs"])
...mapState("execution", ["execution", "task", "logs"]),
},
methods: {
toggleShowOutput(task) {
@@ -141,23 +164,25 @@
this.$forceUpdate();
},
toggleShowMetric(task, index) {
this.showMetrics[task.id + "-" + index] = !this.showMetrics[task.id + "-" + index];
this.showMetrics[task.id + "-" + index] = !this.showMetrics[
task.id + "-" + index
];
this.$forceUpdate();
},
loadLogs() {
let params = {minLevel: this.level};
if (this.taskRunId) {
params.taskRunId = this.taskRunId
params.taskRunId = this.taskRunId;
}
if (this.execution && this.execution.state.current === "RUNNING") {
this.$store
.dispatch("execution/followLogs", {
id: this.$route.params.id,
params: params
params: params,
})
.then(sse => {
.then((sse) => {
this.sse = sse;
this.$store.commit("execution/setLogs", []);
@@ -168,23 +193,26 @@
} else {
this.$store.dispatch("execution/loadLogs", {
executionId: this.$route.params.id,
params: params
params: params,
});
}
},
findLogs(taskRunId, attemptNumber) {
return (this.logs || [])
.filter(log => {
return log.taskRunId === taskRunId && log.attemptNumber === attemptNumber;
})
return (this.logs || []).filter((log) => {
return (
log.taskRunId === taskRunId &&
log.attemptNumber === attemptNumber
);
});
},
convertMetric(metrics) {
return (metrics || [])
.reduce((accumulator, r) => {
accumulator[r.name] = r.type === "timer" ? humanizeDuration(parseInt(r.value * 1000)) : r.value
return accumulator;
}, Object.create(null));
return (metrics || []).reduce((accumulator, r) => {
accumulator[r.name] =
r.type === "timer"
? humanizeDuration(parseInt(r.value * 1000))
: r.value;
return accumulator;
}, Object.create(null));
},
},
beforeDestroy() {
@@ -192,7 +220,7 @@
this.sse.close();
this.sse = undefined;
}
}
},
};
</script>
<style lang="scss" scoped>

View File

@@ -1,23 +1,23 @@
<template>
<div>
<log-filters/>
<log-filters />
<log-list :level="level" :filter="filterTerm" />
</div>
</template>
<script>
import LogList from "./LogList";
import LogFilters from "./LogFilters";
import { mapState } from "vuex";
export default {
components: { LogList, LogFilters },
computed: {
...mapState("execution", ["execution", "task", "logs"]),
filterTerm() {
return (this.$route.query.q || "").toLowerCase();
import LogList from "./LogList";
import LogFilters from "./LogFilters";
import {mapState} from "vuex";
export default {
components: {LogList, LogFilters},
computed: {
...mapState("execution", ["execution", "task", "logs"]),
filterTerm() {
return (this.$route.query.q || "").toLowerCase();
},
level() {
return this.$route.query.level || "INFO";
},
},
level() {
return this.$route.query.level || "INFO";
},
},
};
};
</script>

View File

@@ -3,14 +3,16 @@
<div class="log-content">
<main-log-filter @onChange="loadData" />
<div v-if="logs === undefined">
<b-alert variant="light" show>{{$t('no result')}}</b-alert>
<b-alert variant="light" show>
{{ $t('no result') }}
</b-alert>
</div>
<div class="bg-dark text-white">
<template v-for="(log, i) in logs">
<log-line
level="TRACE"
filter=""
:excludeMetas="isFlowEdit ? ['namespace', 'flowId'] : []"
:exclude-metas="isFlowEdit ? ['namespace', 'flowId'] : []"
:log="log"
:key="`${log.taskRunId}-${i}`"
/>
@@ -22,59 +24,59 @@
</template>
<script>
import LogLine from "../logs/LogLine";
import Pagination from "../layout/Pagination";
import { mapState } from "vuex";
import RouteContext from "../../mixins/routeContext";
import MainLogFilter from "./MainLogFilter";
import qb from "../../utils/queryBuilder";
import LogLine from "../logs/LogLine";
import Pagination from "../layout/Pagination";
import {mapState} from "vuex";
import RouteContext from "../../mixins/routeContext";
import MainLogFilter from "./MainLogFilter";
import qb from "../../utils/queryBuilder";
export default {
mixins: [RouteContext],
components: { LogLine, Pagination, MainLogFilter },
data() {
return {
task: undefined,
};
},
created() {
this.loadData();
},
computed: {
...mapState("log", ["logs", "total", "level"]),
routeInfo() {
export default {
mixins: [RouteContext],
components: {LogLine, Pagination, MainLogFilter},
data() {
return {
title: this.$t("logs"),
task: undefined,
};
},
isFlowEdit() {
return this.$route.name === 'flowEdit'
}
},
methods: {
onPageChanged(pagination) {
this.$router.push({
query: { ...this.$route.query, ...pagination },
});
created() {
this.loadData();
},
loadData() {
let q = qb.logQueryBuilder(this.$route);
if (this.isFlowEdit) {
q += ` AND namespace:${this.$route.params.namespace}`
q += ` AND flowId:${this.$route.params.id}`
computed: {
...mapState("log", ["logs", "total", "level"]),
routeInfo() {
return {
title: this.$t("logs"),
};
},
isFlowEdit() {
return this.$route.name === "flowEdit"
}
this.$store.dispatch("log/findLogs", {
q,
page: this.$route.query.page || 1,
size: this.$route.query.size || 25,
minLevel: this.$route.query.level || "INFO"
});
},
},
};
methods: {
onPageChanged(pagination) {
this.$router.push({
query: {...this.$route.query, ...pagination},
});
this.loadData();
},
loadData() {
let q = qb.logQueryBuilder(this.$route);
if (this.isFlowEdit) {
q += ` AND namespace:${this.$route.params.namespace}`
q += ` AND flowId:${this.$route.params.id}`
}
this.$store.dispatch("log/findLogs", {
q,
page: this.$route.query.page || 1,
size: this.$route.query.size || 25,
minLevel: this.$route.query.level || "INFO"
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/_variable.scss";

View File

@@ -1,6 +1,6 @@
<template>
<b-navbar toggleable="lg" type="light" variant="light">
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-navbar-toggle target="nav-collapse" />
<b-collapse id="nav-collapse" is-nav>
<b-nav-form>
<search-field ref="searchField" @onSearch="onChange" />
@@ -12,17 +12,17 @@
</b-navbar>
</template>
<script>
import NamespaceSelect from "../namespace/NamespaceSelect";
import SearchField from "../layout/SearchField";
import DateRange from "../layout/DateRange";
import LogLevelSelector from "./LogLevelSelector";
import NamespaceSelect from "../namespace/NamespaceSelect";
import SearchField from "../layout/SearchField";
import DateRange from "../layout/DateRange";
import LogLevelSelector from "./LogLevelSelector";
export default {
components: { NamespaceSelect, DateRange, SearchField, LogLevelSelector },
methods: {
onChange() {
this.$emit("onChange");
export default {
components: {NamespaceSelect, DateRange, SearchField, LogLevelSelector},
methods: {
onChange() {
this.$emit("onChange");
},
},
},
};
};
</script>

View File

@@ -5,41 +5,41 @@
:placeholder="$t('Select namespace')"
:options="namespaces"
class="ns-selector"
></v-select>
/>
</template>
<script>
import { mapState } from "vuex";
export default {
props: {
dataType: {
type: String,
required: true
}
},
created() {
this.$store.dispatch("namespace/loadNamespaces", {dataType: this.dataType});
this.selectedNamespace = this.$route.query.namespace || "";
},
computed: {
...mapState("namespace", ["namespaces"])
},
data() {
return {
selectedNamespace: ""
};
},
methods: {
onNamespaceSelect() {
const query = { ...this.$route.query };
query.namespace = this.selectedNamespace;
if (!this.selectedNamespace) {
delete query.namespace;
import {mapState} from "vuex";
export default {
props: {
dataType: {
type: String,
required: true
}
},
created() {
this.$store.dispatch("namespace/loadNamespaces", {dataType: this.dataType});
this.selectedNamespace = this.$route.query.namespace || "";
},
computed: {
...mapState("namespace", ["namespaces"])
},
data() {
return {
selectedNamespace: ""
};
},
methods: {
onNamespaceSelect() {
const query = {...this.$route.query};
query.namespace = this.selectedNamespace;
if (!this.selectedNamespace) {
delete query.namespace;
}
this.$router.push({query});
this.$emit("onNamespaceSelect");
}
this.$router.push({ query });
this.$emit("onNamespaceSelect");
}
}
};
};
</script>

View File

@@ -2,13 +2,15 @@
<div>
<b-row>
<b-col md="9" class="markdown">
<markdown v-if="plugin" :source="plugin.markdown" :permalink="true"></markdown>
<markdown v-if="plugin" :source="plugin.markdown" :permalink="true" />
<div v-else>
<b-alert variant="info" show>{{ $t('plugins.please') }}</b-alert>
<b-alert variant="info" show>
{{ $t('plugins.please') }}
</b-alert>
</div>
</b-col>
<b-col md="3">
<Toc @routerChange="routerChange" v-if="plugins" :plugins="plugins"></Toc>
<Toc @routerChange="routerChange" v-if="plugins" :plugins="plugins" />
</b-col>
</b-row>
</div>
@@ -16,9 +18,9 @@
<script>
import RouteContext from "../../mixins/routeContext";
import Markdown from '../layout/Markdown.vue'
import Toc from './Toc.vue'
import { mapState } from "vuex";
import Markdown from "../layout/Markdown.vue"
import Toc from "./Toc.vue"
import {mapState} from "vuex";
export default {
mixins: [RouteContext],
@@ -65,7 +67,7 @@
routerChange() {
window.scroll({
top: 0,
behavior: 'smooth'
behavior: "smooth"
})
this.loadPlugin();

View File

@@ -2,9 +2,11 @@
<div class="plugins-list">
<b-card class="accordion" no-body :key="plugin.manifest['X-Kestra-Title']" v-for="(plugin, index) in plugins">
<b-card-header header-tag="header" class="p-0" role="tab">
<b-button block v-b-toggle="plugin.manifest['X-Kestra-Title']" variant="light">{{ plugin.manifest['X-Kestra-Title'] }}</b-button>
<b-button block v-b-toggle="plugin.manifest['X-Kestra-Title']" variant="light">
{{ plugin.manifest['X-Kestra-Title'] }}
</b-button>
</b-card-header>
<b-collapse :id="plugin.manifest['X-Kestra-Title']" :visible="index === 0" accordion="my-accordion" role="tabpanel">
<b-collapse :id="plugin.manifest['X-Kestra-Title']" :visible="index === 0" accordion="my-accordion" role="tabpanel">
<b-card-body>
<ul class="section-nav toc-h3">
<li v-for="(classes, namespace) in group(plugin.tasks)" :key="namespace">
@@ -12,14 +14,13 @@
<ul>
<li v-for="cls in classes" :key="cls">
<router-link
v-on:click.native="$emit('routerChange')"
:to="{ name: 'pluginView', params: {cls: namespace + '.' + cls}}"
@click.native="$emit('routerChange')"
:to="{name: 'pluginView', params: {cls: namespace + '.' + cls}}"
>
{{ cls }}
</router-link>
</li>
</ul>
</li>
</ul>
</b-card-body>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<b-form-group :label="$t('Language')" label-cols-sm="3">
<b-form-select v-model="lang" :options="langOptions"></b-form-select>
<b-form-select v-model="lang" :options="langOptions" />
</b-form-group>
<b-form-group :label="$t('Fold auto')" label-cols-sm="3">
<b-checkbox v-model="autofoldTextEditor" value="1" unchecked-value="0" />
@@ -10,45 +10,45 @@
</template>
<script>
import RouteContext from "../../mixins/routeContext";
export default {
mixins: [RouteContext],
data() {
return {
langOptions: [
{ value: "en", text: "English" },
{ value: "fr", text: "Français" }
]
};
},
computed: {
routeInfo() {
import RouteContext from "../../mixins/routeContext";
export default {
mixins: [RouteContext],
data() {
return {
title: this.$t("settings")
langOptions: [
{value: "en", text: "English"},
{value: "fr", text: "Français"}
]
};
},
lang: {
set(lang) {
localStorage.setItem("lang", lang);
this.$moment.locale(lang);
this.$root.$i18n.locale = lang;
this.$toast().saved();
computed: {
routeInfo() {
return {
title: this.$t("settings")
};
},
get() {
return localStorage.getItem("lang") || "en";
}
},
autofoldTextEditor: {
set(value) {
localStorage.setItem("autofoldTextEditor", value);
this.$toast().saved();
lang: {
set(lang) {
localStorage.setItem("lang", lang);
this.$moment.locale(lang);
this.$root.$i18n.locale = lang;
this.$toast().saved();
},
get() {
return localStorage.getItem("lang") || "en";
}
},
get() {
return localStorage.getItem('autofoldTextEditor')
}
},
autofoldTextEditor: {
set(value) {
localStorage.setItem("autofoldTextEditor", value);
}
};
this.$toast().saved();
},
get() {
return localStorage.getItem("autofoldTextEditor")
}
},
}
};
</script>

View File

@@ -1,19 +1,20 @@
<template>
<div :id="uuid" :class="'executions-charts' + (this.global ? '' : ' mini')" v-if="dataReady">
<current-chart :data="collections" :options="options"></current-chart>
<current-chart :data="collections" :options="options" />
<b-tooltip
custom-class="tooltip-stats"
no-fade
:target="uuid"
:placement="(this.global ? 'bottom' : 'left')"
triggers="hover">
<span v-html="tooltip"></span>
triggers="hover"
>
<span v-html="tooltip" />
</b-tooltip>
</div>
</template>
<script>
import {Line} from 'vue-chartjs'
import {Line} from "vue-chartjs"
import humanizeDuration from "humanize-duration";
import {tooltip, defaultConfig} from "../../utils/charts.js";
import Utils from "../../utils/utils";
@@ -72,7 +73,7 @@
datasets: [{
label: "Duration",
backgroundColor: "#c7e7e5",
fill: 'start',
fill: "start",
pointRadius: 1,
borderWidth: 1,
borderColor: "#1dbaaf",

View File

@@ -1,19 +1,20 @@
<template>
<div :id="uuid" :class="'executions-charts' + (this.global ? '' : ' mini')" v-if="dataReady">
<current-chart :data="collections" :options="options"></current-chart>
<current-chart :data="collections" :options="options" />
<b-tooltip
custom-class="tooltip-stats"
no-fade
:target="uuid"
:placement="(this.global ? 'bottom' : 'left')"
triggers="hover">
<span v-html="tooltip"></span>
triggers="hover"
>
<span v-html="tooltip" />
</b-tooltip>
</div>
</template>
<script>
import {Bar} from 'vue-chartjs'
import {Bar} from "vue-chartjs"
import Utils from "../../utils/utils.js";
import {tooltip, defaultConfig} from "../../utils/charts.js";
import State from "../..//utils/state";

View File

@@ -1,6 +1,8 @@
<template>
<div class="state-global-charts">
<div class="title" :title="$t('last 30 days executions')">{{$t('last 30 days executions')}}</div>
<div class="title" :title="$t('last 30 days executions')">
{{ $t('last 30 days executions') }}
</div>
<state-chart
v-if="ready"
:data="data"

View File

@@ -2,15 +2,15 @@
<div v-if="ready">
<data-table @onPageChanged="onPageChanged" ref="dataTable" :total="total" :max="maxTaskRunSetting">
<template v-slot:navbar>
<search-field ref="searchField" @onSearch="onSearch" :fields="searchableFields"/>
<search-field ref="searchField" @onSearch="onSearch" :fields="searchableFields" />
<namespace-select
data-type="flow"
v-if="$route.name !== 'flowEdit'"
@onNamespaceSelect="onNamespaceSelect"
/>
<status-filter-buttons @onRefresh="loadData"/>
<date-range @onDate="onSearch"/>
<refresh-button class="float-right" @onRefresh="loadData"/>
<status-filter-buttons @onRefresh="loadData" />
<date-range @onDate="onSearch" />
<refresh-button class="float-right" @onRefresh="loadData" />
</template>
<template v-slot:top>
@@ -38,16 +38,18 @@
</template>
<template v-slot:cell(details)="row">
<router-link
:to="{name: 'executionEdit', params: {namespace: row.item.namespace, flowId: row.item.flowId, id: row.item.executionId},query: {tab:'gantt'} }">
<eye id="edit-action"/>
:to="{name: 'executionEdit', params: {namespace: row.item.namespace, flowId: row.item.flowId, id: row.item.executionId},query: {tab:'gantt'}}"
>
<eye id="edit-action" />
</router-link>
</template>
<template v-slot:cell(state.startDate)="row">{{ row.item.state.startDate | date('LLLL') }}
<template v-slot:cell(state.startDate)="row">
{{ row.item.state.startDate | date('LLLL') }}
</template>
<template v-slot:cell(state.endDate)="row">
<span
v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)"
>{{ row.item.state.endDate | date('LLLL') }}</span>
<span
v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)"
>{{ row.item.state.endDate | date('LLLL') }}</span>
</template>
<template v-slot:cell(state.current)="row">
<status
@@ -57,15 +59,16 @@
/>
</template>
<template v-slot:cell(state.duration)="row">
<span
v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)"
>{{ durationFrom(row.item) | humanizeDuration }}</span>
<span
v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)"
>{{ durationFrom(row.item) | humanizeDuration }}</span>
<span v-else>{{ row.item.state.duration | humanizeDuration }}</span>
</template>
<template v-slot:cell(flowId)="row">
<router-link
:to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.flowId}}"
>{{ row.item.flowId }}
>
{{ row.item.flowId }}
</router-link>
</template>
<template v-slot:cell(id)="row">
@@ -75,7 +78,7 @@
<code>{{ row.item.executionId | id }}</code>
</template>
<template v-slot:cell(taskId)="row">
<code v-b-tooltip.hover :title=row.item.taskId>{{ row.item.taskId | ellipsis(25) }} </code>
<code v-b-tooltip.hover :title="row.item.taskId">{{ row.item.taskId | ellipsis(25) }} </code>
</template>
<template v-slot:cell(executionId)="row">
<code>{{ row.item.executionId | id }}</code>
@@ -214,15 +217,15 @@
this.$router.push({
name: "executionEdit",
params: {namespace: item.namespace, flowId: item.flowId, id: item.executionId},
query: {tab: 'gantt'}
query: {tab: "gantt"}
});
},
loadData(callback) {
this.$store
.dispatch("stat/taskRunDaily", {
q: this.executionQuery,
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'),
endDate: this.$moment(this.endDate).format('YYYY-MM-DD')
startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
})
.then(() => {
this.dailyReady = true;

View File

@@ -1,17 +1,17 @@
<template>
<div>
<editor v-model="content" lang="yaml"></editor>
<editor v-model="content" lang="yaml" />
<bottom-line v-if="canSave || canDelete">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<b-button class="btn-danger" v-if="canDelete" @click="deleteFile">
<delete />
<span>{{$t('delete')}}</span>
<span>{{ $t('delete') }}</span>
</b-button>
<b-button @click="save" v-if="canSave">
<content-save />
<span>{{$t('save')}}</span>
<span>{{ $t('save') }}</span>
</b-button>
</li>
</ul>
@@ -20,39 +20,39 @@
</template>
<script>
import flowTemplateEdit from "../../mixins/flowTemplateEdit";
import { mapState } from "vuex";
import flowTemplateEdit from "../../mixins/flowTemplateEdit";
import {mapState} from "vuex";
export default {
mixins: [flowTemplateEdit],
data() {
return {
dataType: "template",
};
},
computed: {
...mapState("template", ["template"]),
},
watch: {
'$route.params'() {
export default {
mixins: [flowTemplateEdit],
data() {
return {
dataType: "template",
};
},
computed: {
...mapState("template", ["template"]),
},
watch: {
"$route.params"() {
this.reload()
},
},
created() {
this.reload()
},
},
created() {
this.reload()
},
destroyed() {
this.$store.commit("template/setTemplate", undefined);
},
methods: {
reload() {
if (this.$route.name === "templateEdit") {
this.$store
.dispatch("template/loadTemplate", this.$route.params)
.then(this.loadFile);
destroyed() {
this.$store.commit("template/setTemplate", undefined);
},
methods: {
reload() {
if (this.$route.name === "templateEdit") {
this.$store
.dispatch("template/loadTemplate", this.$route.params)
.then(this.loadFile);
}
}
}
}
};
};
</script>

View File

@@ -36,7 +36,9 @@
<template v-slot:cell(id)="row">
<router-link
:to="{name: `${dataType}Edit`, params: {namespace: row.item.namespace, id: row.item.id}}"
>{{row.item.id}}</router-link>
>
{{ row.item.id }}
</router-link>
</template>
</b-table>
</template>
@@ -48,7 +50,7 @@
<router-link :to="{name: 'templateAdd'}">
<b-button variant="primary">
<plus />
{{$t('create')}}
{{ $t('create') }}
</b-button>
</router-link>
</li>
@@ -58,75 +60,75 @@
</template>
<script>
import { mapState } from "vuex";
import permission from "../../models/permission";
import action from "../../models/action";
import NamespaceSelect from "../namespace/NamespaceSelect";
import Plus from "vue-material-design-icons/Plus";
import Eye from "vue-material-design-icons/Eye";
import BottomLine from "../layout/BottomLine";
import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions";
import DataTable from "../layout/DataTable";
import SearchField from "../layout/SearchField";
import {mapState} from "vuex";
import permission from "../../models/permission";
import action from "../../models/action";
import NamespaceSelect from "../namespace/NamespaceSelect";
import Plus from "vue-material-design-icons/Plus";
import Eye from "vue-material-design-icons/Eye";
import BottomLine from "../layout/BottomLine";
import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions";
import DataTable from "../layout/DataTable";
import SearchField from "../layout/SearchField";
export default {
mixins: [RouteContext, DataTableActions],
components: {
BottomLine,
Plus,
Eye,
DataTable,
SearchField,
NamespaceSelect,
},
data() {
return {
dataType: "template",
permission: permission,
action: action,
};
},
computed: {
...mapState("template", ["templates", "total"]),
...mapState("stat", ["dailyGroupByFlow", "daily"]),
...mapState("auth", ["user"]),
fields() {
const title = (title) => {
return this.$t(title);
export default {
mixins: [RouteContext, DataTableActions],
components: {
BottomLine,
Plus,
Eye,
DataTable,
SearchField,
NamespaceSelect,
},
data() {
return {
dataType: "template",
permission: permission,
action: action,
};
return [
{
key: "id",
label: title("template"),
sortable: true,
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "actions",
label: "",
class: "row-action",
},
];
},
},
methods: {
loadData(callback) {
this.$store
.dispatch("template/findTemplates", {
q: this.query,
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
sort: this.$route.query.sort,
})
.then(() => {
callback();
});
computed: {
...mapState("template", ["templates", "total"]),
...mapState("stat", ["dailyGroupByFlow", "daily"]),
...mapState("auth", ["user"]),
fields() {
const title = (title) => {
return this.$t(title);
};
return [
{
key: "id",
label: title("template"),
sortable: true,
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "actions",
label: "",
class: "row-action",
},
];
},
},
},
};
methods: {
loadData(callback) {
this.$store
.dispatch("template/findTemplates", {
q: this.query,
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
sort: this.$route.query.sort,
})
.then(() => {
callback();
});
},
},
};
</script>

View File

@@ -1,40 +1,40 @@
import Vue from 'vue'
import Vue from "vue"
import humanizeDuration from "humanize-duration";
Vue.filter('id', value => value ? value.toString().substr(0, 8) : '');
Vue.filter("id", value => value ? value.toString().substr(0, 8) : "");
Vue.filter('humanizeDuration', (value, options) => {
options = options || { maxDecimalPoints: 2 }
options.spacer = ''
var language = localStorage.getItem('lang') || 'en'
Vue.filter("humanizeDuration", (value, options) => {
options = options || {maxDecimalPoints: 2}
options.spacer = ""
var language = localStorage.getItem("lang") || "en"
options.language = language
options.languages = {}
options.languages[language] = {
y: () => 'y',
mo: () => 'mo',
w: () => 'w',
d: () => 'd',
h: () => 'h',
m: () => 'm',
s: () => 's',
ms: () => 'ms',
y: () => "y",
mo: () => "mo",
w: () => "w",
d: () => "d",
h: () => "h",
m: () => "m",
s: () => "s",
ms: () => "ms",
}
return humanizeDuration(value * 1000, options)
});
Vue.filter('cap', value => value ? value.toString().capitalize() : '');
Vue.filter("cap", value => value ? value.toString().capitalize() : "");
Vue.filter('lower', value => value ? value.toString().toLowerCase() : '');
Vue.filter("lower", value => value ? value.toString().toLowerCase() : "");
Vue.filter('date', (dateString, format) => {
Vue.filter("date", (dateString, format) => {
let f;
if (format === 'full') {
f = 'MMMM Do YYYY, h: mm: ss'
} else if (format === 'human') {
f = 'LLLL'
if (format === "full") {
f = "MMMM Do YYYY, h: mm: ss"
} else if (format === "human") {
f = "LLLL"
} else {
f = format
}
return Vue.moment(dateString).format(f)
})
Vue.filter('ellipsis', (text, len) => text.length > len ? text.substr(0, len) + '...' : text.substr(0, len))
Vue.filter("ellipsis", (text, len) => text.length > len ? text.substr(0, len) + "..." : text.substr(0, len))

View File

@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueAxios from 'vue-axios';
import axios from 'axios';
import Vue from "vue";
import VueAxios from "vue-axios";
import axios from "axios";
// eslint-disable-next-line no-undef
let root = (process.env.VUE_APP_API_URL || "") + KESTRA_BASE_PATH;
@@ -12,7 +12,7 @@ export default (callback, store, nprogress) => {
const instance = axios.create({
timeout: 15000,
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json"
},
onUploadProgress: function (progressEvent) {
if (progressEvent && progressEvent.loaded && progressEvent.total) {
@@ -27,7 +27,7 @@ export default (callback, store, nprogress) => {
return response
}, errorResponse => {
if (errorResponse.response && errorResponse.response.data) {
store.dispatch('core/showErrorMessage', errorResponse.response.data)
store.dispatch("core/showErrorMessage", errorResponse.response.data)
}
return Promise.reject(errorResponse);

View File

@@ -1,34 +1,34 @@
import './filters'
import 'moment/locale/fr'
import './utils/global'
import './custom.scss'
import "./filters"
import "moment/locale/fr"
import "./utils/global"
import "./custom.scss"
// @TODO: move to scss
import 'vue-material-design-icons/styles.css';
import "vue-material-design-icons/styles.css";
import App from './App.vue'
import BootstrapVue from 'bootstrap-vue'
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import VueMoment from 'vue-moment'
import NProgress from 'vue-nprogress'
import App from "./App.vue"
import BootstrapVue from "bootstrap-vue"
import Vue from "vue"
import VueI18n from "vue-i18n"
import VueMoment from "vue-moment"
import NProgress from "vue-nprogress"
import VueRouter from 'vue-router'
import VueSSE from 'vue-sse';
import VueSidebarMenu from 'vue-sidebar-menu'
import VueRouter from "vue-router"
import VueSSE from "vue-sse";
import VueSidebarMenu from "vue-sidebar-menu"
import Vuex from "vuex";
import VueAnalytics from 'vue-analytics';
import VueAnalytics from "vue-analytics";
import configureHttp from './http'
import configureHttp from "./http"
import Toast from "./utils/toast";
import { extendMoment } from 'moment-range';
import Translations from './translations.json'
import moment from 'moment'
import routes from './routes/routes'
import stores from './stores/store'
import vSelect from 'vue-select'
import VueHotkey from 'v-hotkey'
import {extendMoment} from "moment-range";
import Translations from "./translations.json"
import moment from "moment"
import routes from "./routes/routes"
import stores from "./stores/store"
import vSelect from "vue-select"
import VueHotkey from "v-hotkey"
let app = document.querySelector('#app');
let app = document.querySelector("#app");
if (app) {
Vue.use(Vuex)
@@ -49,7 +49,7 @@ if (app) {
Vue.use(VueI18n);
let i18n = new VueI18n({
locale: localStorage.getItem('lang') || 'en',
locale: localStorage.getItem("lang") || "en",
messages: Translations
});
@@ -60,13 +60,13 @@ if (app) {
Vue.use(VueHotkey)
Vue.use(VueSSE);
Vue.use(VueMoment, { moment: extendMoment(moment) });
Vue.use(VueMoment, {moment: extendMoment(moment)});
Vue.use(VueSidebarMenu);
Vue.use(BootstrapVue);
Vue.use(Toast)
Vue.component('v-select', vSelect);
Vue.component("v-select", vSelect);
Vue.config.productionTip = false;

View File

@@ -15,7 +15,7 @@ export default {
computed: {
routeInfo() {
return {
title: this.$t(this.dataType + 's')
title: this.$t(this.dataType + "s")
};
},
storageName() {
@@ -25,7 +25,7 @@ export default {
return this.fields.filter(f => f.sortable);
},
isBasePage() {
return ['executionsList', 'flowsList'].includes(this.$route.name)
return ["executionsList", "flowsList"].includes(this.$route.name)
}
},
methods: {
@@ -39,13 +39,13 @@ export default {
`${sortItem.sortBy}:${sortItem.sortDesc ? "desc" : "asc"}`
];
this.$router.push({
query: { ...this.$route.query, sort }
query: {...this.$route.query, sort}
});
this.loadData(this.onDataLoaded);
this.saveFilters()
},
onRowDoubleClick(item) {
this.$router.push({ name: this.dataType + "Edit", params: item });
this.$router.push({name: this.dataType + "Edit", params: item});
},
onPageChanged(item) {
this.$router.push({
@@ -76,10 +76,10 @@ export default {
}
},
loadFilters () {
const query = { ...this.$route.query}
const query = {...this.$route.query}
let change = false
if (this.isBasePage) {
const userPreferences = JSON.parse(localStorage.getItem(this.storageName) || '{}')
const userPreferences = JSON.parse(localStorage.getItem(this.storageName) || "{}")
for (const key in userPreferences) {
if (!query[key] && userPreferences[key]) {
query[key] = userPreferences[key]
@@ -88,7 +88,7 @@ export default {
}
}
if (change) {
this.$router.push({ query: query });
this.$router.push({query: query});
}
}
}

View File

@@ -1,5 +1,5 @@
import { canSaveFlowTemplate, saveFlowTemplate } from "../utils/flowTemplate";
import { mapGetters, mapState } from "vuex";
import {canSaveFlowTemplate, saveFlowTemplate} from "../utils/flowTemplate";
import {mapGetters, mapState} from "vuex";
import BottomLine from "../components/layout/BottomLine";
import ContentSave from "vue-material-design-icons/ContentSave";
@@ -124,12 +124,12 @@ export default {
} else {
const item = YamlUtils.parse(this.content);
this.$store
.dispatch(`${this.dataType}/create${this.dataType.capitalize()}`, { [this.dataType]: item})
.dispatch(`${this.dataType}/create${this.dataType.capitalize()}`, {[this.dataType]: item})
.then(() => {
this.$router.push({
name: `${this.dataType}Edit`,
params: item,
query: { tab: "data-source" }
query: {tab: "data-source"}
});
})
.then(() => {

View File

@@ -16,7 +16,7 @@ export default {
methods: {
onLoad() {
if (!this.preventRouteInfo) {
this.$store.commit('layout/setTopNavbar', this.routeInfo)
this.$store.commit("layout/setTopNavbar", this.routeInfo)
let baseTitle;

View File

@@ -1,145 +1,145 @@
<template>
<sidebar-menu
:menu="menu"
@toggle-collapse="onToggleCollapse"
width="200px"
:collapsed="collapsed"
>
<div class="logo" slot="header">
<router-link :to="{name: 'home'}">
<span>
<span class="img" />
<sidebar-menu
:menu="menu"
@toggle-collapse="onToggleCollapse"
width="200px"
:collapsed="collapsed"
>
<div class="logo" slot="header">
<router-link :to="{name: 'home'}">
<span>
<span class="img" />
</span>
</router-link>
</div>
<span slot="toggle-icon">
<chevron-right v-if="collapsed" />
<chevron-left v-else />
</span>
</router-link>
</div>
<span slot="toggle-icon">
<chevron-right v-if="collapsed" />
<chevron-left v-else />
</span>
</sidebar-menu>
</sidebar-menu>
</template>
<script>
import Vue from "vue";
import { SidebarMenu } from "vue-sidebar-menu";
import ChevronLeft from "vue-material-design-icons/ChevronLeft";
import ChevronRight from "vue-material-design-icons/ChevronRight";
import Graph from "vue-material-design-icons/Graph";
import Cog from "vue-material-design-icons/Cog";
import TimelineClock from "vue-material-design-icons/TimelineClock";
import BookOpen from "vue-material-design-icons/BookOpen";
import CardText from "vue-material-design-icons/CardText";
import HexagonMultiple from "vue-material-design-icons/HexagonMultiple";
import ChartTimeline from "vue-material-design-icons/ChartTimeline";
import Vue from "vue";
import {SidebarMenu} from "vue-sidebar-menu";
import ChevronLeft from "vue-material-design-icons/ChevronLeft";
import ChevronRight from "vue-material-design-icons/ChevronRight";
import Graph from "vue-material-design-icons/Graph";
import Cog from "vue-material-design-icons/Cog";
import TimelineClock from "vue-material-design-icons/TimelineClock";
import BookOpen from "vue-material-design-icons/BookOpen";
import CardText from "vue-material-design-icons/CardText";
import HexagonMultiple from "vue-material-design-icons/HexagonMultiple";
import ChartTimeline from "vue-material-design-icons/ChartTimeline";
Vue.component("graph", Graph);
Vue.component("settings", Cog);
Vue.component("timelineclock", TimelineClock);
Vue.component("bookopen", BookOpen);
Vue.component("cardtext", CardText);
Vue.component("hexagon-multiple", HexagonMultiple);
Vue.component("charttimeline", ChartTimeline);
Vue.component("graph", Graph);
Vue.component("settings", Cog);
Vue.component("timelineclock", TimelineClock);
Vue.component("bookopen", BookOpen);
Vue.component("cardtext", CardText);
Vue.component("hexagon-multiple", HexagonMultiple);
Vue.component("charttimeline", ChartTimeline);
export default {
components: {
ChevronLeft,
ChevronRight,
SidebarMenu,
},
methods: {
onToggleCollapse(folded) {
this.collapsed = folded;
localStorage.setItem("menuCollapsed", folded ? "true" : "false");
this.$emit("onMenuCollapse", folded);
}
},
data() {
return {
collapsed: localStorage.getItem("menuCollapsed") === "true"
};
},
computed: {
menu() {
return [
{
href: "/flows",
alias: [
"/flows*"
],
title: this.$t("flows"),
icon: {
element: "graph",
class: "menu-icon"
}
export default {
components: {
ChevronLeft,
ChevronRight,
SidebarMenu,
},
{
href: "/templates",
alias: [
"/templates*"
],
title: this.$t("templates"),
icon: {
element: "cardtext",
class: "menu-icon"
methods: {
onToggleCollapse(folded) {
this.collapsed = folded;
localStorage.setItem("menuCollapsed", folded ? "true" : "false");
this.$emit("onMenuCollapse", folded);
}
},
{
href: "/executions",
alias: [
"/executions*"
],
title: this.$t("executions"),
icon: {
element: "timelineclock",
class: "menu-icon"
}
data() {
return {
collapsed: localStorage.getItem("menuCollapsed") === "true"
};
},
{
href: "/taskruns",
alias: ["/taskruns*"],
title: this.$t("taskruns"),
icon: {
element: "charttimeline",
class: "menu-icon"
}
},
{
href: "/logs",
alias: [
"/logs*"
],
title: this.$t("logs"),
icon: {
element: "hexagon-multiple",
class: "menu-icon"
}
},
{
href: "/plugins",
alias: [
"/plugins*"
],
title: this.$t("plugins.documentation"),
icon: {
element: "bookopen",
class: "menu-icon"
}
},
{
href: "/settings",
alias: [
"/settings*"
],
title: this.$t("settings"),
icon: {
element: "settings",
class: "menu-icon"
}
computed: {
menu() {
return [
{
href: "/flows",
alias: [
"/flows*"
],
title: this.$t("flows"),
icon: {
element: "graph",
class: "menu-icon"
}
},
{
href: "/templates",
alias: [
"/templates*"
],
title: this.$t("templates"),
icon: {
element: "cardtext",
class: "menu-icon"
}
},
{
href: "/executions",
alias: [
"/executions*"
],
title: this.$t("executions"),
icon: {
element: "timelineclock",
class: "menu-icon"
}
},
{
href: "/taskruns",
alias: ["/taskruns*"],
title: this.$t("taskruns"),
icon: {
element: "charttimeline",
class: "menu-icon"
}
},
{
href: "/logs",
alias: [
"/logs*"
],
title: this.$t("logs"),
icon: {
element: "hexagon-multiple",
class: "menu-icon"
}
},
{
href: "/plugins",
alias: [
"/plugins*"
],
title: this.$t("plugins.documentation"),
icon: {
element: "bookopen",
class: "menu-icon"
}
},
{
href: "/settings",
alias: [
"/settings*"
],
title: this.$t("settings"),
icon: {
element: "settings",
class: "menu-icon"
}
}
];
}
}
];
}
}
};
};
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template>
<div/>
<div />
</template>
<script>
export default {

View File

@@ -1,46 +1,46 @@
import ExecutionRoot from '../components/executions/ExecutionRoot.vue'
import Executions from '../components/executions/Executions.vue'
import TaskRuns from '../components/taskruns/TaskRuns.vue'
import FlowEdit from '../components/flows/FlowEdit.vue'
import FlowRoot from '../components/flows/FlowRoot.vue'
import Flows from '../components/flows/Flows.vue'
import LogsWrapper from '../components/logs/LogsWrapper.vue'
import Plugin from '../components/plugins/Plugin.vue'
import Settings from '../components/settings/Settings.vue'
import TemplateEdit from '../components/templates/TemplateEdit.vue'
import Templates from '../components/templates/Templates.vue'
import ExecutionRoot from "../components/executions/ExecutionRoot.vue"
import Executions from "../components/executions/Executions.vue"
import TaskRuns from "../components/taskruns/TaskRuns.vue"
import FlowEdit from "../components/flows/FlowEdit.vue"
import FlowRoot from "../components/flows/FlowRoot.vue"
import Flows from "../components/flows/Flows.vue"
import LogsWrapper from "../components/logs/LogsWrapper.vue"
import Plugin from "../components/plugins/Plugin.vue"
import Settings from "../components/settings/Settings.vue"
import TemplateEdit from "../components/templates/TemplateEdit.vue"
import Templates from "../components/templates/Templates.vue"
export default {
mode: 'history',
mode: "history",
// eslint-disable-next-line no-undef
base: KESTRA_UI_PATH,
routes: [
//Flows
{ name: 'home', path: '/', component: Flows },
{ name: 'flowsList', path: '/flows', component: Flows },
{ name: 'flowsAdd', path: '/flows/new', component: FlowEdit },
{ name: 'flowEdit', path: '/flows/edit/:namespace/:id', component: FlowRoot },
{name: "home", path: "/", component: Flows},
{name: "flowsList", path: "/flows", component: Flows},
{name: "flowsAdd", path: "/flows/new", component: FlowEdit},
{name: "flowEdit", path: "/flows/edit/:namespace/:id", component: FlowRoot},
//Executions
{ name: 'executionsList', path: '/executions', component: Executions },
{ name: 'executionEdit', path: '/executions/:namespace/:flowId/:id', component: ExecutionRoot },
{name: "executionsList", path: "/executions", component: Executions},
{name: "executionEdit", path: "/executions/:namespace/:flowId/:id", component: ExecutionRoot},
//TaskRuns
{ name: 'taskRunList', path: '/taskruns', component: TaskRuns },
{name: "taskRunList", path: "/taskruns", component: TaskRuns},
//Documentation
{ name: 'plugin', path: '/plugins', component: Plugin },
{ name: 'pluginView', path: '/plugins/:cls', component: Plugin },
{name: "plugin", path: "/plugins", component: Plugin},
{name: "pluginView", path: "/plugins/:cls", component: Plugin},
//Templates
{ name: 'templateList', path: '/templates', component: Templates },
{ name: 'templateAdd', path: '/templates/new', component: TemplateEdit },
{ name: 'templateEdit', path: '/templates/edit/:namespace/:id', component: TemplateEdit },
{name: "templateList", path: "/templates", component: Templates},
{name: "templateAdd", path: "/templates/new", component: TemplateEdit},
{name: "templateEdit", path: "/templates/edit/:namespace/:id", component: TemplateEdit},
//Settings
{ name: 'logs', path: '/logs', component: LogsWrapper },
{name: "logs", path: "/logs", component: LogsWrapper},
//Settings
{ name: 'settings', path: '/settings', component: Settings },
{name: "settings", path: "/settings", component: Settings},
]
}

View File

@@ -5,7 +5,7 @@ export default {
},
actions: {
showErrorMessage({commit}, message) {
commit('setErrorMessage', message)
commit("setErrorMessage", message)
}
},
mutations: {

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
state: {
@@ -10,68 +10,68 @@ export default {
logs: []
},
actions: {
loadExecutions({ commit }, options) {
return Vue.axios.get(`/api/v1/executions`, { params: options }).then(response => {
commit('setExecutions', response.data.results)
commit('setTotal', response.data.total)
loadExecutions({commit}, options) {
return Vue.axios.get("/api/v1/executions", {params: options}).then(response => {
commit("setExecutions", response.data.results)
commit("setTotal", response.data.total)
})
},
restartExecution(_, options) {
return Vue.axios.post(`/api/v1/executions/${options.id}/restart?taskId=${options.taskId}`, { params: options }, {
return Vue.axios.post(`/api/v1/executions/${options.id}/restart?taskId=${options.taskId}`, {params: options}, {
headers: {
'content-type': 'multipart/form-data'
"content-type": "multipart/form-data"
}
})
},
kill(_, options) {
return Vue.axios.delete(`/api/v1/executions/${options.id}/kill`);
},
loadExecution({ commit }, options) {
loadExecution({commit}, options) {
return Vue.axios.get(`/api/v1/executions/${options.id}`).then(response => {
commit('setExecution', response.data)
commit("setExecution", response.data)
})
},
findExecutions({ commit }, options) {
findExecutions({commit}, options) {
const sort = options.sort
delete options.sort
let sortQueryString = ''
let sortQueryString = ""
if (sort) {
sortQueryString = `?sort=${sort}`
}
return Vue.axios.get(`/api/v1/executions/search${sortQueryString}`, { params: options }).then(response => {
commit('setExecutions', response.data.results)
commit('setTotal', response.data.total)
return Vue.axios.get(`/api/v1/executions/search${sortQueryString}`, {params: options}).then(response => {
commit("setExecutions", response.data.results)
commit("setTotal", response.data.total)
})
},
triggerExecution(_, options) {
return Vue.axios.post(`/api/v1/executions/trigger/${options.namespace}/${options.id}`, options.formData, {
timeout: 60 * 60 * 1000,
headers: {
'content-type': 'multipart/form-data'
"content-type": "multipart/form-data"
}
})
},
createFlow({ commit }, options) {
return Vue.axios.post('/api/v1/executions', options.execution).then(response => {
commit('setFlow', response.data.flow)
createFlow({commit}, options) {
return Vue.axios.post("/api/v1/executions", options.execution).then(response => {
commit("setFlow", response.data.flow)
})
},
followExecution(_, options) {
return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/executions/${options.id}/follow`, { format: 'json' })
return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/executions/${options.id}/follow`, {format: "json"})
},
followLogs(_, options) {
return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/logs/${options.id}/follow`, { format: 'json', params: options.params })
return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/logs/${options.id}/follow`, {format: "json", params: options.params})
},
loadTree({ commit }, execution) {
loadTree({commit}, execution) {
return Vue.axios.get(`/api/v1/executions/${execution.id}/tree`).then(response => {
commit('setDataTree', response.data.tasks)
commit("setDataTree", response.data.tasks)
})
},
loadLogs({ commit }, options) {
loadLogs({commit}, options) {
return Vue.axios.get(`/api/v1/logs/${options.executionId}`, {
params: options.params
}).then(response => {
commit('setLogs', response.data)
commit("setLogs", response.data)
})
}
},

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
state: {
@@ -10,58 +10,58 @@ export default {
},
actions: {
findFlows({ commit }, options) {
const sortString = options.sort ? `?sort=${options.sort}` : ''
findFlows({commit}, options) {
const sortString = options.sort ? `?sort=${options.sort}` : ""
delete options.sort
return Vue.axios.get(`/api/v1/flows/search${sortString}`, {
params: options
}).then(response => {
commit('setFlows', response.data.results)
commit('setTotal', response.data.total)
commit("setFlows", response.data.results)
commit("setTotal", response.data.total)
return response.data;
})
},
loadFlow({ commit }, options) {
loadFlow({commit}, options) {
return Vue.axios.get(`/api/v1/flows/${options.namespace}/${options.id}`).then(response => {
commit('setFlow', response.data)
commit("setFlow", response.data)
return response.data;
})
},
saveFlow({ commit }, options) {
saveFlow({commit}, options) {
return Vue.axios.put(`/api/v1/flows/${options.flow.namespace}/${options.flow.id}`, options.flow).then(response => {
if (response.status >= 300) {
return Promise.reject(new Error("Server error on flow save"))
} else {
commit('setFlow', response.data)
commit("setFlow", response.data)
return response.data;
}
})
},
createFlow({ commit }, options) {
return Vue.axios.post('/api/v1/flows', options.flow).then(response => {
commit('setFlow', response.data)
createFlow({commit}, options) {
return Vue.axios.post("/api/v1/flows", options.flow).then(response => {
commit("setFlow", response.data)
return response.data;
})
},
deleteFlow({ commit }, flow) {
deleteFlow({commit}, flow) {
return Vue.axios.delete(`/api/v1/flows/${flow.namespace}/${flow.id}`).then(() => {
commit('setFlow', null)
commit("setFlow", null)
})
},
loadTree({ commit }, flow) {
loadTree({commit}, flow) {
return Vue.axios.get(`/api/v1/flows/${flow.namespace}/${flow.id}/tree`).then(response => {
commit('setDataTree', response.data.tasks)
commit("setDataTree", response.data.tasks)
return response.data.tasks;
})
},
loadRevisions({ commit }, options) {
loadRevisions({commit}, options) {
return Vue.axios.get(`/api/v1/flows/${options.namespace}/${options.id}/revisions`).then(response => {
commit('setRevisions', response.data)
commit("setRevisions", response.data)
return response.data;
})

View File

@@ -5,17 +5,17 @@ export default {
configurationPanelPosition: undefined,
},
actions: {
updateConfigurationPanelPosition({ commit, state }) {
updateConfigurationPanelPosition({commit, state}) {
if (state.node) {
const position = {
right: state.node.getCTM().e + 95,
top: state.node.getCTM().f + 10
}
commit('setConfigurationPanelPosition', position)
commit("setConfigurationPanelPosition", position)
}
},
setNode({commit}, node) {
commit('setNode', node)
commit("setNode", node)
// dispatch('updateConfigurationPanelPosition')
},
},

View File

@@ -1,16 +1,16 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
state: {
logs: undefined,
total: 0,
level: 'INFO'
level: "INFO"
},
actions: {
findLogs({ commit }, options) {
return Vue.axios.get(`/api/v1/logs/search`, { params: options }).then(response => {
commit('setLogs', response.data.results)
commit('setTotal', response.data.total)
findLogs({commit}, options) {
return Vue.axios.get("/api/v1/logs/search", {params: options}).then(response => {
commit("setLogs", response.data.results)
commit("setTotal", response.data.total)
})
},
},

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
state: {
@@ -6,9 +6,9 @@ export default {
},
actions: {
loadNamespaces({ commit }, options) {
loadNamespaces({commit}, options) {
return Vue.axios.get(`/api/v1/${options.dataType}s/distinct-namespaces`).then(response => {
commit('setNamespaces', response.data)
commit("setNamespaces", response.data)
})
},
},

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
@@ -7,14 +7,14 @@ export default {
plugins: undefined,
},
actions: {
list({ commit }) {
return Vue.axios.get(`/api/v1/plugins`).then(response => {
commit('setPlugins', response.data)
list({commit}) {
return Vue.axios.get("/api/v1/plugins").then(response => {
commit("setPlugins", response.data)
})
},
load({ commit }, options) {
load({commit}, options) {
return Vue.axios.get(`/api/v1/plugins/${options.cls}`).then(response => {
commit('setPlugin', response.data)
commit("setPlugin", response.data)
})
},

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
state: {
@@ -7,23 +7,23 @@ export default {
taskRunDaily: undefined
},
actions: {
dailyGroupByFlow({ commit }, payload) {
return Vue.axios.post(`/api/v1/stats/executions/daily/group-by-flow`, payload).then(response => {
commit('setDailyGroupByFlow', response.data)
dailyGroupByFlow({commit}, payload) {
return Vue.axios.post("/api/v1/stats/executions/daily/group-by-flow", payload).then(response => {
commit("setDailyGroupByFlow", response.data)
return response.data;
})
},
daily({ commit }, payload) {
return Vue.axios.post(`/api/v1/stats/executions/daily`, payload).then(response => {
commit('setDaily', response.data)
daily({commit}, payload) {
return Vue.axios.post("/api/v1/stats/executions/daily", payload).then(response => {
commit("setDaily", response.data)
return response.data;
})
},
taskRunDaily({ commit }, payload) {
return Vue.axios.post(`/api/v1/stats/taskruns/daily`, payload).then(response => {
commit('setTaskRunDaily', response.data)
taskRunDaily({commit}, payload) {
return Vue.axios.post("/api/v1/stats/taskruns/daily", payload).then(response => {
commit("setTaskRunDaily", response.data)
return response.data;
})

View File

@@ -1,16 +1,16 @@
import auth from './auth'
import core from './core'
import execution from './executions'
import flow from './flow'
import graph from './graph'
import layout from './layout'
import log from './logs'
import namespace from './namespaces'
import plugin from './plugins'
import settings from './settings'
import stat from './stat'
import template from './template'
import taskrun from './taskruns'
import auth from "./auth"
import core from "./core"
import execution from "./executions"
import flow from "./flow"
import graph from "./graph"
import layout from "./layout"
import log from "./logs"
import namespace from "./namespaces"
import plugin from "./plugins"
import settings from "./settings"
import stat from "./stat"
import template from "./template"
import taskrun from "./taskruns"
export default {
modules: {

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
state: {
@@ -7,21 +7,21 @@ export default {
maxTaskRunSetting: 100
},
actions: {
findTaskRuns({ commit }, options) {
findTaskRuns({commit}, options) {
const sort = options.sort
delete options.sort
let sortQueryString = ''
let sortQueryString = ""
if (sort) {
sortQueryString = `?sort=${sort}`
}
return Vue.axios.get(`/api/v1/taskruns/search${sortQueryString}`, { params: options }).then(response => {
commit('setTaskruns', response.data.results)
commit('setTotal', response.data.total)
return Vue.axios.get(`/api/v1/taskruns/search${sortQueryString}`, {params: options}).then(response => {
commit("setTaskruns", response.data.results)
commit("setTotal", response.data.total)
})
},
maxTaskRunSetting({ commit }) {
return Vue.axios.get(`/api/v1/taskruns/maxTaskRunSetting`).then(response => {
commit('setMaxTaskRunSetting', response.data)
maxTaskRunSetting({commit}) {
return Vue.axios.get("/api/v1/taskruns/maxTaskRunSetting").then(response => {
commit("setMaxTaskRunSetting", response.data)
})
}
},

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from "vue"
export default {
namespaced: true,
state: {
@@ -8,46 +8,46 @@ export default {
},
actions: {
findTemplates({ commit }, options) {
const sortString = options.sort ? `?sort=${options.sort}` : ''
findTemplates({commit}, options) {
const sortString = options.sort ? `?sort=${options.sort}` : ""
delete options.sort
return Vue.axios.get(`/api/v1/templates/search${sortString}`, {
params: options
}).then(response => {
commit('setTemplates', response.data.results)
commit('setTotal', response.data.total)
commit("setTemplates", response.data.results)
commit("setTotal", response.data.total)
return response.data;
})
},
loadTemplate({ commit }, options) {
loadTemplate({commit}, options) {
return Vue.axios.get(`/api/v1/templates/${options.namespace}/${options.id}`).then(response => {
commit('setTemplate', response.data)
commit("setTemplate", response.data)
return response.data;
})
},
saveTemplate({ commit }, options) {
saveTemplate({commit}, options) {
return Vue.axios.put(`/api/v1/templates/${options.template.namespace}/${options.template.id}`, options.template).then(response => {
if (response.status >= 300) {
return Promise.reject(new Error("Server error on template save"))
} else {
commit('setTemplate', response.data)
commit("setTemplate", response.data)
return response.data;
}
})
},
createTemplate({ commit }, options) {
return Vue.axios.post('/api/v1/templates', options.template).then(response => {
commit('setTemplate', response.data)
createTemplate({commit}, options) {
return Vue.axios.post("/api/v1/templates", options.template).then(response => {
commit("setTemplate", response.data)
return response.data;
})
},
deleteTemplate({ commit }, template) {
deleteTemplate({commit}, template) {
return Vue.axios.delete(`/api/v1/templates/${template.namespace}/${template.id}`).then(() => {
commit('setTemplate', null)
commit("setTemplate", null)
})
},
},

View File

@@ -5,18 +5,18 @@ export function tooltip(tooltipModel) {
const bodyLines = (tooltipModel.body || []).map(r => r.lines);
if (tooltipModel.body) {
let innerHtml = '';
let innerHtml = "";
titleLines.forEach(function (title) {
innerHtml += '<h6>' + title + '</h6>';
innerHtml += "<h6>" + title + "</h6>";
});
bodyLines.forEach(function (body, i) {
let colors = tooltipModel.labelColors[i];
let style = 'background:' + colors.backgroundColor;
style += '; border-color:' + colors.borderColor;
let span = '<span class="square" style="' + style + '"></span>';
innerHtml += span + body + '<br />';
let style = "background:" + colors.backgroundColor;
style += "; border-color:" + colors.borderColor;
let span = "<span class=\"square\" style=\"" + style + "\"></span>";
innerHtml += span + body + "<br />";
});
return innerHtml;
@@ -36,7 +36,7 @@ export function defaultConfig(overide) {
}
},
tooltips: {
mode: 'index',
mode: "index",
intersect: false,
enabled: false,
},

View File

@@ -19,7 +19,7 @@ export function canSaveFlowTemplate(isEdit, user, item, dataType) {
export function saveFlowTemplate(self, file, dataType) {
return self.$store
.dispatch(`${dataType}/save${dataType.capitalize()}`, { [dataType]: file })
.dispatch(`${dataType}/save${dataType.capitalize()}`, {[dataType]: file})
.then((response) => {
self.$toast().saved(response.id);
})

View File

@@ -12,5 +12,5 @@ String.prototype.hashCode = function () {
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash + '';
return hash + "";
}

View File

@@ -22,11 +22,11 @@ export default class Markdown {
breaks: true,
linkify: true,
typographer: true,
langPrefix: 'language-',
quotes: '“”‘’',
langPrefix: "language-",
quotes: "“”‘’",
})
md.renderer.rules.table_open = () => `<table class="table table-bordered">\n`
md.renderer.rules.table_open = () => "<table class=\"table table-bordered\">\n"
return md.render(
markdown

View File

@@ -34,13 +34,13 @@ export default class QueryBuilder {
query.push(QueryBuilder.toLucene(q.q));
}
return query.join(" AND ") || '*'
return query.join(" AND ") || "*"
}
static logQueryBuilder(route) {
const q = route.query
const start = q.start ? iso(q.start) : '*'
const end = q.end ? iso(q.end) : '*'
const start = q.start ? iso(q.start) : "*"
const end = q.end ? iso(q.end) : "*"
return [
`${q.q ? QueryBuilder.toLucene(q.q) : "*"}`,
`timestamp:[${start} TO ${end}]`,

View File

@@ -58,11 +58,11 @@ export default class State {
static icon() {
return {
[State.CREATED]: "pause-circle-outline",
[State.SUCCESS]: 'check-circle-outline',
[State.RUNNING]: 'play-circle-outline',
[State.FAILED]: 'close-circle-outline',
[State.KILLING]: 'close-circle-outline',
[State.KILLED]: 'stop-circle-outline'
[State.SUCCESS]: "check-circle-outline",
[State.RUNNING]: "play-circle-outline",
[State.FAILED]: "close-circle-outline",
[State.KILLING]: "close-circle-outline",
[State.KILLED]: "stop-circle-outline"
};
}
}

View File

@@ -5,7 +5,7 @@ export default {
return {
_wrap: function(message) {
return [self.$createElement('span', {domProps: {innerHTML: message}})];
return [self.$createElement("span", {domProps: {innerHTML: message}})];
},
confirm: function(message, callback) {
return self.$bvModal

View File

@@ -12,9 +12,9 @@ export default class Utils {
return []
.concat(...Object
.keys(child)
.map(key => typeof child[key] === 'object' ?
.map(key => typeof child[key] === "object" ?
_flatten(child[key], path.concat([key])) :
({ [path.concat([key]).join(".")] : child[key] })
({[path.concat([key]).join(".")] : child[key]})
)
);
}(object));
@@ -31,12 +31,12 @@ export default class Utils {
if (typeof(flat[key]) === "string") {
let date = moment(flat[key], moment.ISO_8601);
if (date.isValid()) {
return {key, value: date.format('LLLL')};
return {key, value: date.format("LLLL")};
}
}
if (typeof(flat[key]) === "number") {
return {key, value: flat[key].toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ')};
return {key, value: flat[key].toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1 ")};
}
return {key, value: flat[key]};

View File

@@ -1,4 +1,4 @@
import JsYaml from 'js-yaml';
import JsYaml from "js-yaml";
import _cloneDeep from "lodash/cloneDeep";
export default class YamlUtils {
@@ -26,7 +26,7 @@ export default class YamlUtils {
} else if (typeof(value) === "string" || value instanceof String) {
value = value
.replaceAll("\t", " ")
.replaceAll(/\u00A0/g, ' ');
.replaceAll(/\u00A0/g, " ");
if (value.indexOf("\\n") >= 0) {
return value.replaceAll("\\n", "\n") + "\n";

View File

@@ -1,4 +1,4 @@
const path = require('path');
const path = require("path");
module.exports = {
publicPath: "/ui/",
@@ -6,18 +6,14 @@ module.exports = {
configureWebpack: {
resolve: {
alias: {
Override: path.resolve(__dirname, 'src/override/')
Override: path.resolve(__dirname, "src/override/")
}
},
module: {
rules: [
{
test: /\.sass$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
use: ["vue-style-loader", "css-loader", "sass-loader"]
}
]
},