feature: cytoscape node positions are remembered

This commit is contained in:
Cindy Chang
2024-06-28 16:20:41 +08:00
parent b890df9de6
commit 2110388a2d
8 changed files with 134 additions and 19 deletions

View File

@@ -4,6 +4,7 @@ import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import Gradient from 'javascript-color-gradient'; // 多個色階產生器
import { getTimeLabel } from '@/module/timeLabel.js'; // 時間格式轉換器
import CytoscapeStore from '@/stores/cytoscapeStore';
cytoscape.use( dagre );
@@ -19,6 +20,7 @@ cytoscape.use( dagre );
* @param {string} dataLayerOption DataLayer's options
* @param {string} curve Curve's type
* @param {string} graphId cytoscape's container
* @return {cytoscape.Core} cy
*/
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
// create color Gradient
@@ -217,8 +219,29 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
content:content
});
if(node.data("label").length > 10) tip.show();
})
});
cy.on('mouseout', 'node', function(event) {
tip.hide();
})
});
const cytoscapeStore = CytoscapeStore();
cy.ready(() => {
cytoscapeStore.nodePositions.forEach(pos => {
const node = cy.getElementById(pos.id);
if (node) {
node.position(pos.position);
}
});
// 在改變節點位置後,盡可能地記錄節點線條的位置情報
cy.on('dragfree', 'node', (event) => {
const node = event.target;
const position = node.position();
cytoscapeStore.saveNodePosition(node.id(), position);
cytoscapeStore.savePositionsToStorage();
});
});
return cy;
}

View File

@@ -2,9 +2,9 @@ import { createRouter, createWebHistory, } from "vue-router";
import AuthContainer from '@/views/AuthContainer.vue';
import MainContainer from '@/views/MainContainer.vue';
import Login from '@/views/Login/Login.vue';
import Files from '@/views/Files/index.vue';
import Files from '@/views/Files/Files.vue';
import Upload from '@/views/Upload/index.vue';
import Map from '@/views/Discover/Map/index.vue';
import Map from '@/views/Discover/Map/Map.vue';
import Conformance from '@/views/Discover/Conformance/index.vue';
import Performance from '@/views/Discover/Performance/index.vue';
import CompareDashboard from '@/views/Compare/Dashboard/index.vue';

View File

@@ -0,0 +1,60 @@
import { defineStore } from 'pinia';
// interface NodePosition {
// id: string;
// position: { x: number; y: number };
// graphId: string;
// }
export default defineStore('useCytoscapeStore', {
state: () => ({
nodePositions: [],
currentGraphId: "",
}),
actions: {
/**
* 儲存或更新單個節點的位置資訊。
* @param {string} id
* @param {object} position {x, y}
* @param {string} graphId
*/
saveNodePosition(id, position) {
const existingNode = this.nodePositions.find(node => node.id === id && node.graphId === this.currentGraphId);
if (existingNode) { // 如果存在,更新位置資訊
existingNode.position = position;
} else { // 如果不存在,新增一個新的位置資訊
this.nodePositions.push({
id,
position,
graphId: this.currentGraphId,
});
}
},
/**
* 根據節點 ID 獲取該節點的位置資訊
* @param {string} id
* @returns
*/
getNodePosition(id) { // 返回該節點的位置資訊,如果不存在則返回 undefined
return this.nodePositions.find(node => node.id === id)?.position;
},
/**
* 從本地存儲中加載節點位置資訊。
*/
loadPositionsFromStorage() {
const storedPositions = localStorage.getItem('cy-node-positions');
if (storedPositions) {
this.nodePositions = JSON.parse(storedPositions);
}
},
/**
* 將節點位置資訊儲存到本地存儲中。
*/
savePositionsToStorage() {
localStorage.setItem('cy-node-positions', JSON.stringify(this.nodePositions));
},
setCurrentGraphId(currentGraphId) {
this.currentGraphId = currentGraphId;
},
},
});

View File

@@ -2,6 +2,7 @@ import { defineStore } from "pinia";
import axios from 'axios';
import apiError from '@/module/apiError.js';
import { deleteCookie, setCookie, getCookie } from "../utils/cookieUtil";
import LoginStore from "@/stores/login.js";
export default defineStore('loginStore', {
// data, methods, computed
@@ -60,7 +61,6 @@ export default defineStore('loginStore', {
* Refresh Token
*/
async refreshToken() {
console.log('TODO:TODO:', this.auth);
const api = '/api/oauth/token';
this.auth.grant_type = 'refresh_token';
@@ -123,6 +123,9 @@ export default defineStore('loginStore', {
},
setRememberedReturnToUrl(returnToUrl){
this.rememberedReturnToUrl = returnToUrl
}
}
},
setIsLoggedIn(boolean) {
this.isLoggedIn = boolean;
},
},
});

View File

@@ -53,12 +53,14 @@
</template>
<script>
import { onBeforeMount, } from 'vue';
import { storeToRefs } from 'pinia';
import axios from 'axios';
import LoadingStore from '@/stores/loading.js';
import AllMapDataStore from '@/stores/allMapData.js';
import ConformanceStore from '@/stores/conformance.js';
import cytoscapeMap from '@/module/cytoscapeMap.js';
import CytoscapeStore from '@/stores/cytoscapeStore.js';
import SidebarView from '@/components/Discover/Map/SidebarView.vue';
import SidebarState from '@/components/Discover/Map/SidebarState.vue';
import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue';
@@ -69,9 +71,26 @@ export default {
const loadingStore = LoadingStore();
const allMapDataStore = AllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace, temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases, postRuleData } = storeToRefs(allMapDataStore);
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId,
filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace,
temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases,
postRuleData
} = storeToRefs(allMapDataStore);
return { isLoading, processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData, ruleData, allMapDataStore, cases, postRuleData }
const cytoscapeStore = CytoscapeStore();
const { setCurrentGraphId } = cytoscapeStore;
onBeforeMount(() => {
setCurrentGraphId(logId);
});
return { isLoading, processMap, bpmn, stats, insights, traceId, traces, baseTraces,
baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe,
filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData,
ruleData, allMapDataStore, cases, postRuleData,
setCurrentGraphId,
};
},
props:['type', 'checkType', 'checkId', 'checkFileId'], // router props
components: {
@@ -94,6 +113,7 @@ export default {
nodes: [],
edges: [],
},
cytoscapeGraph: null,
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
mapType: 'processMap', // processMap | bpmn
dataLayerType: 'freq', // freq | duration
@@ -355,7 +375,7 @@ export default {
if(this[type].vertices.length !== 0){
this.setNodesData(mapData);
this.setEdgesData(mapData);
cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId);
this.cytoscapeGraph = cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId);
};
},
},

View File

@@ -11,7 +11,8 @@
</span>
<input type="text" id="account" class="w-full border border-neutral-300 rounded py-1 pl-10 pr-2 focus:outline-none focus:border-primary focus:ring-1 "
:class="{'border-danger':isInvalid}" required autofocus v-model.trim="auth.username"
@change="changeHandler($event)"/>
@change="changeHandler($event)"
@focus="onInputAccountFocus"/>
</label>
<label for="passwordt" class="relative block">
<p class="text-sm font-normal mb-2">Password</p>
@@ -20,7 +21,8 @@
</span>
<input :type="showPassword ? 'text' : 'password'" id="password" aria-describedby="password-addon"
class="w-full border border-neutral-300 rounded py-1 pl-10 pr-2 focus:outline-none focus:border-primary focus:ring-1 "
:class="{'border-danger':isInvalid}" required v-model.trim="auth.password" @change="changeHandler($event)"/>
:class="{'border-danger':isInvalid}" required v-model.trim="auth.password" @change="changeHandler($event)"
@focus="onInputPwdFocus"/>
<span class="absolute bottom-2 right-4 inline-flex items-center cursor-pointer" v-show="auth.password"
@click="showPassword = !showPassword">
<IconEyeOpen class="h-5 w-5" v-if="showPassword"/>
@@ -34,7 +36,7 @@
</span>
</p>
<button id="login_btn_main_btn" type="submit" class="w-full btn btn-lg"
:class="this.isDisabled ? 'btn-disable' : 'btn-c-primary'"
:class="isDisabledButton ? 'btn-disable' : 'btn-c-primary'"
:disabled="isDisabledButton">
Log in
</button>
@@ -45,6 +47,7 @@
</template>
<script>
import { ref, } from 'vue';
import { storeToRefs, mapActions } from 'pinia';
import loginStore from '@/stores/login.js';
import IconMember from '@/components/icons/IconMember.vue';
@@ -67,11 +70,13 @@ export default {
const { auth, isInvalid } = storeToRefs(store);
// 調用 store 裡的 action
const { signIn } = store;
const isJustFocus = ref(true);
return {
auth,
isInvalid,
signIn,
isJustFocus,
}
},
components: {
@@ -85,9 +90,8 @@ export default {
/**
* if input no value , disabled.
*/
isDisabledButton() {
let inputAreSpaces = this.auth.username === '' || this.auth.password === '' || this.isInvalid === true;
return this.isDisabled = inputAreSpaces ? true : false;
isDisabledButton() {
return this.auth.username === '' || this.auth.password === '' || this.isInvalid;
},
},
methods: {
@@ -101,6 +105,10 @@ export default {
this.isInvalid = false;
}
},
onInputAccountFocus(){
},
onInputPwdFocus(){
},
...mapActions(loginStore, ['setRememberedReturnToUrl']),
},
created() {

View File

@@ -107,12 +107,13 @@ export default {
// go to log in();
// }
// }
beforeRouteEnter(to, from, next) {
async beforeRouteEnter(to, from, next) {
const loginStore = LoginStore();
if (!loginStore.isLoggedIn) {
if (getCookie('luciaRefreshToken')) {
loginStore.refreshToken();
await loginStore.refreshToken();
loginStore.setIsLoggedIn(true);
} else {
next({
path: '/login',