diff options
Diffstat (limited to 'test-scheduler/ui/src/components')
-rw-r--r-- | test-scheduler/ui/src/components/editor/editor.vue | 141 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/editor/flow.vue | 340 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/editor/step.vue | 222 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/env_component/api_param.vue | 63 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/env_component/base_input.vue | 20 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/env_component/service_api.vue | 112 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/env_component/service_modal.vue | 193 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/environment.vue | 239 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/message/showMessage.js | 30 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/testcase.vue | 349 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/testcase_content.vue | 215 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/testsuite.vue | 369 | ||||
-rw-r--r-- | test-scheduler/ui/src/components/workflow_graph/wfresult.vue | 236 |
13 files changed, 2529 insertions, 0 deletions
diff --git a/test-scheduler/ui/src/components/editor/editor.vue b/test-scheduler/ui/src/components/editor/editor.vue new file mode 100644 index 00000000..e23b6d82 --- /dev/null +++ b/test-scheduler/ui/src/components/editor/editor.vue @@ -0,0 +1,141 @@ +<template> +<div class="col-md-offset-1 col-md-10" style="margin-top: 20px;"> + <ul class="nav nav-tabs"> + <li class="active"><a data-toggle="tab" data-target="#step-pane">Step</a></li> + <li><a data-toggle="tab" data-target="#flow-pane">Flow</a></li> + </ul> + <div class="tab-content"> + <div id="step-pane" class="tab-pane active fade in"> + <br> + <div class="row"> + <div class='col-md-12'> + <step v-bind:stepList="stepList" v-on:stepList="getStepList"></step> + </div> + </div> + </div> + <div id="flow-pane" class="tab-pane fade"> + <br> + <div class="row"> + <div class='col-md-12'> + <div class="row"> + <button style='margin-left:20px; margin-bottom: 30px;' class="btn btn-success" type="button" id="new-flow" v-on:click='addSubflow'> <span class="bold">ADD FLOW</span></button> + </div> + <div class='row'> + <div class='col-md-2'> + <ul id="flow-tabs"> + <li class="active"><a data-toggle="tab" data-target="#flow-main">main</a></li> + <li v-for="subflow in subflowList"><a data-toggle="tab" v-bind:data-target="'#' + subflow.tabId">{{ subflow.name }}</a></li> + </ul> + </div> + <div class="col-md-10"> + <div class="tab-content"> + <div id="flow-main" class="tab-pane active fade in"> + <flow v-model='mainflowName' v-bind:orderList='mainOrdersList' v-bind:stepsRefs='stepNameList' v-bind:flowsRefs='flowNameList' v-on:orderList='updateOrderList($event, mainflowName)'> + </flow> + </div> + <div v-for="subflow in subflowList" v-bind:id="subflow.tabId" class="tab-pane fade"> + <flow v-model='subflow.name' v-bind:orderList='subflow.orderList' v-bind:stepsRefs='stepNameList' v-bind:flowsRefs='flowNameList' v-on:orderList='updateOrderList($event, subflow.name)'></flow> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</div> +</template> +<script> +import '../../assets/css/editor.css' +import step from './step.vue' +import flow from './flow.vue' +import showMessage from '../message/showMessage.js' +export default { + name: 'editor', + props: ['saveSignal', 'stepList', 'mainOrdersList', 'subflowList'], + model: { + prop: 'saveSignal', + event: 'saveResponse' + }, + data: function(){ + return { + mainflowName: 'main' + } + }, + components: { + step, + flow + }, + methods: { + getStepList: function(stepList) { + this.stepList = stepList; + }, + addSubflow: function() { + var tabid = "flow-" + Math.floor(Math.random()*(1000000)); + this.subflowList.push({'tabId': tabid, 'name': 'fake', 'orderList': []}); + }, + updateOrderList: function(orderList, flowName) { + if(flowName == 'main') { + this.mainOrdersList = orderList; + } else { + for(var i = 0; i < this.subflowList.length; ++i) { + if(this.subflowList[i].name == flowName) { + this.subflowList[i].orderList = orderList; + } + } + } + } + }, + computed: { + flowNameList: function() { + var stepNameArr = []; + for(var i = 0; i < this.subflowList.length; i++) { + stepNameArr.push(this.subflowList[i].name); + } + console.log(stepNameArr); + return stepNameArr; + }, + stepNameList: function() { + var stepNameArr = []; + for(var i = 0; i < this.stepList.length; i++) { + stepNameArr.push(this.stepList[i].name); + } + return stepNameArr; + } + }, + watch: { + saveSignal: function(newVal) { + if(newVal == true) { + console.log("editor newVal true"); + var self = this; + var msgTitle = "SAVE -- TESTCASE"; + $.ajax({ + url: this.global.SERVER_ADDR + "testcase/save", + method: "POST", + data: { + suiteName: this.$route.query.suiteName, + caseName: this.$route.query.caseName, + stepList: JSON.stringify(this.stepList), + subflowList: JSON.stringify(this.subflowList), + mainOrdersList: JSON.stringify(this.mainOrdersList) + }, + success: function(data) { + console.log("ajax save content!"); + if(data['code'] == 200) { + showMessage("success", msgTitle, "Save content successfully!"); + self.$emit('saveResponse', true); + } else { + showMessage(data['code'], msgTitle, "Failed to save content!", data['error']); + self.$emit('saveResponse', false); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, "Failed to save content!", msg); + self.$emit('saveResponse', false); + } + }); + } + } + } +} +</script> diff --git a/test-scheduler/ui/src/components/editor/flow.vue b/test-scheduler/ui/src/components/editor/flow.vue new file mode 100644 index 00000000..b89cde4d --- /dev/null +++ b/test-scheduler/ui/src/components/editor/flow.vue @@ -0,0 +1,340 @@ +<template> +<div class="row" style="border: 1px solid #cec8c8"> + <div class="col-md-12" style="padding: 10px 0 5px;"> + <div class="form-group"> + <label class="col-md-2 control-label" style="font-size: 22px;">flowName</label> + <div class="col-md-6"> + <input v-if="flowName != 'main'" type="text" class="form-control" v-model="flowName" v-on:input='$emit("editFlowName", $event.target.value)' placeholder="please input flow name." /> + <p style="font-size: 22px;" v-else>{{flowName}}</p> + </div> + </div> + </div> + <div class="col-md-12"> + <div class="ibox"> + <div class="ibox-title"> + <h5 class="text-success">Order</h5> + <div class="ibox-tools"> + <a class="collapse-link"> + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + </div> + </div> + <div class="ibox-content"> + <div class="row"> + <button class="btn btn-success" type="button" id="new-order" v-on:click='addOrder'> <span class="bold">ADD ORDER</span></button> + <div class="col-md-3"> + <span class="select-box" > + <select id="orderSelect" class="select form-control" v-model='orderSelected' > + <option value="1">Normal</option> + <option value="2">Switch</option> + <option value="3">Parallel</option> + </select> + </span> + </div> + <br> + <!-- Normal --> + <div id="normal-panel" v-show='orderSelected == 1'> + <div class="col-lg-11" id="normalform"> + <br> + <div> + <div class="ibox border-ibox"> + <div class="ibox-title"> + <h5>Normal</h5> + <div class="ibox-tools"> + <a class="collapse-link" > + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + </div> + </div> + <div class="ibox-content"> + <!-- <div class="form-group"> + <div class="col-lg-6 row"><label>name</label><input id="NName" type="name" placeholder="name" class="form-control"></div> + </div> --> + <div class="form-group"> + <label class='headmsg control-label'>Step</label> + <div class='col-md-4'> + <select class="chooseStep form-control" id="NStep" v-model='normalStep'> + <option></option> + <option v-for='stepRef in stepsRefs' v-bind:value='stepRef'>{{ stepRef }}</option> + </select> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <!-- Switch --> + <div id="switch-panel" v-show='orderSelected == 2'> + <br> + <div class="row"> + <div class="col-lg-11"> + <div class="ibox border-ibox float-e-margins"> + <div class="ibox-title"> + <h5>Switch</h5> + <div class="ibox-tools"> + <button class="btn btn-success " type="button" id="new-case" v-on:click='addNewCase'> <span class="bold">New Case</span></button> + <a class="collapse-link" > + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + </div> + </div> + <div class="ibox-content"> + <div class="row"> + <div class="row"> + <div class="form-group"> + <label class="headmsg control-label">Value:</label> + <div class="col-md-5"><input type="text" class="case form-control" v-model='switchValue'></div> + </div> + </div> + <div class="row"> + <label class="headmsg control-label">Cases:</label> + <div class='col-md-12 row'> + <div class='col-md-offset-1' v-for='switchCase in switchCases' style='border-left-style: solid; border-left-color: gray; margin-bottom: 30px;'> + <div class="row"> + <div class="form-group"> + <label class="headmsg control-label">CaseValue:</label> + <div class="col-md-5"><input type="text" class="case form-control" v-model='switchCase.value'></div> + </div> + </div> + <div class="row"> + <label class="headmsg control-label">Case:</label> + <div class="col-md-3"> + <select class="myselect chooseStep form-control" v-model='switchCase.orderType'> + <option></option> + <option v-for='orderType in ["step", "flow"]' v-bind:value='orderType'>{{ orderType }}</option> + </select> + </div> + <div class="col-md-4"> + <select class="myselect chooseStep form-control" v-model='switchCase.orderValue'> + <option></option> + <option v-if='switchCase.orderType == "step"' v-for='stepRef in stepsRefs' v-bind:value='stepRef'>{{ stepRef }}</option> + <option v-if='switchCase.orderType == "flow"' v-for='flowRef in filtedFlowsRefs' v-bind:value='flowRef'>{{ flowRef }}</option> + </select> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <!-- Parallel --> + <div id="parallel-panel" v-show='orderSelected == 3'> + <br> + <div class="row"> + <div class="col-lg-11"> + <div class="ibox border-ibox float-e-margins"> + <div class="ibox-title"> + <h5>Parallel</h5> + <div class="ibox-tools"> + <button class="btn btn-success " type="button" id="para-step" v-on:click='addNewBranch'> <span class="bold"><i class='fa fa-plus-square-o'></i> Branch</span></button> + <a class="collapse-link" > + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + </div> + </div> + <div id="parallel"> + <div class="ibox-content"> + <div class='row'> + <div class='row'><label class="headmsg control-label">Branches:</label></div> + <div class='row'> + <div class='col-md-offset-1'> + <div v-for='branch in parallelBranches' class="row" style='border-left-style: solid; border-left-color: gray; margin-bottom: 20px; padding-top: 7px; padding-bottom: 7px;'> + <label class="headmsg control-label">Branch:</label> + <div class="col-md-3"> + <select class="myselect chooseStep form-control" v-model='branch.orderType'> + <option></option> + <option v-for='orderType in ["step", "flow"]' v-bind:value='orderType'>{{ orderType }}</option> + </select> + </div> + <div class="col-md-4"> + <select class="myselect chooseStep form-control" v-model='branch.orderValue'> + <option></option> + <option v-if='branch.orderType == "step"' v-for='stepRef in stepsRefs' v-bind:value='stepRef'>{{ stepRef }}</option> + <option v-if='branch.orderType == "flow"' v-for='flowRef in filtedFlowsRefs' v-bind:value='flowRef'>{{ flowRef }}</option> + </select> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="ibox"> + <div class="ibox-title"> + <h5 class="text-success">OrderList</h5> + <div class="ibox-tools"> + <a class="collapse-link" > + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + </div> + </div> + <div class="ibox-content" id="order-list"> + <div v-for='(order, index) in orderList' class='ibox float-e-margins' style='margin-bottom: 0;'> + <div class="ibox-title step"> + <h5>Order #{{index+1}} <strong style='margin-left: 20px;'>{{['normal', 'switch', 'parallel'][order.type - 1]}}</strong> </h5> + <div class="ibox-tools"> + <a class="collapse-link" > + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + <a class="close-link" v-on:click='removeOrder'> + <i class="fa fa-times"></i> + </a> + </div> + </div> + <div class="ibox-content"> + <div v-if='order.type == 1' class="row"> + <label class="control-label" style='padding-right: 20px;'> step: </label> + <label class="control-label"> {{ order.step }}</label> + </div> + <div v-if='order.type == 2' class="row"> + <div> + <label class="control-label" style='padding-right: 20px;'> value: </label> + <label class="control-label"> {{order.value}} </label> + </div> + <div><label class="control-label"> cases: </label></div> + <div v-for='sCase in order.cases' class="row"> + <label class="control-label col-md-offset-1"> -- <b>{{ sCase.value }}</b> : </label> + <label class='coltrol-label' style='margin-left: 20px;'>{{ sCase.orderValue }} [{{ sCase.orderType }}]</label> + </div> + </div> + <div v-if='order.type == 3' class="row"> + <div><label class="control-label"> </label></div> + <div v-for='branch in order.branches' class="row"> + <label class="control-label col-md-offset-1" style='padding-right: 20px;'> -- branch: </label> + <label class="control-label"> {{ branch.orderValue }} [{{ branch.orderType }}]</label> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</div> +</template> +<script> +export default { + name: 'flow', + props: ['stepsRefs', 'flowsRefs', 'flowName', 'orderList'], + model: { + prop: 'flowName', + event: 'editFlowName' + }, + data: function() { + return { + normalStep: '', + switchValue: '', + switchCases: [{'value': '', 'orderType': '', 'orderValue': ''}], + parallelBranches: [{'orderType': '', 'orderValue': ''}], + orderSelected: 1 + } + }, + mounted: function(){ + // this.selectPluginUpdate(); + }, + updated: function(){ + // this.selectPluginUpdate(); + }, + methods: { + addOrder: function(){ + var select = this.orderSelected; + if(select == 1){ + if(this.normalStep == ""){ + alert("Not completed!!!"); + return; + } + var temp = {type:1,step:this.normalStep}; + this.orderList.push(temp); + this.normalStep = ''; + } + if(select == 2){ + var caseList = []; + for(var i=0; i<this.switchCases.length; ++i){ + var caseValue = this.switchCases[i].value; + if(caseValue == ""){ + alert("Not completed!!!"); + return; + } + var caseOrderType = this.switchCases[i].orderType; + if(caseOrderType == ""){ + alert("Not completed!!!"); + return; + } + var caseOrderValue = this.switchCases[i].orderValue; + if(caseOrderValue == ""){ + alert("Not completed!!!"); + return; + } + caseList.push({value: caseValue, orderType: caseOrderType, orderValue: caseOrderValue}); + } + temp = {type:2, value: this.switchValue, cases:caseList}; + this.orderList.push(temp); + this.switchValue = ''; + this.switchCases = [{value: '', orderType: '', orderValue: ''}]; + } + if(select == 3){ + var branchList = []; + var allStep = $('#parallel .chooseStep'); + for(var i=0; i<this.parallelBranches.length; ++i){ + var branchOrderType = this.parallelBranches[i].orderType; + if(branchOrderType == ""){ + alert("Not completed!!!"); + return; + } + var branchOrderValue = this.parallelBranches[i].orderValue; + if(branchOrderValue == ""){ + alert("Not completed!!!"); + return; + } + branchList.push({orderType: branchOrderType, orderValue: branchOrderValue}); + } + temp = {type:3,branches:branchList}; + this.orderList.push(temp); + this.parallelBranches = [{orderType: '', orderValue: ''}]; + } + this.$emit("orderList", this.orderList); + }, + removeOrder: function(index){ + this.orderList.splice(index, 1); + }, + addNewCase: function() { + this.switchCases.push({value: '', orderType: '', orderValue: ''}); + }, + addNewBranch: function() { + this.parallelBranches.push({step: ''}); + }, + collapseBox: function(event) { + console.log("collapse"); + var ele = event.target; + console.log(event); + console.log(ele); + var ibox = $(ele).closest('div.ibox'); + var content = ibox.children('.ibox-content'); + content.slideToggle(200); + $(ele).toggleClass('fa-chevron-up').toggleClass('fa-chevron-down'); + } + }, + computed: { + filtedFlowsRefs: function() { + var subflowNameArr = []; + for(var i = 0; i < this.flowsRefs.length; i++) { + if(this.flowsRefs[i] != this.flowName) { + subflowNameArr.push(this.flowsRefs[i]); + } + } + return subflowNameArr; + } + } +} +</script> diff --git a/test-scheduler/ui/src/components/editor/step.vue b/test-scheduler/ui/src/components/editor/step.vue new file mode 100644 index 00000000..95af8429 --- /dev/null +++ b/test-scheduler/ui/src/components/editor/step.vue @@ -0,0 +1,222 @@ +<template> +<!-- step --> +<div class="row"> + <div class="col-md-6 col-sm-12"> + <div class="ibox float-e-margins"> + <div class="ibox-title"> + <h5 class="text-success">Step</h5> + <div class="ibox-tools" style="height: 25px;"> + <button class="btn btn-success " type="button" id="new-step"> <span class="bold">New Step</span></button> + <a class="collapse-link" > + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + </div> + </div> + <div class="ibox-content" style="border: 1px solid #cec8c8"> + <form class="form-horizontal"> + <div class="row"> + <div class="form-group"> + <label class="col-md-2 control-label">Name:</label> + <div class="col-md-5"><input type="text" class="form-control" id="name"></div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">Service:</label> + <div class="col-md-4"> + <select class="form-control" id="service"> + <option></option> + <option v-for='service in dataService' v-bind:value='service'>{{service}}</option> + </select> + </div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">Action:</label> + <div class="col-md-4"> + <select class="form-control" id="action"> + <option></option> + <option v-for='action in dataAction' v-bind:value='action.name'>{{action.name}}</option> + </select> + </div> + </div> + </div> + <div class="row" id="parameter"> + <div class="form-group" v-for='(param, index) in dataParam'> + <label class="col-md-2 control-label" v-bind:title="param.description">{{ param.name }} + </label> + <div class="col-md-5"> + <input type="text" class="form-control" v-bind:placeholder="param.description" v-bind:id="'par'+index"> + </div> + </div> + </div> + </form> + </div> + </div> + </div> + <div class="col-md-6 col-sm-12"> + <div class="ibox float-e-margins"> + <div class="ibox-title"> + <h5 class="text-success">StepList</h5> + <div class="ibox-tools"> + <a class="collapse-link"> + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + </div> + </div> + <div class="ibox-content" id="step-list" style="border: 1px solid #cec8c8"> + <div v-for='(step, index) in stepList' class='ibox'> + <div class="ibox-title step"> + <h5>Step{{index + 1}} {{step.name}} </h5> + <div class="ibox-tools"> + <a class="collapse-link"> + <i class="fa fa-chevron-up" v-on:click.stop='collapseBox'></i> + </a> + <a class="close-link" v-on:click='removeStep(index)'> + <i class="fa fa-times"></i> + </a> + </div> + </div> + <div class="ibox-content"> + <div class="row"> + <label class="control-label"><span style='padding-right: 20px;'>Service:</span> {{ step.service }}</label> + </div> + <div class="row"> + <label class="control-label"><span style='padding-right: 20px;'>Action:</span> {{ step.action }}</label> + </div> + <div class="param row"> + <label class="control-label"> + <span style='padding-right: 20px;'>Parameter:</span> + <span v-for='param in step.params'>{{param.key}} = {{param.value}} ; </span> + </label> + </div> + </div> + </div> + </div> + </div> + </div> +</div> +</template> +<script> +export default { + name: 'step', + props: ['stepList'], + data: function() { + return { + dataService: [], + dataAction: [], + dataParam: [] + } + }, + mounted: function() { + this.getServiceList(); + var self = this; + $("#service").change(function(){ + self.selectService(); + }); + $("#action").change(function(){ + self.selectAction(); + }); + $('#new-step').click(function(){ + self.newStep(); + }); + }, + methods: { + getServiceList: function(){ + console.log("get serviceList!"); + var self = this; + $.ajax({ + url: this.global.SERVER_ADDR + "service/list", + method: "GET", + async:false, + success: function(data){ + console.log("ajax success!"); + if(data['code'] == 200){ + self.dataService = []; + self.dataService = data['result']; + } + } + }); + }, + getServiceContent: function(name){ + var self = this; + $.ajax({ + url: this.global.SERVER_ADDR + "service/content", + method: "GET", + async:false, + data: { + "serviceName": name + }, + success: function(data){ + if(data['code'] == 200){ + self.dataAction = []; + self.dataAction = data['result']['actions']; + } + } + }); + }, + getParams: function(name){ + for(var i in this.dataAction){ + if(this.dataAction[i].name == name){ + this.dataParam = []; + this.dataParam = this.dataAction[i].params; + break; + } + } + }, + selectService: function(event){ + var selectedName = $("#service").val(); + this.getServiceContent(selectedName); + }, + selectAction: function(event){ + var selectedName = $("#action").val(); + if(selectedName=="") { + this.dataParam = []; + return; + } + this.getParams(selectedName); + if(this.dataParam==undefined) this.dataParam = []; + }, + newStep: function(){ + var ser = $("#service").val(); + var act = $("#action").val(); + var na = $("#name").val(); + if(ser==""||act==""||na==""){ + alert('Not completed!'); + return; + } + var parCount = this.dataParam.length; + var par = []; + for(var i=0; i<parCount; ++i){ + var temp = $('#par'+i).val(); + if(temp==""){ + alert('Not completed!'); + return; + } + var name = this.dataParam[i].name; + par.push({key: name, value: temp}); + } + this.stepList.push({name: na, service: ser, action: act, params: par}); + $("#name").val(""); + $("#service").val(""); + this.dataAction = []; + this.dataParam = []; + }, + removeStep: function(index) { + this.stepList.splice(index, 1); + }, + collapseBox: function(event) { + console.log("collapse"); + var ele = event.target; + console.log(event); + console.log(ele); + var ibox = $(ele).closest('div.ibox'); + var content = ibox.children('.ibox-content'); + content.slideToggle(200); + $(ele).toggleClass('fa-chevron-up').toggleClass('fa-chevron-down'); + } + }, + watch: { + stepList: function() { + this.$emit('stepList', this.stepList); + } + } +} +</script> diff --git a/test-scheduler/ui/src/components/env_component/api_param.vue b/test-scheduler/ui/src/components/env_component/api_param.vue new file mode 100644 index 00000000..55eb913e --- /dev/null +++ b/test-scheduler/ui/src/components/env_component/api_param.vue @@ -0,0 +1,63 @@ +<template> + <div class="row"> + <div class="form-group"> + <label class="col-lg-3 control-label">Params</label> + <div class="col-lg-2"> + <button type="button" class="btn btn-primary btn-sm" v-on:click="addNewParam()">New</button> + </div> + </div> + <div class="form-group"> + <div class="col-lg-offset-2 col-lg-8"> + <div class="table-responsive"> + <table class="table table-bordered text-center"> + <thead> + <tr> + <th>name</th> + <th class="text-center">description</th> + <th class="text-center">operation</th> + </tr> + </thead> + <tbody> + <tr v-for="param in params"> + <td><input type="text" class="form-control text-center" style="border: 0px" v-model="param['name']"></td> + <td><input type="text" class="form-control text-center" style="border: 0px" v-bind:title="param['description']" v-model="param['description']"></td> + <td> + <button type="button" class="btn btn-white" v-on:click="deleteParam(param['name'])"> + <i class="fa fa-trash"></i> + </button> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> +</template> +<script> +export default { + props: ["params"], + data: function() { + return { + paramArr: this.params + } + }, + watch: { + paramArr: function(){ + this.$emit("params", this.paramArr); + } + }, + methods: { + addNewParam: function() { + this.params.push({'name': '', 'description': ''}); + }, + deleteParam: function(paramName) { + for(var i = 0;i < this.params.length; i++) { + if(paramName == this.params[i]['name']) { + this.params.splice(i, 1); + } + } + } + } +} +</script> diff --git a/test-scheduler/ui/src/components/env_component/base_input.vue b/test-scheduler/ui/src/components/env_component/base_input.vue new file mode 100644 index 00000000..f4bbde5c --- /dev/null +++ b/test-scheduler/ui/src/components/env_component/base_input.vue @@ -0,0 +1,20 @@ +<template> + <div class="form-group"> + <label class="col-lg-3 control-label">{{ name }}</label> + <div class="col-lg-7"> + <input type="text" class="form-control" v-bind:value="value" v-on:input="$emit('input', $event.target.value)"> + </div> + </div> +</template> +<script> +export default { + props: ['name', 'value'], + data: function() { + return { + inputName: this.name, + inputValue: this.value, + fake: "abc" + } + } +} +</script> diff --git a/test-scheduler/ui/src/components/env_component/service_api.vue b/test-scheduler/ui/src/components/env_component/service_api.vue new file mode 100644 index 00000000..f6f58d2c --- /dev/null +++ b/test-scheduler/ui/src/components/env_component/service_api.vue @@ -0,0 +1,112 @@ +<template> + <div class="panel panel-success"> + <div class="panel-heading"> + <button type="button" class="btn btn-xs btn-danger pull-right" v-on:click="deleteApi(name)">Delete</button> + <h5 class="panel-title"> + <a data-toggle="collapse" data-parent="panelParent" v-bind:href="'#' + name + '-collapse'" style="display:block;">{{ name }}</a> + </h5> + </div> + <div v-bind:id="name + '-collapse'" class="panel-collapse collapse fade"> + <div class="panel-body"> + <base-input name="name" v-model="name"></base-input> + <base-input name="method" v-model="method"></base-input> + <base-input name="baseuri" v-model="baseuri"></base-input> + <api-param v-bind:params="params"></api-param> + <div class="form-group" v-bind:class="{ 'has-error': jsonSyntaxError}"> + <label class="col-lg-3 control-label" id="templateLabel"> + Template <i class="fa fa-question-circle"></i> + </label> + <div class="col-lg-7"> + <!-- help text --> + <span id="tempHelp">Json格式文本,用于定义发送http请求的报文内容。示例如下:<br>( 其中 ((<variable>)) 为占位符,用于替换实际值 )<br>GET方式:<br>{<br> "uri" : "((baseuri))?name=((name))"<br>}<br>POST方式:<br>{<br> "uri" : "((baseuri))",<br> "body" : {<br> "name" : "((name))",<br> "account" : {<br> "id" : "((user_name))",<br> "addr" : "SH"<br> }<br> }<br>}</span> + <textarea class="form-control" style="min-height: 200px;" v-model="templateStr"></textarea> + <span class="help-block" v-show="jsonSyntaxError">Json语法错误,请检查!</span> + </div> + </div> + </div> + </div> + </div> +</template> +<script> +import base_input from "./base_input.vue" +import api_param from "./api_param.vue" +import Vue from "vue" +export default { + props: ['panelParent', 'name', 'method', 'baseuri', 'params', 'template'], + watch: { + name: function(val) { + this.$emit("name", val); + }, + method: function(val) { + this.$emit("method", val); + }, + baseuri: function(val) { + this.$emit("baseuri", val); + }, + params: function(val) { + this.$emit("params", val); + }, + templateStr: function(val) { + try { + console.log(JSON.parse(val)); + this.jsonSyntaxError = false; + this.$emit("template", JSON.parse(val)); + } catch(err) { + console.log("catch the exception templateStr"); + this.jsonSyntaxError = true; + } + } + }, + components: { + 'base-input': base_input, + 'api-param': api_param + }, + data: function() { + return { + jsonSyntaxError: false, + templateStr: JSON.stringify(this.template, null, 2) + }; + }, + methods: { + deleteApi: function(apiName) { + this.$emit("delete", apiName); + } + } +} +</script> +<style scoped> +#templateLabel:hover+div #tempHelp{ + display: block; +} +#tempHelp { + display: none; + position: absolute; + width: 90%; + min-height: 150px; + background-color: #ab2d2d; + color: white; + transition: display 1s; + text-align: left; + padding: 10px 16px; + z-index: 2; + font-size: 10px; + opacity: 0.9; +} +#tempHelp::after { + content: ''; + position: absolute; + bottom: 92%; + right: 100%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent #ab2d2d transparent transparent; +} +@media(max-width:1200px) { + #tempHelp::after { + bottom: 100%; + left: 5%; + border-color: transparent transparent #ab2d2d transparent; + } +} +</style> diff --git a/test-scheduler/ui/src/components/env_component/service_modal.vue b/test-scheduler/ui/src/components/env_component/service_modal.vue new file mode 100644 index 00000000..ea2f0cbb --- /dev/null +++ b/test-scheduler/ui/src/components/env_component/service_modal.vue @@ -0,0 +1,193 @@ +<template> + <div class="modal inmodal fade" id="myModal"> + <div class="modal-dialog modal-lg"> + <div class="modal-content animated"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal"> + <span aria-hidden="true">×</span><span class="sr-only">Close</span> + </button> + <h3 class="modal-title">{{type.service}}</h3> + </div> + <div class="modal-body"> + <div class="row"> + <form method="get" class="form-horizontal"> + <div id="service-address"> + <button class="btn btn-default">Basic</button> + <div class="form-group"> + <label class="col-sm-3 control-label">name</label> + <div class="col-sm-7"> + <input type="text" class="form-control service-title" v-model="type.service" placeholder="please input service name."> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">ip</label> + <div class="col-sm-7"><input type="text" class="form-control" v-model="ip"></div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">port</label> + <div class="col-sm-7"><input type="text" class="form-control" v-model="port"></div> + </div> + </div> + <div class="hr-line-dashed"></div> + <div id="service-apis"> + <div id="apis-nav"> + <button class="btn btn-default">Apis</button> + </div> + <br /> + <div id="api-panels" class="api col-sm-offset-1 col-sm-10"> + <div class="panel-group" id="accordion"> + <service-api v-for="api in apis" panel-parent="#accordion" v-bind="api" v-on:name="api.name = $event" v-on:method="api.method = $event" v-on:baseuri="api.baseuri = $event" v-on:params="api.params = $event" v-on:template="api.template = $event" v-on:delete="removeApi"></service-api> + </div> + <button type="button" class="btn btn-primary pull-right" v-on:click="addNewApi()">New</button> + </div> + </div> + </form> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-white" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-primary" v-on:click="save()">Save changes</button> + </div> + </div> + </div> + </div> +</template> +<script> +import service_api from "./service_api.vue"; +import showMessage from '../message/showMessage.js' +export default { + props: ['type'], + data: function() { + return { + typeTag: this.type.tag, + ip: "", + port: "", + apis: [] + } + }, + created: function() { + }, + watch: { + type: { + handler: function(newVal, oldVal) { + console.log("###########type is changed!"); + if(newVal.content) { + var content = newVal.content; + this.ip = content.ip; + this.port = content.port; + this.apis = content.apis; + console.log(this.apis); + } else { + this.resetModalData(); + } + console.log("end!"); + }, + deep: true + } + }, + methods: { + addNewApi: function() { + var newApi = {'name': 'new', 'method': 'GET', 'baseuri': '', 'params': [], 'template': {"uri": "((baseuri))"}}; + this.apis.push(newApi); + }, + removeApi: function(name) { + for(var i = 0; i < this.apis.length; i++) { + if(name == this.apis[i]['name']) { + this.apis.splice(i, 1); + } + } + }, + save: function() { + if(this.ip == "") { + showMessage("warning", "SERVICE", "ip is not filled!"); + return; + } else if (this.port == "") { + showMessage("warning", "SERVICE", "port is not filled!"); + return; + } else if (this.type.service == "") { + showMessage("warning", "SERVICE", "service name is not filled!"); + return; + } + if(this.type.edit == true) { + this.saveEdition(); + } else { + this.saveCreation(); + } + this.resetModalData(); + $("#myModal").modal("hide"); + }, + saveEdition: function() { + var self = this; + var msgTitle = "SAVE -- SERVICE"; + $.ajax({ + url: this.global.SERVER_ADDR + "env/editService", + method: "post", + data: { + "oldName": self.type.originName, + "newName": self.type.service, + "ip": self.ip, + "port": self.port, + "apis": JSON.stringify(self.apis), + }, + success: function(data) { + if(data['code'] == 200) { + showMessage("success", msgTitle, "Save service <strong>" + self.type.service + "</strong> successfully!"); + } else { + showMessage(data['code'], msgTitle, "Failed to save service <strong>" + self.type.service + "</strong>!", data['error']); + } + }, + error: function() { + showMessage("error", msgTitle, "Failed to save service <strong>" + self.type.service + "</strong>!", msg); + } + }); + var edition = { + 'oldName': this.type.originName, + 'newName': this.type.service + }; + this.$emit("service-edition", edition); + }, + saveCreation: function() { + console.log("save creation!!!"); + var self = this; + var msgTitle = "CREATE -- SERVICE"; + $.ajax({ + url: this.global.SERVER_ADDR + "env/createService", + method: "post", + data: { + "name": self.type.service, + "ip": self.ip, + "port": self.port, + "apis": JSON.stringify(self.apis) + }, + success: function(data) { + if(data['code'] == 200) { + showMessage("success", msgTitle, "Create <strong>"+ self.type.service + "</strong> successfully!"); + } else { + showMessage(data['code'], msgTitle, "Failed to create service <strong>" + self.type.service + "</strong>!", data['error']); + self.$emit("creation-fail", self.type.service); + } + }, + error: function() { + showMessage("error", msgTitle, "Failed to create service <strong>" + self.type.service + "</strong>!", msg); + self.$emit("creation-fail", self.type.service); + } + }); + this.$emit("service-creation", this.type.service); + }, + resetModalData: function() { + this.ip = ""; + this.port = ""; + this.apis = []; + }, + getData: function() { + console.log("apis:"); + for(i in this.apis) { + console.log(this.apis[i]); + } + } + }, + components: { + 'service-api': service_api + } +} +</script> diff --git a/test-scheduler/ui/src/components/environment.vue b/test-scheduler/ui/src/components/environment.vue new file mode 100644 index 00000000..2e8ee014 --- /dev/null +++ b/test-scheduler/ui/src/components/environment.vue @@ -0,0 +1,239 @@ +<template> +<div class="wrapper wrapper-content"> + <div class="row"> + <div class="col-lg-offset-2 col-lg-8"> + <div class="ibox"> + <div class="ibox-content"> + <h1>CONTEXT <i class="fa fa-question-circle"></i></h1> + <div class="row"> + <div class="col-md-offset-1 col-md-10"> + <textarea v-model="context" id="context-content" style="white-space:nowrap; overflow:scroll; font-size: 16px; padding: 4px; width: 100%; min-height: 300px; max-height: 300px;"> + </textarea> + <button type="button" class="btn btn-primary pull-right" v-on:click="saveContext()">Save</button> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-lg-offset-2 col-lg-8"> + <div class="ibox"> + <div class="ibox-content"> + <h1>Service</h1> + <div class="service-table"> + <table id="serviceList" class="table table-bordered table-hover text-center"> + <thead> + <tr> + <th class="text-center">No</th> + <th class="text-center">Service</th> + <th class="text-center">Time</th> + <th class="text-center">Operation</th> + </tr> + </thead> + <tbody> + <tr v-for="service in serviceList" v-on:click="editService(service.name)" style="cursor: pointer;"> + <td style="vertical-align: middle;">{{ service.id }}</td> + <td style="vertical-align: middle;">{{ service.name }}</td> + <td style="vertical-align: middle;">{{ service.time }}</td> + <td style="vertical-align: middle;"><button type="button" class="btn btn-white" v-on:click.stop="deleteService(service.name)"><i class="fa fa-trash"></i></button> + </td> + </tr> + </tbody> + </table> + <div> + <button class="btn btn-lg btn-success" style="float:right;" v-on:click="addNewService()"> Add </button> + </div> + </div> + <!-- modal of one service --> + <div class="row"> + <service-modal v-bind:type="type" v-on:service-creation="plusAService" v-on:service-edition="editAServiceName" v-on:creation-fail="creationFailHandler"></service-modal> + </div> + </div> + </div> + </div> + </div> +</div> +</template> +<script> +import Vue from 'vue' +import service_modal from "./env_component/service_modal.vue" +import showMessage from './message/showMessage.js' +export default { + name: 'environment', + data: function() { + return { + serviceList: [], + type: { + edit: true, + service: "ansible", + tag: "default" + }, + context: '' + } + }, + components: { + 'service-modal': service_modal + }, + created: function() { + var self = this; + var msgTitle = "GET -- SERVICE LIST"; + var errorInfo = 'Unable to get the service list'; + $.ajax({ + url: this.global.SERVER_ADDR + "env/getAllServices", + method: "GET", + success: function(data) { + if(data['code'] == 200) { + self.serviceList = data['result']; + } else { + showMessage(data['code'], msgTitle, errorInfo, data['error']); + } + }, + error: function(obj, status, msg) { + showMessage("error", msgTitle, errorInfo, msg); + } + }); + $.ajax({ + url: this.global.SERVER_ADDR + "env/getContext", + method: "GET", + success: function(data) { + if(data['code'] == 200) { + self.context = data['result']['context']; + } else { + showMessage(data['code'], msgTitle, errorInfo, data['error']); + } + }, + error: function(obj, status, msg) { + showMessage("error", msgTitle, errorInfo, msg); + } + }); + }, + methods: { + addNewService: function(){ + this.type.edit = false; + this.type.tag = "abc"; + this.type.service = null; + this.type.originName = null; + if(this.type.content){ + this.type.content = null; + } + $("#myModal").modal("show"); + }, + plusAService: function(serviceName){ + var item = {'id': '', 'name': '', 'time': ''}; + item['id'] = this.serviceList[this.serviceList.length - 1]['id'] + 1; + item['name'] = serviceName; + item['time'] = this.getFormatDate(new Date()); + this.serviceList.push(item); + }, + editAServiceName: function(edition) { + for(var i = 0;i < this.serviceList.length; i++) { + if(edition['oldName'] == this.serviceList[i]['name']) { + this.serviceList[i]['name'] = edition['newName']; + } + } + }, + editService: function(serviceName){ + this.type.edit = true; + this.type.tag = "abc"; + this.type.service = serviceName; + this.type.originName = serviceName; + if(this.type.content){ + this.type.content = null; + } + console.log(this.type); + var self = this; + var msgTitle = "GET -- SERVICE"; + var errorInfo = "Unable to get the service: <strong>" + self.type.service + "</strong>"; + $.ajax({ + url: this.global.SERVER_ADDR + "env/getService", + method: "GET", + data: { + "serviceName": serviceName + }, + success: function(data) { + if(data['code'] == 200) { + self.type.tag = "hhh"; + self.type.content = data['result']; + self.type.originName = self.type.service; + } else { + showMessage(data['code'], msgTitle, errorInfo, data['error']); + } + }, + error: function(obj, status, msg) { + showMessage("error", msgTitle, errorInfo, msg); + } + }); + $("#myModal").modal("show"); + }, + deleteService: function(serviceName){ + var msgTitle = "DELETE -- SERVICE"; + $.ajax({ + url: this.global.SERVER_ADDR + "env/deleteService", + method: "POST", + data: { + "serviceName": serviceName + }, + success: function(data) { + if(data['code'] == 200) { + showMessage(data['code'], msgTitle, "Delete <strong>" + serviceName + "</strong> successfully."); + } else { + showMessage(data['code'], msgTitle, "Failed to delete <strong>" + serviceName + "</strong>", data['error']); + } + }, + error: function(obj, status, msg) { + showMessage("error", msgTitle, "Failed to delete <strong>" + serviceName + "</strong>", msg); + } + }); + for(var i = 0;i < this.serviceList.length; i++) { + if(serviceName == this.serviceList[i]['name']) { + this.serviceList.splice(i, 1); + } + } + }, + creationFailHandler: function(serviceName) { + for(var i = 0; i < this.serviceList.length; i++) { + if(serviceName == this.serviceList[i].name) { + this.serviceList.splice(i, 1); + } + } + }, + getFormatDate: function(date) { + var year = date.getFullYear(); + var month = date.getMonth() + 1; + var strDate = date.getDate(); + var seperator = "-"; + if(month >= 1 && month <= 9) { + month = "0" + month; + } + if(strDate >= 1 && strDate <= 9) { + strDate = "0" + strDate; + } + var formatDate = year + seperator + month + seperator + strDate; + return formatDate; + }, + saveContext: function() { + var self = this; + var msgTitle = "SAVE -- CONTEXT"; + var errorInfo = "Failed to save context!"; + $.ajax({ + url: this.global.SERVER_ADDR + "env/editContext", + method: "POST", + data: { + context: self.context + }, + success: function(data) { + if(data['code'] == 200) { + showMessage(data['code'], msgTitle, "Save context successfully!"); + } else { + showMessage(data['code'], msgTitle, errorInfo, data['error']); + } + }, + error: function(obj, status, msg) { + showMessage("error", msgTitle, errorInfo, msg); + } + }); + } + } +} +</script> diff --git a/test-scheduler/ui/src/components/message/showMessage.js b/test-scheduler/ui/src/components/message/showMessage.js new file mode 100644 index 00000000..2ac203e7 --- /dev/null +++ b/test-scheduler/ui/src/components/message/showMessage.js @@ -0,0 +1,30 @@ +import toastr from '../../assets/js/toastr.min.js' +export default function(type, title, info, detail="") { + if(typeof type == "number") { + if(200 <= type && type < 300) { + type = "success"; + } else if (300 <= type && type < 500) { + type = "warning"; + } else if (500 <= type) { + type = "error"; + } else { + type = "info"; + } + } + if(detail != "") { + detail = "* <strong>detail:</strong> " + detail; + } + var content = "<br>* " + info + "<br><br>" + detail; + if(type == "success"){ + toastr.success(content, title); + } + else if(type == "info") { + toastr.info(content, title); + } + else if(type == "error"){ + toastr.error(content, title); + } + else { + toastr.warning(content, title); + } +} diff --git a/test-scheduler/ui/src/components/testcase.vue b/test-scheduler/ui/src/components/testcase.vue new file mode 100644 index 00000000..9c8ef85e --- /dev/null +++ b/test-scheduler/ui/src/components/testcase.vue @@ -0,0 +1,349 @@ +<template> + <div class="wrapper wrapper-content animated fadeIn"> + <div class="row" style="margin-bottom: 20px;"> + <div class="col-md-8"> + <ol class="breadcrumb" style="padding-left: 20px; font-size: 17px;"> + <li> + <router-link to="/" >root</router-link> + </li> + <li> + <router-link :to="{ path: '/testcase', query: { name: sname }}"><b>{{this.$route.query.name}}</b></router-link> + </li> + </ol> + </div> + </div> + <div id="page-content" style="" class="row"> + <div class="col-lg-8"> + <div class="ibox"> + <div class="ibox-title"> + <h5 style="font-size:26px;margin-top: -3px;">Test Case</h5> + <div class="ibox-tools"> + <button class="btn btn-info btn-sm my-button-sm" type="button" v-on:click="runMultiTestcase()">Run</button> + <button class="btn btn-success btn-sm my-button-sm" type="button" v-on:click="create()">Create</button> + <button class="btn btn-danger btn-sm my-button-sm" v-on:click="deleteCases()" type="button">Delete</button> + <a class="collapse-link"> + <i class="fa fa-chevron-up"></i> + </a> + <a class="fullscreen-link"> + <i class="fa fa-expand"></i> + </a> + </div> + </div> + <div class="ibox-content" style="text-align:center;"> + <table class="my-table table table-bordered" cellspacing="0" cellpadding="0" style="text-align: center;"> + <thead> + <tr> + <td class="checkbox1" style="width:20px"><input type="checkbox" v-model="selectAll"> All</td> + <td class="smallbox" style="with:250px;">TestCase Name</td> + </tr> + </thead> + <tbody> + <tr v-for="testcase in testcases"> + <td><input class="checkbox1" style="width:20px" type="checkbox" v-model="selected" :value="testcase.testcase"> </td> + <td class="smallbox" style="with:250px;"><router-link :to="{ path: '/content', query: { suiteName: sname, caseName: testcase.testcase } }">{{testcase.testcase}}</router-link></td> + </tr> + </tbody> + <tfoot id="create-box" style="display: none"> + <tr> + <td class="checkbox1" style="width:20px"><input type="checkbox"> </td> + <td class="smallbox" style="with:250px;"><input type="text" v-model="newCase" @keydown.enter="additem" ></td> + </tr> + </tfoot> + </table> + </div> + </div> + </div> + </div> + <hr /> + <div class="row"> + <div class="col-lg-12"> + <div class="ibox"> + <div class="ibox-title"> + <h5 style="font-size:26px;margin-top: -3px;">Workflow</h5> + <div class="ibox-tools"> + <a class="collapse-link"> + <i class="fa fa-chevron-up"></i> + </a> + <a class="fullscreen-link"> + <i class="fa fa-expand"></i> + </a> + </div> + </div> + <div class="ibox-content" style="padding-top: 30px;"> + <div id="executing" class="row" style="padding: 0 30px 60px;"> + <div class="col-md-offset-2 col-md-8"> + <div class="table-responsive"> + <table class="table text-center" style="margin-top: 30px;"> + <thead> + <tr> + <th class="text-center">#</th> + <th class="text-center">testcase</th> + <th class="text-center">status</th> + <th class="text-center">operation</th> + </tr> + </thead> + <tbody> + <tr v-for="testcase in runTestcases"> + <td>{{ testcase.id }}</td> + <td>{{ testcase.testcase }}</td> + <td><span class="badge" v-bind:class="'badge-' + statusClass(testcase.status)">{{ testcase.status }}</span></td> + <td> + <div style="display: inline-block;min-width: 130px;"> + <button class="btn btn-primary btn-outline btn-xs fadeIn" v-on:click="runTestcase()" v-show="testcase.status == 'failed'">rerun</button> + <button class="btn btn-primary btn-outline btn-xs fadeIn" v-on:click="runNextCase($event.target)" v-show="testcase.status == 'failed'">run next one</button> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + <hr class="hr-line-solid"> + <div class="row" style="margin-top: 60px;"> + <wfresult v-bind:workflowId="workflowId" v-bind:wfloading='wfloading' v-bind:wfJson='wfJson' v-on:wfComplete="wfComplete = $event"></wfresult> + </div> + </div> + </div> + </div> + </div> + </div> +</template> +<script> +import wfresult from './workflow_graph/wfresult.vue' +import showMessage from './message/showMessage.js' +export default { + name: 'testcase', + data () { + return { + testcases: [], + sname: this.$route.query.name, + newCase:'', + addstory:'', + workflowId: '', + wfloading: false, + wfJson: '', + selected: [], + curRunningId: 0, + runTestcases: [], + wfComplete: false + } + }, + created: function() { + var self = this; + var msgTitle = "GET -- TESTCASES"; + var errorInfo = "Failed to get testcase list."; + $.ajax({ + url: this.global.SERVER_ADDR + "testsuite/content", + method:"GET", + data:{ + suiteName: this.$route.query.name + }, + success:function (data) { + if(data['code'] == 200) { + self.testcases = data['result']; + } else { + showMessage(data['code'], msgTitle, errorInfo, data['error']); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, errorInfo, msg); + } + }); + }, + computed: { + selectAll: { + get: function () { + return this.testcases ? this.selected.length == this.testcases.length : false; + }, + set: function (value) { + var selected = []; + if (value) { + this.testcases.forEach(function (testcase) { + selected.push(testcase.testcase); + }); + } + this.selected = selected; + } + } + }, + methods:{ + create: function () { + var cbox = document.getElementById("create-box"); + cbox.style.display = "table-footer-group"; + }, + additem: function () { + var self = this; + var msgTitle = "CREATE -- TESTCASE"; + const caseName = self.newCase.trim(); + if(caseName) + { + $.ajax({ + url: this.global.SERVER_ADDR + "testcase/new", + method:"POST", + data:{ + suiteName: self.sname, + caseName: caseName + }, + success:function (data) { + if(data['code'] == 200){ + self.testcases.push({ + id: self.testcases.length + 1 , + testcase: caseName, + }); + showMessage(data['code'], msgTitle, "Create <strong>" + caseName + "</strong> succesfully!"); + } else { + showMessage(data['code'], msgTitle, "Failed to create <strong>" + caseName + "</strong>!", data['error']); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, "Failed to create <strong>" + caseName + "</strong>!", msg); + } + }); + } + var cbox = document.getElementById("create-box"); + cbox.style.display = "none"; + this.newCase = ''; + }, + deleteCases:function () { + var self = this; + var msgTitle = "DELETE -- TESTCASE"; + var deleteArr = self.selected.slice(0); + self.testcases = self.testcases.filter(item => { + for(var i in deleteArr) { + if(item.testcase == deleteArr[i]) { + return false; + } + } + return true; + }); + self.selected = []; + for(var i in deleteArr) + { + $.ajax({ + url: this.global.SERVER_ADDR + "testcase/delete", + method: "POST", + data: { + suiteName: self.sname, + caseName: deleteArr[i] + }, + success: function(data) { + if(data['code'] == 200){ + showMessage(data['code'], msgTitle, "Delete <strong>" + deleteArr[i] + "</strong> succesfully!"); + } else { + showMessage(data['code'], msgTitle, "Failed to delete <strong>" + deleteArr[i] + "</strong>!", data['error']); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, "Failed to delete <strong>" + deleteArr[i] + "</strong>!", msg); + } + }); + } + }, + runMultiTestcase: function() { + var self = this; + var msgTitle = "RUN -- TESTCASES"; + self.runTestcases = []; + if(self.selected.length == 0) { + showMessage("warning", msgTitle, "please select one!"); + return; + } + for(var i=0; i < self.selected.length; i++) { + var testcaseItem = {'id': i, 'testcase': '', 'status': "waiting"}; + testcaseItem['testcase'] = self.selected[i]; + self.runTestcases.push(testcaseItem); + } + self.curRunningId = 0; + showMessage("info", msgTitle, "start to run <strong>testcases</strong>"); + self.runOneTestcase(); + }, + runOneTestcase: function() { + var self = this; + var msgTitle = "RUN -- TESTCASE"; + if (self.curRunningId == self.runTestcases.length) { + self.curRunningId = 0; + return; + } + self.wfComplete = false; + var i = self.curRunningId; + self.runTestcases[i]['status'] = "running"; + $.ajax({ + url: self.global.SERVER_ADDR + "execute/testcase", + method: "POST", + data: { + "suiteName": self.sname, + "caseName": self.runTestcases[self.curRunningId]['testcase'] + }, + beforeSend: function(XHR) { + self.wfloading = true; + }, + success: function(data) { + if(data['code'] == 200) { + self.workflowId = data['result']['workflowId']; + $.ajax({ + url: self.global.SERVER_ADDR + "story-content", + method: "GET", + data: { + "service": self.sname, + "story": self.runTestcases[self.curRunningId]['testcase'] + }, + success: function(data) { + if(data['code'] == 200) { + self.wfJson = data['result']['content']; + } else { + showMessage(data['code'], msgTitle, "workflow.json get failed!"); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, msg); + } + }); + } else { + var i = self.curRunningId; + self.runTestcases[i]['status'] = "failed"; + self.wfloading = false; + showMessage(data['code'], msgTitle, "Failed to run <strong>" + self.runTestcases[i]['testcase'] + "</strong>", data['error']); + } + }, + error: function(obj, status, msg) { + var i = self.curRunningId; + self.runTestcases[i]['status'] = "failed"; + self.wfloading = false; + showMessage(status, msgTitle, "Failed to run <strong>" + self.runTestcases[i]['testcase'] + "</strong>", msg); + } + }); + }, + statusClass: function(status) { + if(status == "waiting") { + return "success"; + } + if(status == "running") { + return "warning"; + } + if(status == "pass") { + return "primary"; + } + if(status == "failed") { + return "danger"; + } + }, + runNextCase: function(obj) { + $(obj).parent().css({"display": "none"}); + var i = this.curRunningId++; + this.runOneTestcase(); + } + }, + watch: { + wfComplete: function(val) { + if(val == false) return; + this.wfloading = false; + var i = this.curRunningId++; + this.runTestcases[i]['status'] = "pass"; + this.runOneTestcase(); + } + }, + components: { + wfresult + } +} +</script> diff --git a/test-scheduler/ui/src/components/testcase_content.vue b/test-scheduler/ui/src/components/testcase_content.vue new file mode 100644 index 00000000..49169cee --- /dev/null +++ b/test-scheduler/ui/src/components/testcase_content.vue @@ -0,0 +1,215 @@ +<template> + <div class="wrapper wrapper-content animated fadeIn"> + <div class="row" style="margin-bottom: 20px;"> + <div class="col-md-8"> + <ol class="breadcrumb" style="padding-left: 20px; font-size: 17px;"> + <li> + <router-link to="/" >root</router-link> + </li> + <li> + <router-link :to="{ path: '/testcase', query: { name: suitename }}" >{{this.$route.query.suiteName}}</router-link> + </li> + <li> + <router-link :to="{ path: '/content', query: { suiteName: suitename, caseName: casename } }"><b>{{this.$route.query.caseName}}</b></router-link> + </li> + </ol> + </div> + </div> + <div id="page-content" class="row"> + <div class="col-lg-12"> + <div class="ibox"> + <div class="ibox-title"> + <h5 style="font-size:26px;margin-top: -3px;">Test Case Content</h5> + <div v-show="contentLoading || contentSaving" class="sk-spinner sk-spinner-circle" style="float: left;margin-left: 10px;"> + <div class="sk-circle1 sk-circle"></div> + <div class="sk-circle2 sk-circle"></div> + <div class="sk-circle3 sk-circle"></div> + <div class="sk-circle4 sk-circle"></div> + <div class="sk-circle5 sk-circle"></div> + <div class="sk-circle6 sk-circle"></div> + <div class="sk-circle7 sk-circle"></div> + <div class="sk-circle8 sk-circle"></div> + <div class="sk-circle9 sk-circle"></div> + <div class="sk-circle10 sk-circle"></div> + <div class="sk-circle11 sk-circle"></div> + <div class="sk-circle12 sk-circle"></div> + </div> + <div class="ibox-tools"> + <button class="btn btn-info btn-sm my-button-sm" type="button" v-on:click="runTestcase()">Run</button> + <button class="btn btn-success btn-sm my-button-sm" type="button" v-on:click="setEditable()">Edit</button> + <button v-show="isEditable" class="btn btn-warning btn-sm my-button-sm" v-on:click="saveTestcase()" type="button">Save</button> + <button v-show="isEditable" class="btn btn-danger btn-sm my-button-sm" v-on:click="cancelEdit()" type="button">Cancel</button> + <a class="collapse-link"> + <i class="fa fa-chevron-up"></i> + </a> + <a class="fullscreen-link"> + <i class="fa fa-expand"></i> + </a> + </div> + </div> + <div class="ibox-content" style="max-height: 600px; overflow-y: auto; padding: 0; border: 1px solid #e7e7e7;"> + <div style='text-align:center;'> + <textarea v-show='!isEditable' v-model="content" id="tc_content" style="white-space:nowrap; overflow:scroll;max-width:2400px; width: 100%;height: 100%;min-height: 500px;font-size:16px;border:none; vertical-align: middle; padding: 30px 0 20px 40px;"> + </textarea> + </div><editor v-show='isEditable' v-bind:saveSignal='saveSignal' v-bind = 'editorContent' v-on:saveResponse='processSaveResponse'></editor> + </div> + </div> + </div> + </div> + <hr /> + <div class="row"> + <div class="col-lg-12"> + <div class="ibox"> + <div class="ibox-title"> + <h5 style="font-size:26px;margin-top: -3px;">Workflow</h5> + <div class="ibox-tools"> + <a class="collapse-link"> + <i class="fa fa-chevron-up"></i> + </a> + <a class="fullscreen-link"> + <i class="fa fa-expand"></i> + </a> + </div> + </div> + <div class="ibox-content" style="padding-top: 60px;"> + <wfresult v-bind:workflowId="workflowId" v-bind:wfloading='wfloading' v-bind:wfJson='wfJson'></wfresult> + </div> + </div> + </div> + </div> + </div> +</template> +<script> +import editor from './editor/editor.vue' +import wfresult from './workflow_graph/wfresult.vue' +import showMessage from './message/showMessage.js' +export default { + name: 'testcase_content', + data () { + return { + content: '', + editorContent: {'stepList': [], 'mainOrdersList': [], 'subflowList': []}, + bakContent: '', + isEditable: false, + contentLoading: false, + contentSaving: false, + suitename:this.$route.query.suiteName, + casename:this.$route.query.caseName, + workflowId: '', + wfloading: false, + wfJson: '', + saveSignal: false + } + }, + created: function() { + this.getTestcase(); + }, + methods: { + setEditable: function(){ + this.isEditable = true; + this.bakContent = this.content; + }, + cancelEdit: function(){ + this.content = this.bakContent; + this.isEditable = false; + }, + saveTestcase: function(){ + console.log("save"); + this.saveSignal = true; + this.contentSaving = true; + }, + runTestcase: function(){ + var self = this; + var msgTitle = "RUN -- TESTCASE"; + $.ajax({ + url: this.global.SERVER_ADDR + "execute/testcase", + method: "POST", + data: { + "suiteName": this.$route.query.suiteName, + "caseName": this.$route.query.caseName + }, + beforeSend: function(XHR) { + self.wfloading = true; + showMessage("info", msgTitle, "start to run <strong>" + self.$route.query.caseName + "</strong>"); + }, + success: function(data) { + if(data['code'] == 200) { + self.workflowId = data['result']['workflowId']; + showMessage(data['code'], msgTitle, "<strong>" + self.$route.query.caseName + "</strong> finished!"); + $.ajax({ + url: self.global.SERVER_ADDR + "story-content", + method: "GET", + data: { + "service": self.$route.query.suiteName, + "story": self.$route.query.caseName + }, + success: function(data) { + if(data['code'] == 200) { + self.wfJson = data['result']['content']; + } else { + showMessage(data['code'], msgTitle, "workflow.json get failed!"); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, msg); + } + }); + } else { + self.wfloading = false; + showMessage(data['code'], msgTitle, "Failed to run <strong>" + self.$route.query.caseName + "</strong>", data['error']); + } + }, + error: function(obj, status, msg) { + self.wfloading = false; + showMessage(status, msgTitle, "Failed to run <strong>" + self.$route.query.caseName + "</strong>", msg); + } + }); + }, + getTestcase: function(){ + var self = this; + var msgTitle = "GET -- TESTCASE"; + $.ajax({ + url: this.global.SERVER_ADDR + "testcase/content", + method:"GET", + data:{ + suiteName: this.$route.query.suiteName, + caseName: this.$route.query.caseName + }, + beforeSend: function(XHR) { + self.contentLoading = true; + }, + success:function (data) { + if(data['code'] == 200) { + self.content = data['result']['content']; + self.contentLoading = false; + self.editorContent = data['result']['editorContent']; + } + else { + showMessage("error", msgTitle, "fail to load testcase content!", data['error']); + self.contentLoading = false; + } + }, + error: function (obj, status, msg) { + showMessage(status, msgTitle, "fail to load testcase content!", msg); + self.contentLoading = false; + } + }); + }, + async processSaveResponse(result) { + if(result == true) { + this.saveSignal = false; + this.isEditable = false; + this.contentSaving = false; + this.getTestcase(); + } else { + this.saveSignal = false; + this.contentSaving = false; + } + } + }, + components: { + editor, + wfresult + } +} +</script> diff --git a/test-scheduler/ui/src/components/testsuite.vue b/test-scheduler/ui/src/components/testsuite.vue new file mode 100644 index 00000000..5bdcbcc1 --- /dev/null +++ b/test-scheduler/ui/src/components/testsuite.vue @@ -0,0 +1,369 @@ +<template> + <div class="wrapper wrapper-content animated fadeIn"> + <div class="row" style="margin-bottom: 20px;"> + <div class="col-md-8"> + <ol class="breadcrumb" style="padding-left: 20px; font-size: 17px;"> + <li> + <router-link to="/" >root</router-link> + </li> + </ol> + </div> + </div> + <div id="page-content" class="row"> + <div class="col-lg-8"> + <div class="ibox"> + <div class="ibox-title"> + <h5 style="font-size:26px;margin-top: -3px;">Test Suite</h5> + <div class="ibox-tools"> + <button class="btn btn-info btn-sm my-button-sm" type="button" v-on:click="runTestsuites()">Run</button> + <button class="btn btn-success btn-sm my-button-sm" type="button" v-on:click="createSuite()">Create</button> + <button class="btn btn-danger btn-sm my-button-sm" v-on:click="deleteSuites()" type="button">Delete</button> + <a class="collapse-link"> + <i class="fa fa-chevron-up"></i> + </a> + <a class="fullscreen-link"> + <i class="fa fa-expand"></i> + </a> + </div> + </div> + <div class="ibox-content" style="text-align:center;"> + <table class="my-table table table-bordered" cellspacing="0" cellpadding="0" style="text-align: center;"> + <thead> + <tr> + <td style="width:20px"><input type="checkbox" v-model="selectAll"> All</td> + <td class="smallbox" style="with:250px;">TestSuite Name</td> + </tr> + </thead> + <tbody> + <tr v-for="testsuite in testsuites"> + <td><input style="width:20px" type="checkbox" v-model="selected" :value="testsuite.testsuite"> </td> + <td class="smallbox" style="with:250px;"><router-link :to="{ path: '/testcase', query: { name: testsuite.testsuite }}" >{{testsuite.testsuite}}</router-link></td> + </tr> + </tbody> + <tfoot id="create-box" style="display: none"> + <tr> + <td style="width:20px"><input type="checkbox"> </td> + <td class="smallbox" style="with:250px;"><input type="text" v-model="newSuite" @keydown.enter="addItem" ></td> + </tr> + </tfoot> + </table> + </div> + </div> + </div> + </div> + <hr /> + <div class="row"> + <div class="col-lg-12"> + <div class="ibox"> + <div class="ibox-title"> + <h5 style="font-size:26px;margin-top: -3px;">Workflow</h5> + <div class="ibox-tools"> + <a class="collapse-link"> + <i class="fa fa-chevron-up"></i> + </a> + <a class="fullscreen-link"> + <i class="fa fa-expand"></i> + </a> + </div> + </div> + <div class="ibox-content" style="padding-top: 30px;"> + <div id="executing" class="row" style="padding: 0 30px 60px;"> + <div class="col-md-offset-2 col-md-8"> + <div class="table-responsive"> + <table class="table text-center" style="margin-top: 30px;"> + <thead> + <tr> + <th class="text-center">#</th> + <th class="text-center">testcase</th> + <th class="text-center">status</th> + <th class="text-center">operation</th> + </tr> + </thead> + <tbody> + <tr v-for="testcase in casesInSuite"> + <td>{{ testcase.id }}</td> + <td>{{ testcase.testcase }}</td> + <td><span class="badge" v-bind:class="'badge-' + statusClass(testcase.status)">{{ testcase.status }}</span></td> + <td> + <div style="display: inline-block;min-width: 130px;"> + <button class="btn btn-primary btn-outline btn-xs fadeIn" v-on:click="runTestcase()" v-show="testcase.status == 'failed'">rerun</button> + <button class="btn btn-primary btn-outline btn-xs fadeIn" v-on:click="runNextCase($event.target)" v-show="testcase.status == 'failed'">run next one</button> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + <hr class="hr-line-solid"> + <div class="row" style="margin-top: 60px;"> + <wfresult v-bind:workflowId="workflowId" v-bind:wfloading='wfloading' v-bind:wfJson='wfJson' v-on:wfComplete="wfComplete = $event"></wfresult> + </div> + </div> + </div> + </div> + </div> + </div> +</template> +<script> +import wfresult from './workflow_graph/wfresult.vue' +import showMessage from './message/showMessage.js' +export default { + name: 'testsuite', + data () { + return { + newSuite : '', + testsuites : '', + service_selected : '', + workflowId: '', + wfloading: false, + wfJson: '', + selected: [], + casesInSuite: [], + running: { + suiteName: "", + caseName: "" + }, + curRunningId: 0, + wfComplete: false + } + }, + created: function() { + var self = this; + var msgTitle = "GET -- TESTSUITES"; + var errorInfo = "Failed to get testsuite list."; + $.ajax({ + url: this.global.SERVER_ADDR + "testsuite/list", + method:"GET", + data:{}, + success:function (data) { + if(data['code'] == 200) { + self.testsuites = data['result']; + } else { + showMessage(data['code'], msgTitle, errorInfo, data['error']); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, errorInfo, msg); + } + }); + }, + computed: { + selectAll: { + get: function () { + return this.testsuites ? this.selected.length == this.testsuites.length : false; + }, + set: function (value) { + var selected = []; + if (value) { + this.testsuites.forEach(function (story) { + selected.push(story.testsuite); + }); + } + this.selected = selected; + } + } +}, + methods:{ + createSuite: function () { + var cbox = document.getElementById("create-box"); + cbox.style.display = "table-footer-group"; + }, + addItem: function () { + var self = this; + const suiteName = self.newSuite.trim(); + if(suiteName) + { + var msgTitle = "CREATE -- TESTSUITE"; + $.ajax({ + url: this.global.SERVER_ADDR + "testsuite/new", + method:"POST", + data:{ + suiteName:suiteName + }, + success:function(data){ + if(data['code'] == 200){ + self.testsuites.push({ + id: self.testsuites.length + 1 , + testsuite: suiteName, + }); + showMessage(data['code'], msgTitle, "Create <strong>" + suiteName + "</strong> succesfully!"); + } else { + showMessage(data['code'], msgTitle, "Failed to create <strong>" + suiteName + "</strong>!", data['error']); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, "Failed to create <strong>" + suiteName + "</strong>!", msg); + } + }) + } + var cbox = document.getElementById("create-box"); + cbox.style.display = "none"; + this.newSuite = ''; + }, + deleteSuites:function () { + var self = this; + var msgTitle = "DELETE -- TESTSUITE"; + var deleteArr = self.selected.slice(0); + self.testsuites = self.testsuites.filter(item => { + for(var i in deleteArr) { + if(item.testsuite == deleteArr[i]) { + return false; + } + } + return true; + }); + self.selected = []; + for(var i in deleteArr) + { + $.ajax({ + url: this.global.SERVER_ADDR + "testsuite/delete", + method:"POST", + data:{ + suiteName: deleteArr[i] + }, + success:function (data) { + if(data['code'] == 200){ + showMessage(data['code'], msgTitle, "Delete <strong>" + deleteArr[i] + "</strong> succesfully!"); + } else { + showMessage(data['code'], msgTitle, "Failed to delete <strong>" + deleteArr[i] + "</strong>!", data['error']); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, "Failed to delete <strong>" + deleteArr[i] + "</strong>!", msg); + } + }); + } + }, + runTestsuites: function() { + var self = this; + var msgTitle = "RUN -- TESTSUITE"; + if(self.selected.length == 0) { + showMessage("warning", msgTitle, "please select one!"); + return; + } else if(self.selected.length != 1) { + showMessage("warning", msgTitle, "sorry, one suite at a time!"); + return; + } + self.running.suiteName = self.selected[0]; + $.ajax({ + url: this.global.SERVER_ADDR + "testsuite/content", + method: "GET", + data: { + "suiteName": self.running.suiteName + }, + success: function(data) { + if (data['code'] == 200) { + var caseList = data['result']; + if(caseList.length == 0) { + showMessage("info", msgTitle, "<strong>" + self.running.suiteName + "</strong> is empty!"); + return; + } + for(var i=0; i < caseList.length; i++) { + caseList[i]['status'] = "waiting"; + } + self.casesInSuite = caseList; + showMessage(data['code'], msgTitle, "Start to run <strong>" + self.running.suiteName + "</strong>"); + self.runTestcase(); + } else { + showMessage(data['code'], msgTitle, "Failed to run <strong>" + self.running.suiteName + "</strong>", data['error']); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, "Failed to run <strong>" + self.running.suiteName + "</strong>", msg); + } + }); + }, + runTestcase: function() { + var self = this; + var msgTitle = "RUN -- TESTCASE"; + if (self.curRunningId == self.casesInSuite.length) { + self.curRunningId = 0; + return; + } + self.wfComplete = false; + var i = self.curRunningId; + self.casesInSuite[i]['status'] = "running"; + self.running.caseName = self.casesInSuite[i]['testcase']; + $.ajax({ + url: self.global.SERVER_ADDR + "execute/testcase", + method: "POST", + data: { + "suiteName": self.running.suiteName, + "caseName": self.running.caseName + }, + beforeSend: function(XHR) { + self.wfloading = true; + }, + success: function(data) { + if(data['code'] == 200) { + self.workflowId = data['result']['workflowId']; + $.ajax({ + url: self.global.SERVER_ADDR + "story-content", + method: "GET", + data: { + "service": self.running.suiteName, + "story": self.running.caseName + }, + success: function(data) { + if(data['code'] == 200) { + self.wfJson = data['result']['content']; + } else { + showMessage(data['code'], msgTitle, "workflow.json get failed!"); + } + }, + error: function(obj, status, msg) { + showMessage(status, msgTitle, msg); + } + }); + } else { + var i = self.curRunningId; + self.casesInSuite[i]['status'] = "failed"; + self.wfloading = false; + showMessage(data['code'], msgTitle, "Failed to run <strong>" + self.running.caseName + "</strong>", data['error']); + } + }, + error: function(obj, status, msg) { + var i = self.curRunningId; + self.casesInSuite[i]['status'] = "failed"; + self.wfloading = false; + showMessage(status, msgTitle, "Failed to run <strong>" + self.running.caseName + "</strong>", msg); + } + }); + }, + statusClass: function(status) { + if(status == "waiting") { + return "success"; + } + if(status == "running") { + return "warning"; + } + if(status == "pass") { + return "primary"; + } + if(status == "failed") { + return "danger"; + } + }, + runNextCase: function(obj) { + $(obj).parent().css({"display": "none"}); + var i = this.curRunningId++; + this.runTestcase(); + } + }, + watch: { + wfComplete: function(val) { + if(val == false) return; + this.wfloading = false; + var i = this.curRunningId++; + this.casesInSuite[i]['status'] = "pass"; + // run the next testcase. + this.runTestcase(); + } + }, + components: { + wfresult + } +} +</script> diff --git a/test-scheduler/ui/src/components/workflow_graph/wfresult.vue b/test-scheduler/ui/src/components/workflow_graph/wfresult.vue new file mode 100644 index 00000000..a252870c --- /dev/null +++ b/test-scheduler/ui/src/components/workflow_graph/wfresult.vue @@ -0,0 +1,236 @@ +<template> +<div style="min-height: 600px; overflow-x: auto; overflow-y: auto;"> + <div id="file-section1" class="col-md-4"> + <div id="workflow-content-div"> + <div class="dark-gray-bg" style="font-size: 17px; max-width: 500px; margin: 0 auto 10px;"> WORKFLOW.JSON CONTENT</div> + <pre class="white-pink" id="workflow-content" style="height: 600px; background-color:#f5f5f5;"></pre> + </div> + </div> + <div class="col-md-8" id="graph-show-section" style=""> + <div v-show="!wfloading" style="margin-top: 10px;"> + <div style="min-height: 30px; text-align: right;"> + <button v-show="workflowId != '' && !wfCompletedFlag" class="btn" v-on:click="completeWF(1)" title="mark the status 'complete' by hand.">mark it <strong>complete</strong></button> + </div> + <div id="workflow-graph"> + </div> + </div> + <div v-show="wfloading" class="spiner-example" id="loading"> + <div class="sk-spinner sk-spinner-three-bounce"> + <div class="sk-bounce1"></div> + <div class="sk-bounce2"></div> + <div class="sk-bounce3"></div> + </div> + </div> + <div class="row"> + <div id="iframeContainer"></div> + <div id="workflowId" style="display:none"> + <input name="workflowId" type="hidden" v-bind:value="workflowId" /> + <input name="function" type="hidden" value="graphLoad" /> + <button id="graphloadbtn" type="button" onclick="graphLoad()"></button> + </div> + </div> + </div> +</div> +</template> +<script> +export default { + props: ['workflowId', 'wfloading', 'wfJson'], + name: 'wfresult', + data: function() { + return { + frameLoadedFlag : false, + initalPaintFlag : false, + RESPONSE_TIME_LIMIT : 6000, + responseTimeCounter : 0, + wfCompletedFlag : false, + intervalTime : 1000, + timer: null + } + }, + computed: { + wfget: function(){ + return !this.wfloading; + } + }, + watch: { + workflowId: function(val){ + console.log("############## workflowId changed! " + val); + this.graphLoad(); + }, + wfJson: function(val){ + this.fillWfContent(val); + } + }, + mounted: function(){ + window.clearInterval(this.timer); + }, + destroyed: function(){ + window.clearInterval(this.timer); + }, + methods: { + graphLoad: function(){ + console.log("load function activate"); + this.reset(); + this.initialPaintWFGraph(); + var self = this; + self.timer = window.setInterval(function() { + if(!self.initalPaintFlag) { + if(self.responseTimeCounter > self.RESPONSE_TIME_LIMIT) { + self.initialPaintWFGraph(); + self.responseTimeCounter = 0; + } + } else { + if(self.frameLoadedFlag || self.responseTimeCounter > self.RESPONSE_TIME_LIMIT) { + self.repaintWFGraph(); + self.responseTimeCounter = 0; + } + } + self.responseTimeCounter += self.intervalTime; + }, self.intervalTime); + }, + reset: function(){ + this.frameLoadedFlag = false; + this.initalPaintFlag = false; + this.RESPONSE_TIME_LIMIT = 6000; + this.responseTimeCounter = 0; + this.wfCompletedFlag = false; + var graphDiv = document.getElementById("workflow-graph"); + graphDiv.innerHTML = ''; + }, + initialPaintWFGraph: function() { + console.log("start to initial paint"); + this.setIframeSrc(); + var iframe = document.getElementById("testFrame"); + var self = this; + if (iframe.attachEvent) { + iframe.attachEvent("onload", function(){ + window.setTimeout(function(){ + self.frameLoadedFlag = true; + self.initialPaint(); + console.log("finish to initial paint"); + }, 1000); + }); + } else { + iframe.onload = function(){ + window.setTimeout(function(){ + self.frameLoadedFlag = true; + self.initialPaint(); + console.log("finish to initial paint"); + }, 1000); + } + } + }, + setIframeSrc: function(){ + var iframeContainer = document.getElementById("iframeContainer"); + var iframeDiv = document.getElementById("testFrame"); + if(iframeDiv != undefined ) { + iframeContainer.removeChild(iframeDiv); + } + var apiPrefix = this.global.WF_GRAPH_ADDR + "#/workflow/id/"; + var apiUrl = apiPrefix + this.workflowId; + console.log("workflowId prop:" + this.workflowId); + var iframeDiv = "<iframe id=\"testFrame\" width=\"0\" height=\"0\" style=\"border: none;\" src=\"" + apiUrl + "\"></iframe>"; + iframeContainer.innerHTML = iframeDiv; + this.frameLoadedFlag = false; + }, + initialPaint: function() { + var iframe = document.getElementById("testFrame"); + var frameContent = iframe.contentWindow.document.getElementById("graph-ui-content"); + if(frameContent == null) return; + this.frameLoadedFlag = true; + var content = frameContent.cloneNode(true); + content.id = "graph-ui-content-1"; + var graphDiv = document.getElementById("workflow-graph"); + graphDiv.appendChild(content); + this.initalPaintFlag = true; + this.wfloading = false; + console.log("####################wfloading false######################"); + }, + repaint: function() { + var iframe = document.getElementById("testFrame"); + var frameContent = iframe.contentWindow.document.getElementById("graph-ui-content"); + if(frameContent == null) return; + this.frameLoadedFlag = true; + var newContent = frameContent.cloneNode(true); + var newNodes = newContent.getElementsByClassName("node"); + var oldContent = document.getElementById("graph-ui-content-1"); + var oldNodes = oldContent.getElementsByClassName("node"); + var completeText = iframe.contentWindow.document.getElementsByClassName("ui-content")[0].getElementsByTagName("h4")[0].getElementsByTagName("span")[3].innerHTML; + if(newNodes.length > oldNodes.length) { + console.log("execute in new > old"); + newContent.id = "graph-ui-content-1"; + var graphDiv = document.getElementById("workflow-graph"); + graphDiv.innerHTML = ''; + graphDiv.appendChild(newContent); + } + else if(newNodes.length == oldNodes.length) { + console.log("execute in new = old"); + for(var i = 0; i < newNodes.length; i++) + { + var newGraph = newNodes[i].children[0]; + var newGraphStyle = newGraph.getAttribute("style"); + var oldGraph = oldNodes[i].children[0]; + var oldGraphStyle = oldGraph.getAttribute("style"); + if(newGraphStyle != oldGraphStyle) { + oldGraph.setAttribute("style", newGraphStyle); + } + var newText = newNodes[i].getElementsByTagName("text")[0]; + var newTextStyle = newText.getAttribute("style"); + var oldText = oldNodes[i].getElementsByTagName("text")[0]; + var oldTextStyle = oldText.getAttribute("style"); + if(oldTextStyle != newTextStyle) { + oldText.setAttribute("style", newTextStyle); + } + } + if(completeText == "COMPLETED") { + this.completeWF(5); + } + } + else { + console.log("execute in new < old"); + } + }, + repaintWFGraph: function() { + this.setIframeSrc(); + var iframe = document.getElementById("testFrame"); + var self = this; + if (iframe.attachEvent) { + iframe.attachEvent("onload", function(){ + window.setTimeout(function(){ + self.frameLoadedFlag = true; + self.repaint(); + }, 1000); + }); + } else { + iframe.onload = function(){ + window.setTimeout(function(){ + self.frameLoadedFlag = true; + self.repaint(); + }, 1000); + } + } + }, + completeWF: function(delay) { + this.wfCompletedFlag = true; + console.log("#####################################completed: clean the interval " + this.timer); + window.clearInterval(this.timer); + var self = this; + window.setTimeout(function() { + self.$emit("wfComplete", true); + }, delay*1000); + }, + fillWfContent: function(wfContent) { + var contentDiv = document.getElementById("workflow-content"); + contentDiv.innerHTML = wfContent; + } + } +} +</script> +<style scoped> +#workflow-graph { + text-align: center; +} +#workflow-graph > div{ + display: inline-block; +} +</style> |