feature: cytoscape node positions are remembered
This commit is contained in:
@@ -4,6 +4,7 @@ import tippy from 'tippy.js';
|
|||||||
import 'tippy.js/dist/tippy.css';
|
import 'tippy.js/dist/tippy.css';
|
||||||
import Gradient from 'javascript-color-gradient'; // 多個色階產生器
|
import Gradient from 'javascript-color-gradient'; // 多個色階產生器
|
||||||
import { getTimeLabel } from '@/module/timeLabel.js'; // 時間格式轉換器
|
import { getTimeLabel } from '@/module/timeLabel.js'; // 時間格式轉換器
|
||||||
|
import CytoscapeStore from '@/stores/cytoscapeStore';
|
||||||
|
|
||||||
cytoscape.use( dagre );
|
cytoscape.use( dagre );
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ cytoscape.use( dagre );
|
|||||||
* @param {string} dataLayerOption DataLayer's options
|
* @param {string} dataLayerOption DataLayer's options
|
||||||
* @param {string} curve Curve's type
|
* @param {string} curve Curve's type
|
||||||
* @param {string} graphId cytoscape's container
|
* @param {string} graphId cytoscape's container
|
||||||
|
* @return {cytoscape.Core} cy
|
||||||
*/
|
*/
|
||||||
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
|
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
|
||||||
// create color Gradient
|
// create color Gradient
|
||||||
@@ -217,8 +219,29 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
|
|||||||
content:content
|
content:content
|
||||||
});
|
});
|
||||||
if(node.data("label").length > 10) tip.show();
|
if(node.data("label").length > 10) tip.show();
|
||||||
})
|
});
|
||||||
cy.on('mouseout', 'node', function(event) {
|
cy.on('mouseout', 'node', function(event) {
|
||||||
tip.hide();
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { createRouter, createWebHistory, } from "vue-router";
|
|||||||
import AuthContainer from '@/views/AuthContainer.vue';
|
import AuthContainer from '@/views/AuthContainer.vue';
|
||||||
import MainContainer from '@/views/MainContainer.vue';
|
import MainContainer from '@/views/MainContainer.vue';
|
||||||
import Login from '@/views/Login/Login.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 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 Conformance from '@/views/Discover/Conformance/index.vue';
|
||||||
import Performance from '@/views/Discover/Performance/index.vue';
|
import Performance from '@/views/Discover/Performance/index.vue';
|
||||||
import CompareDashboard from '@/views/Compare/Dashboard/index.vue';
|
import CompareDashboard from '@/views/Compare/Dashboard/index.vue';
|
||||||
|
|||||||
60
src/stores/cytoscapeStore.js
Normal file
60
src/stores/cytoscapeStore.js
Normal 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { defineStore } from "pinia";
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import apiError from '@/module/apiError.js';
|
import apiError from '@/module/apiError.js';
|
||||||
import { deleteCookie, setCookie, getCookie } from "../utils/cookieUtil";
|
import { deleteCookie, setCookie, getCookie } from "../utils/cookieUtil";
|
||||||
|
import LoginStore from "@/stores/login.js";
|
||||||
|
|
||||||
export default defineStore('loginStore', {
|
export default defineStore('loginStore', {
|
||||||
// data, methods, computed
|
// data, methods, computed
|
||||||
@@ -60,7 +61,6 @@ export default defineStore('loginStore', {
|
|||||||
* Refresh Token
|
* Refresh Token
|
||||||
*/
|
*/
|
||||||
async refreshToken() {
|
async refreshToken() {
|
||||||
console.log('TODO:TODO:', this.auth);
|
|
||||||
const api = '/api/oauth/token';
|
const api = '/api/oauth/token';
|
||||||
|
|
||||||
this.auth.grant_type = 'refresh_token';
|
this.auth.grant_type = 'refresh_token';
|
||||||
@@ -123,6 +123,9 @@ export default defineStore('loginStore', {
|
|||||||
},
|
},
|
||||||
setRememberedReturnToUrl(returnToUrl){
|
setRememberedReturnToUrl(returnToUrl){
|
||||||
this.rememberedReturnToUrl = returnToUrl
|
this.rememberedReturnToUrl = returnToUrl
|
||||||
}
|
},
|
||||||
}
|
setIsLoggedIn(boolean) {
|
||||||
|
this.isLoggedIn = boolean;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,12 +53,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { onBeforeMount, } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LoadingStore from '@/stores/loading.js';
|
import LoadingStore from '@/stores/loading.js';
|
||||||
import AllMapDataStore from '@/stores/allMapData.js';
|
import AllMapDataStore from '@/stores/allMapData.js';
|
||||||
import ConformanceStore from '@/stores/conformance.js';
|
import ConformanceStore from '@/stores/conformance.js';
|
||||||
import cytoscapeMap from '@/module/cytoscapeMap.js';
|
import cytoscapeMap from '@/module/cytoscapeMap.js';
|
||||||
|
import CytoscapeStore from '@/stores/cytoscapeStore.js';
|
||||||
import SidebarView from '@/components/Discover/Map/SidebarView.vue';
|
import SidebarView from '@/components/Discover/Map/SidebarView.vue';
|
||||||
import SidebarState from '@/components/Discover/Map/SidebarState.vue';
|
import SidebarState from '@/components/Discover/Map/SidebarState.vue';
|
||||||
import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue';
|
import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue';
|
||||||
@@ -69,9 +71,26 @@ export default {
|
|||||||
const loadingStore = LoadingStore();
|
const loadingStore = LoadingStore();
|
||||||
const allMapDataStore = AllMapDataStore();
|
const allMapDataStore = AllMapDataStore();
|
||||||
const { isLoading } = storeToRefs(loadingStore);
|
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
|
props:['type', 'checkType', 'checkId', 'checkFileId'], // 來自 router 的 props
|
||||||
components: {
|
components: {
|
||||||
@@ -94,6 +113,7 @@ export default {
|
|||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: [],
|
||||||
},
|
},
|
||||||
|
cytoscapeGraph: null,
|
||||||
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
|
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
|
||||||
mapType: 'processMap', // processMap | bpmn
|
mapType: 'processMap', // processMap | bpmn
|
||||||
dataLayerType: 'freq', // freq | duration
|
dataLayerType: 'freq', // freq | duration
|
||||||
@@ -355,7 +375,7 @@ export default {
|
|||||||
if(this[type].vertices.length !== 0){
|
if(this[type].vertices.length !== 0){
|
||||||
this.setNodesData(mapData);
|
this.setNodesData(mapData);
|
||||||
this.setEdgesData(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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
</span>
|
</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 "
|
<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"
|
:class="{'border-danger':isInvalid}" required autofocus v-model.trim="auth.username"
|
||||||
@change="changeHandler($event)"/>
|
@change="changeHandler($event)"
|
||||||
|
@focus="onInputAccountFocus"/>
|
||||||
</label>
|
</label>
|
||||||
<label for="passwordt" class="relative block">
|
<label for="passwordt" class="relative block">
|
||||||
<p class="text-sm font-normal mb-2">Password</p>
|
<p class="text-sm font-normal mb-2">Password</p>
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<input :type="showPassword ? 'text' : 'password'" id="password" aria-describedby="password-addon"
|
<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="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"
|
<span class="absolute bottom-2 right-4 inline-flex items-center cursor-pointer" v-show="auth.password"
|
||||||
@click="showPassword = !showPassword">
|
@click="showPassword = !showPassword">
|
||||||
<IconEyeOpen class="h-5 w-5" v-if="showPassword"/>
|
<IconEyeOpen class="h-5 w-5" v-if="showPassword"/>
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<button id="login_btn_main_btn" type="submit" class="w-full btn btn-lg"
|
<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">
|
:disabled="isDisabledButton">
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</button>
|
||||||
@@ -45,6 +47,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { ref, } from 'vue';
|
||||||
import { storeToRefs, mapActions } from 'pinia';
|
import { storeToRefs, mapActions } from 'pinia';
|
||||||
import loginStore from '@/stores/login.js';
|
import loginStore from '@/stores/login.js';
|
||||||
import IconMember from '@/components/icons/IconMember.vue';
|
import IconMember from '@/components/icons/IconMember.vue';
|
||||||
@@ -67,11 +70,13 @@ export default {
|
|||||||
const { auth, isInvalid } = storeToRefs(store);
|
const { auth, isInvalid } = storeToRefs(store);
|
||||||
// 調用 store 裡的 action
|
// 調用 store 裡的 action
|
||||||
const { signIn } = store;
|
const { signIn } = store;
|
||||||
|
const isJustFocus = ref(true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
auth,
|
auth,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
signIn,
|
signIn,
|
||||||
|
isJustFocus,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@@ -85,9 +90,8 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* if input no value , disabled.
|
* if input no value , disabled.
|
||||||
*/
|
*/
|
||||||
isDisabledButton() {
|
isDisabledButton() {
|
||||||
let inputAreSpaces = this.auth.username === '' || this.auth.password === '' || this.isInvalid === true;
|
return this.auth.username === '' || this.auth.password === '' || this.isInvalid;
|
||||||
return this.isDisabled = inputAreSpaces ? true : false;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -101,6 +105,10 @@ export default {
|
|||||||
this.isInvalid = false;
|
this.isInvalid = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onInputAccountFocus(){
|
||||||
|
},
|
||||||
|
onInputPwdFocus(){
|
||||||
|
},
|
||||||
...mapActions(loginStore, ['setRememberedReturnToUrl']),
|
...mapActions(loginStore, ['setRememberedReturnToUrl']),
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
@@ -107,12 +107,13 @@ export default {
|
|||||||
// go to log in();
|
// go to log in();
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
beforeRouteEnter(to, from, next) {
|
async beforeRouteEnter(to, from, next) {
|
||||||
const loginStore = LoginStore();
|
const loginStore = LoginStore();
|
||||||
|
|
||||||
if (!loginStore.isLoggedIn) {
|
if (!loginStore.isLoggedIn) {
|
||||||
if (getCookie('luciaRefreshToken')) {
|
if (getCookie('luciaRefreshToken')) {
|
||||||
loginStore.refreshToken();
|
await loginStore.refreshToken();
|
||||||
|
loginStore.setIsLoggedIn(true);
|
||||||
} else {
|
} else {
|
||||||
next({
|
next({
|
||||||
path: '/login',
|
path: '/login',
|
||||||
|
|||||||
Reference in New Issue
Block a user