// 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/files File management store for listing, uploading, * renaming, downloading, and deleting log/filter/rule files. */ import { defineStore } from "pinia"; import apiClient from "@/api/client.js"; import moment from "moment"; import apiError from "@/module/apiError.js"; import Swal from "sweetalert2"; import { uploadFailedFirst, uploadFailedSecond, uploadloader, uploadSuccess, deleteSuccess, } from "@/module/alertModal.js"; import { useLoadingStore } from "@/stores/loading"; /** Map of file type to API path segment. */ const FILE_TYPE_PATHS = { log: "logs", filter: "filters", "log-check": "log-checks", "filter-check": "filter-checks", }; /** * Returns the API base path for a file type and ID. * @param {string} type - The file type. * @param {number} id - The file ID. * @returns {string} The API base path. */ function getFileApiBase(type, id) { return `/api/${FILE_TYPE_PATHS[type]}/${id}`; } /** Pinia store for file CRUD operations and upload workflow. */ export const useFilesStore = defineStore("filesStore", { state: () => ({ allEventFiles: [ { parentLog: "", fileType: "", ownerName: "", }, ], switchFilesTagData: { ALL: ["Log", "Filter", "Rule", "Design"], DISCOVER: ["Log", "Filter", "Rule"], COMPARE: ["Log", "Filter"], DESIGN: ["Log", "Design"], }, filesTag: "ALL", httpStatus: 200, uploadId: null, allUploadDetail: null, uploadLogId: null, uploadFileName: null, allDependentsData: null, }), getters: { /** * Get allFiles and switch files tag */ allFiles: (state) => { let result = state.allEventFiles; const data = state.switchFilesTagData; const filesTag = state.filesTag; result = result.filter((file) => data[filesTag].includes(file.fileType)); return result; }, /** * Get upload preview */ uploadDetail: (state) => { return state.allUploadDetail; }, /** * Get dependents of files data */ dependentsData: (state) => { return state.allDependentsData; }, }, actions: { /** * Fetch All Files api */ async fetchAllFiles() { const api = "/api/files"; try { const response = await apiClient.get(api); this.allEventFiles = response.data; this.allEventFiles.forEach((o) => { let icon = ""; let fileType = ""; let parentLog = o.name; switch (o.type) { case "log": icon = "work_history"; fileType = "Log"; parentLog = o.name; break; case "filter": icon = "tornado"; fileType = "Filter"; parentLog = o.parent.name; break; case "log-check": case "filter-check": icon = "local_police"; fileType = "Rule"; parentLog = o.parent.name; break; case "design": icon = "shape_line"; fileType = "Design"; parentLog = o.name; break; } o.icon = icon; o.parentLog = parentLog; o.fileType = fileType; o.ownerName = o.owner.name; o.updated_base = o.updated_at; o.accessed_base = o.accessed_at; o.updated_at = moment(o.updated_at) .utcOffset("+08:00") .format("YYYY-MM-DD HH:mm"); o.accessed_at = o.accessed_at ? moment(o.accessed_at) .utcOffset("+08:00") .format("YYYY-MM-DD HH:mm") : null; }); } catch (error) { apiError(error, "Failed to load the files."); } }, /** * Uploads a CSV log file (first stage). * @param {Object} fromData - The form data to send to the backend. */ async upload(fromData) { const api = "/api/logs/csv-uploads"; const config = { data: true, headers: { "Content-Type": "multipart/form-data", }, }; uploadloader(); // Show loading progress bar try { const response = await apiClient.post(api, fromData, config); this.uploadId = response.data.id; this.$router.push({ name: "Upload" }); Swal.close(); // Close the loading progress bar } catch (error) { if (error.response?.status === 422) { // msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv' // type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix' const detail = error.response.data.detail; if (Array.isArray(detail) && detail.length > 0) { const loc = detail[0].loc; uploadFailedFirst(detail[0].type, detail[0].msg, Array.isArray(loc) && loc.length > 2 ? loc[2] : undefined); } } else { Swal.close(); // Close the loading progress bar apiError(error, "Failed to upload the files."); } } }, /** * Fetch upload detail */ async getUploadDetail() { const uploadId = this.uploadId; const api = `/api/logs/csv-uploads/${uploadId}`; try { const response = await apiClient.get(api); this.allUploadDetail = response.data.preview; } catch (error) { apiError(error, "Failed to get upload detail."); } }, /** * Adds a log from an uploaded CSV log file (second stage). * @param {Object} data - The request payload for the backend. */ async uploadLog(data) { const uploadId = this.uploadId; const api = `/api/logs/csv-uploads/${uploadId}`; uploadloader(); // Show loading progress bar try { const response = await apiClient.post(api, data); this.uploadLogId = response.data.id; Swal.close(); // Close the loading progress bar await this.rename(); // Rename the file await uploadSuccess(); this.$router.push({ name: "Files" }); } catch (error) { if (error.response?.status === 422) { const detail = [...error.response.data.detail]; uploadFailedSecond(detail); } else { Swal.close(); // Close the loading progress bar apiError(error, "Failed to upload the log files."); } } }, /** * Rename a Log * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. * @param {string} name - The file name. */ async rename(type, id, fileName) { // If uploadLogId exists, set id and type accordingly; then check the file type. if (this.uploadId && this.uploadFileName) { type = "log"; id = this.uploadLogId; fileName = this.uploadFileName; } const data = { name: fileName }; try { await apiClient.put(`${getFileApiBase(type, id)}/name`, data); this.uploadFileName = null; await this.fetchAllFiles(); } catch (error) { apiError(error, "Failed to rename."); } }, /** * Get the Dependents of the files * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. */ async getDependents(type, id) { try { const response = await apiClient.get( `${getFileApiBase(type, id)}/dependents`, ); this.allDependentsData = response.data; } catch (error) { apiError(error, "Failed to get Dependents of the files."); } }, /** * Delete file * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. */ async deleteFile(type, id) { if (id === null || id === undefined || isNaN(id)) { console.error("Delete File API Error: invalid id"); return; } const loading = useLoadingStore(); loading.isLoading = true; try { await apiClient.delete(getFileApiBase(type, id)); await this.fetchAllFiles(); await deleteSuccess(); } catch (error) { apiError(error, "Failed to delete."); } finally { loading.isLoading = false; } }, /** * Removes a deletion record permanently. * @param {number} id - The file ID. */ async deletionRecord(id) { let api = ""; const loading = useLoadingStore(); loading.isLoading = true; api = `/api/deletion/${id}`; try { await apiClient.delete(api); } catch (error) { apiError(error, "Failed to Remove a Deletion Record."); } finally { loading.isLoading = false; } }, /** * Download file as CSV * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. * @param {string} fileName - The file name. */ async downloadFileCSV(type, id, fileName) { if (type !== "log" && type !== "filter") return; try { const response = await apiClient.get(`${getFileApiBase(type, id)}/csv`); const csvData = response.data; const blob = new Blob([csvData], { type: "text/csv" }); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${fileName}.csv`; link.click(); window.URL.revokeObjectURL(url); } catch (error) { apiError(error, "Failed to download."); } }, }, });