Files
lucia-frontend/src/stores/allMapData.ts
2026-03-10 00:59:37 +08:00

400 lines
12 KiB
TypeScript

// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/**
* @module stores/allMapData Process map data store for fetching,
* filtering, and managing Discover Map page state including
* filter rules, data layers, and process map data.
*/
import { defineStore } from "pinia";
import moment from "moment";
import apiClient from "@/api/client.js";
import apiError from "@/module/apiError.js";
import { Decimal } from "decimal.js";
/**
* Returns the API base path for the current map data source,
* prioritizing temp filter over created filter over log.
* @param {object} state - The store state.
* @returns {string} The API base path.
*/
function getMapApiBase(state) {
const { tempFilterId, createFilterId, logId } = state;
if (tempFilterId !== null) return `/api/temp-filters/${tempFilterId}`;
if (createFilterId !== null) return `/api/filters/${createFilterId}`;
return `/api/logs/${logId}`;
}
/** Pinia store for Discover Map page data and filter management. */
export const useAllMapDataStore = defineStore("allMapDataStore", {
state: () => ({
baseLogId: null,
logId: null,
traceId: null,
baseTraceId: 1,
tempFilterId: null,
createFilterId: null,
filterName: null,
allProcessMap: {},
allBpmn: {},
allStats: {},
allInsights: {},
allTrace: [],
allBaseTrace: [],
allCase: [],
allBaseCase: [],
allTraceTaskSeq: [],
allBaseTraceTaskSeq: [],
allFilterTask: [],
allFilterStartToEnd: [],
allFilterEndToStart: [],
allFilterTimeframe: {},
allFilterTrace: [],
allFilterAttrs: [],
hasResultRule: null, // Whether any data remains after clicking Apply
temporaryData: [], // Data not yet applied via Apply All
postRuleData: [], // Data for the has-result API and temp-filters API
ruleData: [], // Funnle view's data
isRuleData: [], // toggle button data
allFunnelData: [],
isUpdateFilter: false, // Whether the filter file was saved successfully
selectTimeFrame: [], // user select time start and end
infinite404: null, // Whether infinite scroll has reached the last page
infiniteStart: 0, // Starting index for infinite scroll cases
baseInfiniteStart: 0, // Starting index for base infinite scroll cases
}),
getters: {
processMap: (state) => {
return state.allProcessMap;
},
bpmn: (state) => {
return state.allBpmn;
},
stats: (state) => {
return state.allStats;
},
insights: (state) => {
return state.allInsights;
},
traces: (state) => {
return [...state.allTrace].sort((x, y) => x.id - y.id);
},
baseTraces: (state) => {
return [...state.allBaseTrace].sort((x, y) => x.id - y.id);
},
cases: (state) => {
return state.allCase;
},
baseCases: (state) => {
return state.allBaseCase;
},
infiniteFirstCases: (state) => {
if (state.infiniteStart === 0) return state.allCase;
},
BaseInfiniteFirstCases: (state) => {
if (state.baseInfiniteStart === 0) return state.allBaseCase;
},
traceTaskSeq: (state) => {
return state.allTraceTaskSeq;
},
baseTraceTaskSeq: (state) => {
return state.allBaseTraceTaskSeq;
},
// All tasks
filterTasks: (state) => {
return state.allFilterTask;
},
// form start to end tasks
filterStartToEnd: (state) => {
return state.allFilterStartToEnd;
},
// form end to start tasks
filterEndToStart: (state) => {
return state.allFilterEndToStart;
},
filterTimeframe: (state) => {
return state.allFilterTimeframe;
},
filterTrace: (state) => {
return state.allFilterTrace;
},
filterAttrs: (state) => {
if (state.allFilterAttrs !== null) {
return state.allFilterAttrs.map((att) => {
const copy = { ...att };
switch (copy.type) {
case "date":
copy.min =
copy.min === null
? null
: moment(copy.min).format("YYYY/MM/DD HH:mm");
copy.max =
copy.max === null
? null
: moment(copy.max).format("YYYY/MM/DD HH:mm");
break;
case "float":
copy.min =
copy.min === null
? null
: Number(new Decimal(copy.min).toFixed(2, 3));
copy.max =
copy.max === null
? null
: Number(new Decimal(copy.max).toFixed(2, 2));
break;
default:
break;
}
return copy;
});
}
return [];
},
allFunnels: (state) => {
return state.allFunnelData;
},
},
actions: {
/**
* fetch discover api, include '/process-map, /bpmn, /stats, /insights'.
*/
async getAllMapData() {
const api = `${getMapApiBase(this)}/discover`;
try {
const response = await apiClient.get(api);
this.allProcessMap = response.data.process_map;
this.allBpmn = response.data.bpmn;
this.allStats = response.data.stats;
this.allInsights = response.data.insights;
} catch (error) {
apiError(error, "Failed to load the Map.");
}
},
/**
* fetch trace api.
*/
async getAllTrace() {
const baseLogId = this.baseLogId;
const baseApi = `/api/logs/${baseLogId}/traces`;
const api = `${getMapApiBase(this)}/traces`;
try {
let baseResponse;
const response = await apiClient.get(api);
this.allTrace = response.data;
if (baseLogId) {
baseResponse = await apiClient.get(baseApi);
this.allBaseTrace = baseResponse.data;
}
} catch (error) {
apiError(error, "Failed to load the Trace.");
}
},
/**
* fetch trace detail api.
*/
async getTraceDetail() {
const traceId = this.traceId;
const start = this.infiniteStart;
const api = `${getMapApiBase(this)}/traces/${traceId}?start=${start}&page_size=20`;
try {
const response = await apiClient.get(api);
this.allTraceTaskSeq = response.data.task_seq;
this.allCase = response.data.cases;
this.allCase.forEach((c) => {
c.started_at = moment(c.started_at).format("YYYY/MM/DD HH:mm");
c.completed_at = moment(c.completed_at).format("YYYY/MM/DD HH:mm");
c.attributes.forEach((att) => {
switch (att.type) {
case "date":
att.value =
att.value === null
? null
: moment(att.value).format("YYYY/MM/DD HH:mm");
break;
case "float":
att.value =
att.value === null
? null
: Number(new Decimal(att.value).toFixed(2));
break;
default:
break;
}
});
});
return this.allCase;
} catch (error) {
if (error.response?.status === 404) {
this.infinite404 = 404;
return;
}
apiError(error, "Failed to load the Trace Detail.");
return [];
}
},
/**
* fetch base log trace detail api.
*/
async getBaseTraceDetail() {
const logId = this.baseLogId;
const traceId = this.baseTraceId;
const start = this.baseInfiniteStart;
const api = `/api/logs/${logId}/traces/${traceId}?start=${start}&page_size=20`;
try {
const response = await apiClient.get(api);
this.allBaseTraceTaskSeq = response.data.task_seq;
this.allBaseCase = response.data.cases;
this.allBaseCase.forEach((c) => {
c.started_at = moment(c.started_at).format("YYYY/MM/DD HH:mm");
c.completed_at = moment(c.completed_at).format("YYYY/MM/DD HH:mm");
c.attributes.forEach((att) => {
switch (att.type) {
case "date":
att.value =
att.value === null
? null
: moment(att.value).format("YYYY/MM/DD HH:mm");
break;
case "float":
att.value =
att.value === null
? null
: Number(new Decimal(att.value).toFixed(2));
break;
default:
break;
}
});
});
return this.allBaseCase;
} catch (error) {
if (error.response?.status === 404) {
this.infinite404 = 404;
return;
}
apiError(error, "Failed to load the Base Trace Detail.");
return [];
}
},
/**
* fetch Filter Parameters api.
*/
async getFilterParams() {
const logId = this.logId;
const api = `/api/filters/params?log_id=${logId}`;
try {
const response = await apiClient.get(api);
this.allFilterTask = response.data.tasks;
this.allFilterStartToEnd = response.data.sources;
this.allFilterEndToStart = response.data.sinks;
this.allFilterTimeframe = response.data.timeframe;
this.allFilterTrace = response.data.trace;
this.allFilterAttrs = response.data.attrs;
const min = this.allFilterTimeframe.x_axis.min;
const max = this.allFilterTimeframe.x_axis.max;
// Preserve raw data for Chart.js; incorrect format causes wrong output
this.allFilterTimeframe.x_axis.min_base = min;
this.allFilterTimeframe.x_axis.max_base = max;
// Convert to a time format without seconds
this.allFilterTimeframe.x_axis.min =
min === null ? null : moment(min).format("YYYY/MM/DD HH:mm");
this.allFilterTimeframe.x_axis.max =
max === null ? null : moment(max).format("YYYY/MM/DD HH:mm");
} catch (error) {
apiError(error, "Failed to load the Filter Parameters.");
}
},
/**
* Test if the Filter Rules Result in Any Data
*/
async checkHasResult() {
const logId = this.logId;
const api = `/api/filters/has-result?log_id=${logId}`;
try {
const response = await apiClient.post(api, this.postRuleData);
this.hasResultRule = response.data.result;
} catch (error) {
apiError(error, "Failed to load the Has Result.");
}
},
/**
* Add a New Temporary Filter
*/
async addTempFilterId() {
const logId = this.logId;
const api = `/api/temp-filters?log_id=${logId}`;
try {
const response = await apiClient.post(api, this.postRuleData);
this.tempFilterId = response.data.id;
} catch (error) {
apiError(error, "Failed to add the Temporary Filters.");
}
},
/**
* Add a New Filter
* @param {string} value file's name
*/
async addFilterId(value) {
const logId = this.logId;
const api = `/api/filters?log_id=${logId}`;
const createFilterObj = {
name: value,
rules: this.postRuleData,
};
try {
const response = await apiClient.post(api, createFilterObj);
this.createFilterId = response.data.id;
this.tempFilterId = null;
} catch (error) {
apiError(error, "Failed to load the Filters.");
}
},
/**
* Get Filter Detail
* @param {number} createFilterId filter type ID
*/
async fetchFunnel(createFilterId) {
const api = `/api/filters/${createFilterId}`;
if (createFilterId) {
try {
const response = await apiClient.get(api);
this.temporaryData = response.data.rules;
this.logId = response.data.log?.id;
this.filterName = response.data.name;
this.baseLogId = response.data.log?.id;
} catch (error) {
apiError(error, "Failed to get Filter Detail.");
}
}
},
/**
* Update an Existing Filter
*/
async updateFilter() {
const createFilterId = this.createFilterId;
const api = `/api/filters/${createFilterId}`;
const data = this.postRuleData;
try {
const response = await apiClient.put(api, data);
this.isUpdateFilter = response.status === 200;
this.tempFilterId = null;
} catch (error) {
apiError(error, "Failed to update an Existing Filter.");
}
},
},
});