diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..a2bcca6ca08cfaeeaebab395d78c82f8316539a6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,46 @@ +image: nexus-registry.xlab.si:5001/docker:stable + +variables: + REGISTRY: registry-gitlab.xlab.si + MEDINA_REGISTRY: optima-medina-docker-dev.artifact.tecnalia.com + MEDINA_REG_PATH: wp4/t41 + +stages: +- build +- push +- deploy + +before_script: +- mkdir -p $HOME/.docker +- echo "$DOCKER_AUTH_CONFIG" > $HOME/.docker/config.json +- export SERVICE=cce-frontend +- export VERSION=$(grep -w "version" package.json | cut -d ':' -f 2 | cut -d ',' -f 1 | xargs echo) + +build: + stage: build + script: docker build --no-cache -t $REGISTRY/medina/$SERVICE:$VERSION . + +push: + stage: push + script: + - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY + - docker tag $REGISTRY/medina/$SERVICE:$VERSION $REGISTRY/medina/$SERVICE:latest + - docker push $REGISTRY/medina/$SERVICE:$VERSION + - docker push $REGISTRY/medina/$SERVICE:latest + - docker logout $REGISTRY + - docker login $MEDINA_REGISTRY -u medina.fordevelopers@gmail.com -p AKCp8kqMZkcPRPGZhHBw7uKFsyifF1iHb2ZvbBy5PK88wD8EdeSHZqFsc4h1wp3M2oVYGazhv + - docker tag $REGISTRY/medina/$SERVICE:$VERSION $MEDINA_REGISTRY/$MEDINA_REG_PATH/$SERVICE:$VERSION + - docker tag $REGISTRY/medina/$SERVICE:$VERSION $MEDINA_REGISTRY/$MEDINA_REG_PATH/$SERVICE:latest + - docker push $MEDINA_REGISTRY/$MEDINA_REG_PATH/$SERVICE:$VERSION + - docker push $MEDINA_REGISTRY/$MEDINA_REG_PATH/$SERVICE:latest + - docker logout $MEDINA_REGISTRY + only: + - master + +deploy: + stage: deploy + script: + - docker run --rm curlimages/curl -I -X POST "https://xlab:110bb809200c797e6031787b51a049b819@cicd.medina.esilab.org/jenkins/job/medina/job/wp4/job/task_4.1/job/cce-deploy/buildWithParameters?PRJ_ENV=dev&PRJ_IMAGE_TAG=latest&YAMLS_OVERRIDE=" + only: + - master + diff --git a/package-lock.json b/package-lock.json index dde8bb067f624424b40d769d3c20c0d6bb317a97..6f6abfcb8a47991a3377b04dbd451803cae700ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cce-frontend", - "version": "0.1.5", + "version": "0.1.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cce-frontend", - "version": "0.1.5", + "version": "0.1.6", "dependencies": { "@fortawesome/fontawesome-free": "^6.1.1", "@fortawesome/fontawesome-svg-core": "^6.1.1", @@ -20,7 +20,7 @@ "core-js": "^3.23.1", "eslint-plugin-vue": "^9.1.1", "jquery": "^3.6.0", - "moment": "^2.29.3", + "luxon": "^3.0.3", "vue": "^3.2.37", "vue-axios": "^3.4.1", "vue-router": "^4.0.16", @@ -2945,9 +2945,9 @@ "dev": true }, "node_modules/@vue/devtools-api": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.1.4.tgz", - "integrity": "sha512-IiA0SvDrJEgXvVxjNkHPFfDx6SXw0b/TUkqMcDZWNg9fnCAHbTpoo59YfJ9QLFkwa3raau5vSlRVzMSLDnfdtQ==" + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.2.1.tgz", + "integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==" }, "node_modules/@vue/eslint-config-prettier": { "version": "7.0.0", @@ -7707,6 +7707,14 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz", + "integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -7974,14 +7982,6 @@ "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==", "dev": true }, - "node_modules/moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", - "engines": { - "node": "*" - } - }, "node_modules/mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -10350,9 +10350,9 @@ } }, "node_modules/terser": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz", - "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.2", @@ -13871,9 +13871,9 @@ } }, "@vue/devtools-api": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.1.4.tgz", - "integrity": "sha512-IiA0SvDrJEgXvVxjNkHPFfDx6SXw0b/TUkqMcDZWNg9fnCAHbTpoo59YfJ9QLFkwa3raau5vSlRVzMSLDnfdtQ==" + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.2.1.tgz", + "integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==" }, "@vue/eslint-config-prettier": { "version": "7.0.0", @@ -17412,6 +17412,11 @@ "yallist": "^4.0.0" } }, + "luxon": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz", + "integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w==" + }, "magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -17612,11 +17617,6 @@ "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==", "dev": true }, - "moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" - }, "mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -19327,9 +19327,9 @@ "dev": true }, "terser": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz", - "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.2", diff --git a/package.json b/package.json index 6723cc059a6f12abef72416d4c3f527336aec9ec..369132ed10806c4f58b70397ee804743c0d2bc7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cce-frontend", - "version": "0.1.6", + "version": "0.2.0", "private": true, "scripts": { "serve": "vue-cli-service serve", @@ -27,7 +27,7 @@ "core-js": "^3.23.1", "eslint-plugin-vue": "^9.1.1", "jquery": "^3.6.0", - "moment": "^2.29.3", + "luxon": "^3.0.3", "vue": "^3.2.37", "vue-axios": "^3.4.1", "vue-router": "^4.0.16", @@ -39,8 +39,8 @@ "@vue/cli-plugin-eslint": "~5.0.6", "@vue/cli-service": "~5.0.6", "@vue/compiler-sfc": "^3.2.37", - "eslint": "^8.17.0", "@vue/eslint-config-prettier": "^7.0.0", + "eslint": "^8.17.0", "eslint-plugin-prettier": "^4.0.0", "prettier": "^2.7.1", "sass": "^1.52.3", diff --git a/src/App.vue b/src/App.vue index a8faeb78c1d127792a43a69f745515b12363c335..253dc220835e773330cf760f8f2714b03c55904c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,10 +14,8 @@ export default { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - text-align: center; color: #2c3e50; background: #f3f8fb; - padding: 60px; height: 100vh; } </style> diff --git a/src/components/ToeHistory.vue b/src/components/ToeHistory.vue new file mode 100644 index 0000000000000000000000000000000000000000..2abcfd0857847a40e9d494dd8b910c1b17737d7c --- /dev/null +++ b/src/components/ToeHistory.vue @@ -0,0 +1,78 @@ +<template> + <div class="dropdown"> + <button + class="btn btn-primary dropdown-toggle" + type="button" + data-bs-toggle="dropdown" + aria-expanded="false" + > + {{ currentTreeShown }} + </button> + <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1"> + <li + v-for="tree in toeHistory" + :key="tree.treeStateId" + :class="disabled(tree)" + @click="getHistoryTree(tree)" + class="dropdown-item" + > + <a> + {{ formatDatetime(tree.timeUpdated) }} + </a> + </li> + </ul> + </div> +</template> + +<script> +import { DateTime } from "luxon"; +import { getLastTree } from "@/helpers/helpers"; + +export default { + name: "ToeHistory", + props: ["toeHistory", "currentTreeState"], + data: function () { + return { + formatDatetime(datetime) { + return DateTime.fromISO(datetime).toLocaleString( + DateTime.DATETIME_FULL + ); + }, + disabled(tree) { + if (tree.treeStateId === this.currentTreeState.treeStateId) { + return "disabled"; + } + return ""; + }, + }; + }, + computed: { + currentTreeShown() { + if ( + this.currentTreeState.treeStateId === + getLastTree(this.toeHistory).treeStateId + ) { + return "Current tree state"; + } else { + return this.formatDatetime(this.currentTreeState.timeUpdated); + } + }, + }, + methods: { + getHistoryTree(tree) { + this.$store.dispatch("getTreeDataByStateId", tree.treeStateId); + }, + }, +}; +</script> + +<style scoped lang="scss"> +@import "@/styles/_variables.scss"; +.disabled { + pointer-events: none; + opacity: 0.6; +} +.dropdown-item { + cursor: pointer; +} +</style> diff --git a/src/components/ToeList.vue b/src/components/ToeList.vue new file mode 100644 index 0000000000000000000000000000000000000000..b8797bf979d8cca852135320ddf53012e4fd369b --- /dev/null +++ b/src/components/ToeList.vue @@ -0,0 +1,67 @@ +<template> + <div class="dropdown"> + <button + class="btn btn-primary dropdown-toggle" + type="button" + data-bs-toggle="dropdown" + aria-expanded="false" + > + {{ initSelectedToe.name }} + </button> + <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1"> + <li + v-for="toe in toeList" + :key="toe.toeId" + :class="disabled(toe)" + @click="setToeTree(toe)" + class="dropdown-item" + > + <a>{{ toe.name }}</a> + </li> + </ul> + </div> +</template> + +<script> +export default { + name: "ToeList", + props: ["toeList", "initSelectedToe"], + data: function () { + return { + disabled(toe) { + if (toe.toeId === this.initSelectedToe.targetOfEvaluationId) { + return "disabled"; + } + return ""; + }, + }; + }, + methods: { + setToeTree(toe) { + this.$store.dispatch("selectToe", toe.toeId); + }, + }, +}; +</script> + +<style scoped lang="scss"> +@import "@/styles/_variables.scss"; + +.toe-list { + .dropdown-toggle { + max-width: 100%; + padding: 6px 12px; + overflow: hidden; + text-overflow: ellipsis; + } + + .dropdown-item { + cursor: pointer; + } + + .disabled { + pointer-events: none; + opacity: 0.6; + } +} +</style> diff --git a/src/components/Tree.vue b/src/components/Tree.vue index dba1e54e67ccfc0a052fdeae8df2c63ccdc6e153..99043a83ed876f41031b3597bff09ce7f0bcbe68 100644 --- a/src/components/Tree.vue +++ b/src/components/Tree.vue @@ -1,5 +1,5 @@ <template> - <div class="container"> + <div id="toe-container" class="container"> <div class="box info-box" v-show="hoveredNode"> <div class="close-button"> <i @click="infoBoxClose" title="Close" class="fa fa-solid fa-xmark"></i> @@ -151,7 +151,7 @@ <script> import VueTree from "@ssthouse/vue3-tree-chart"; import "@ssthouse/vue3-tree-chart/dist/vue3-tree-chart.css"; -import { formatDateHelper } from "@/helpers/dataFormat"; +import { formatDateHelper } from "@/helpers/helpers"; import $ from "jquery"; import { collapseExpandTree } from "@/mixins/collapseExpandTree"; @@ -233,6 +233,11 @@ export default { return formatDateHelper(date); }, }, + watch: { + treeData() { + this.infoBoxClose(); + }, + }, }; </script> @@ -246,6 +251,11 @@ export default { max-width: 5000px; } +#toe-container { + position: relative; + padding-bottom: 20px; +} + .tree-container { background: white; margin-top: 20px; @@ -289,9 +299,11 @@ export default { } &.info-box { + position: absolute; border-color: $color-blue; z-index: 10; - left: 72px; + left: 12px; + top: 74px; } &#instructions { diff --git a/src/helpers/helpers.js b/src/helpers/helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..56a6e908581f6c39a125d6b909bc455107738179 --- /dev/null +++ b/src/helpers/helpers.js @@ -0,0 +1,23 @@ +import { DateTime } from "luxon"; + +export function formatDateHelper(value) { + if (value) { + return DateTime.fromISO(value).toFormat("DD MMM YYYY HH:mm:ss"); + } +} + +export function getLastTree(historyData) { + if (!historyData.length) return; + let lastTreeIdx = 0; + let closestTime = DateTime.fromISO(historyData[0].timeUpdated); + for (let i = 0; i < historyData.length; i++) { + let diff = DateTime.fromISO(historyData[i].timeUpdated).diff( + DateTime.fromISO(closestTime) + ); + if (diff.milliseconds > 0) { + closestTime = historyData[i].timeUpdated; + lastTreeIdx = i; + } + } + return historyData[lastTreeIdx]; +} diff --git a/src/services/ApiService.js b/src/services/ApiService.js index eed452ce8eaf366f147dc02df80d09dcf135fe42..bfad5be9d6fe12bde8c88111bd02defb7aa7d8ac 100644 --- a/src/services/ApiService.js +++ b/src/services/ApiService.js @@ -1,7 +1,13 @@ import api from "@/client/client"; export const ApiService = { - getEvaluationTreeData() { - return api.get("/tree"); + getToeList() { + return api.get("/toeList"); + }, + getToeHistory(toeId) { + return api.get("/toes/" + toeId + "/listHistory"); + }, + getTreeByStateId(stateId) { + return api.get("/history/" + stateId); }, }; diff --git a/src/store/index.js b/src/store/index.js index f9414c2310c099b43ec5309703610b9b1889aedd..16b1dd45677fbcb6942fb4f809b32b824aac2a5e 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,33 +5,88 @@ import { collapseExpandTree } from "@/mixins/collapseExpandTree"; const store = createStore({ state: { treeData: {}, + toeList: {}, + toeHistory: {}, + treeDataListByStateId: [], }, mutations: { SAVE_TREE_DATA(state, treeData) { state.treeData = treeData; }, + SAVE_TOE_LIST(state, toeList) { + state.toeList = toeList; + }, + SAVE_TOE_HISTORY(state, toeHistory) { + state.toeHistory = toeHistory; + }, + SAVE_TREE_DATA_LIST(state, treeData) { + state.treeDataListByStateId.push(treeData); + }, }, getters: { treeData: (state) => state.treeData, + toeList: (state) => state.toeList, + toeHistory: (state) => state.toeHistory, + treeByStateId: (state) => (stateId) => { + return state.treeDataListByStateId.find( + (tree) => tree.treeStateId === stateId + ); + }, }, actions: { - async getTreeData({ commit }) { - await ApiService.getEvaluationTreeData() + async getTreeDataByStateId({ commit }, stateId) { + if (!stateId) return; + if (stateId && this.getters.treeByStateId(stateId)) { + return commit("SAVE_TREE_DATA", this.getters.treeByStateId(stateId)); + } + + ApiService.getTreeByStateId(stateId) .then((response) => { - // collapse all except root collapseExpandTree.methods.collapseAllFromLevel( response.data.root, 1 ); commit("SAVE_TREE_DATA", response.data); + commit("SAVE_TREE_DATA_LIST", response.data); + }) + .catch((err) => { + console.error("Error getting tree by tree state id: " + err); + }); + }, + async getToeList({ commit }) { + await ApiService.getToeList() + .then((response) => { + commit("SAVE_TOE_LIST", response.data); }) .catch((err) => { - console.log("ERROR at action getTreeData: " + err); + console.error("Error getting ToE list: " + err); + }); + }, + async getToeHistory({ commit }, id) { + await ApiService.getToeHistory(id) + .then((response) => { + commit("SAVE_TOE_HISTORY", response.data); + }) + .catch((err) => { + console.error("Error getting ToE history: " + err); + }); + }, + async initToeListAndTree({ dispatch }) { + // TODO: add logic for when there are no trees + await dispatch("getToeList").then(() => { + dispatch("getToeHistory", this.state.toeList[0].toeId).then(() => { + dispatch( + "getTreeDataByStateId", + this.state.toeHistory[0].treeStateId + ); }); + }); }, - async updateTreeData({ commit }, data) { - commit("SAVE_TREE_DATA", data); + async selectToe({ dispatch }, toeId) { + await dispatch("getToeHistory", toeId).then(() => { + dispatch("getTreeDataByStateId", this.state.toeHistory[0].treeStateId); + }); }, }, }); diff --git a/src/views/Home.vue b/src/views/Home.vue index f30ede8577870e65ec5468e67b15ddab190b2e05..23ba8e13aa4ffbfa6b7f3f712863eab152cf480b 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,5 +1,16 @@ <template> - <div id="home"> + <div id="home" class="container" v-if="treeData.root"> + <div class="row toe-dropdowns"> + <div class="col-sm-6 toe-list"> + <ToeList :toeList="toeList" :initSelectedToe="treeData"></ToeList> + </div> + <div class="col-sm-6 toe-history"> + <ToeHistory + :toeHistory="toeHistory" + :currentTreeState="treeData" + ></ToeHistory> + </div> + </div> <Tree :treeData="treeData.root"></Tree> </div> </template> @@ -7,6 +18,8 @@ <script> import { mapGetters } from "vuex"; import Tree from "@/components/Tree"; +import ToeList from "@/components/ToeList"; +import ToeHistory from "@/components/ToeHistory"; export default { name: "Home", @@ -15,14 +28,27 @@ export default { }, computed: { ...mapGetters(["treeData"]), + ...mapGetters(["toeList"]), + ...mapGetters(["toeHistory"]), }, components: { Tree, + ToeList, + ToeHistory, }, created() { - this.$store.dispatch("getTreeData"); + this.$store.dispatch("initToeListAndTree"); }, }; </script> -<style scoped lang="scss"></style> +<style scoped lang="scss"> +.toe-dropdowns { + padding: 40px 12px 10px; +} +.toe-history { + .dropdown { + float: right; + } +} +</style>