Apply repository-wide ESLint auto-fix formatting pass

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
2026-03-08 12:11:57 +08:00
parent 7c48faaa3d
commit 847904c49b
172 changed files with 13629 additions and 9154 deletions

View File

@@ -16,11 +16,11 @@ const { defineConfig } = require("cypress");
module.exports = defineConfig({
defaultCommandTimeout: 6000,
viewportWidth: 1280,
viewportHeight:720,
viewportHeight: 720,
e2e: {
baseUrl: "http://localhost:4173",
specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}",
},
includeShadowDom: true,
env: {}
env: {},
});

View File

@@ -3,32 +3,32 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Account Management', () => {
describe("Account Management", () => {
beforeEach(() => {
loginWithFixtures();
});
it('displays user list on account admin page', () => {
cy.visit('/account-admin');
cy.wait('@getUsers');
it("displays user list on account admin page", () => {
cy.visit("/account-admin");
cy.wait("@getUsers");
// Should display users from fixture
cy.contains('Test Admin').should('exist');
cy.contains('Alice Wang').should('exist');
cy.contains('Bob Chen').should('exist');
cy.contains("Test Admin").should("exist");
cy.contains("Alice Wang").should("exist");
cy.contains("Bob Chen").should("exist");
});
it('shows active/inactive status badges', () => {
cy.visit('/account-admin');
cy.wait('@getUsers');
it("shows active/inactive status badges", () => {
cy.visit("/account-admin");
cy.wait("@getUsers");
// The user list should show status indicators
cy.contains('testadmin').should('exist');
cy.contains("testadmin").should("exist");
});
it('navigates to my-account page', () => {
cy.visit('/my-account');
cy.wait('@getMyAccount');
cy.url().should('include', '/my-account');
it("navigates to my-account page", () => {
cy.visit("/my-account");
cy.wait("@getMyAccount");
cy.url().should("include", "/my-account");
});
});

View File

@@ -4,55 +4,61 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/07/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../../support/intercept';
import { loginWithFixtures } from "../../support/intercept";
const MSG_ACCOUNT_NOT_UNIQUE = 'Account has already been registered.';
const MSG_ACCOUNT_NOT_UNIQUE = "Account has already been registered.";
describe('Account duplication check.', () => {
describe("Account duplication check.", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('When an account already exists, show error message on confirm.', () => {
const testAccountName = '000000';
it("When an account already exists, show error message on confirm.", () => {
const testAccountName = "000000";
// First creation: account doesn't exist yet
cy.intercept('GET', '/api/users/000000', {
cy.intercept("GET", "/api/users/000000", {
statusCode: 404,
body: { detail: 'Not found' },
}).as('checkNewUser');
body: { detail: "Not found" },
}).as("checkNewUser");
cy.contains('button', 'Create New').should('be.visible').click();
cy.get('#input_account_field').type(testAccountName);
cy.get('#input_name_field').type(testAccountName);
cy.get('#input_first_pwd').type(testAccountName);
cy.get('.checkbox-and-text').first().find('div').first().click();
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type(testAccountName);
cy.get("#input_name_field").type(testAccountName);
cy.get("#input_first_pwd").type(testAccountName);
cy.get(".checkbox-and-text").first().find("div").first().click();
cy.contains('button', 'Confirm')
.should('be.visible')
.and('be.enabled')
cy.contains("button", "Confirm")
.should("be.visible")
.and("be.enabled")
.click();
cy.wait('@postUser');
cy.contains('Account added').should('be.visible');
cy.wait("@postUser");
cy.contains("Account added").should("be.visible");
// Second creation: now account exists — override to return 200
cy.intercept('GET', '/api/users/000000', {
cy.intercept("GET", "/api/users/000000", {
statusCode: 200,
body: { username: '000000', name: '000000', is_admin: false, is_active: true, roles: [] },
}).as('checkExistingUser');
body: {
username: "000000",
name: "000000",
is_admin: false,
is_active: true,
roles: [],
},
}).as("checkExistingUser");
cy.contains('button', 'Create New').should('be.visible').click();
cy.get('#input_account_field').type(testAccountName);
cy.get('#input_name_field').type(testAccountName);
cy.get('#input_first_pwd').type(testAccountName);
cy.get('.checkbox-and-text').first().find('div').first().click();
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type(testAccountName);
cy.get("#input_name_field").type(testAccountName);
cy.get("#input_first_pwd").type(testAccountName);
cy.get(".checkbox-and-text").first().find("div").first().click();
cy.contains('button', 'Confirm')
.should('be.visible')
.and('be.enabled')
cy.contains("button", "Confirm")
.should("be.visible")
.and("be.enabled")
.click();
cy.contains(MSG_ACCOUNT_NOT_UNIQUE).should('be.visible');
cy.contains(MSG_ACCOUNT_NOT_UNIQUE).should("be.visible");
});
});

View File

@@ -4,33 +4,33 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/07/02
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../../support/intercept';
import { loginWithFixtures } from "../../support/intercept";
describe('Password validation on create account.', () => {
describe("Password validation on create account.", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('When password is too short, confirm button stays disabled.', () => {
cy.contains('button', 'Create New').should('be.visible').click();
it("When password is too short, confirm button stays disabled.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get('#input_account_field').type('unit-test-0001');
cy.get('#input_name_field').type('unit-test-0001');
cy.get("#input_account_field").type("unit-test-0001");
cy.get("#input_name_field").type("unit-test-0001");
// Password shorter than 6 characters
cy.get('#input_first_pwd').type('aaa');
cy.get("#input_first_pwd").type("aaa");
cy.contains('button', 'Confirm').should('be.disabled');
cy.contains("button", "Confirm").should("be.disabled");
});
it('When password meets minimum length, confirm button enables.', () => {
cy.contains('button', 'Create New').should('be.visible').click();
it("When password meets minimum length, confirm button enables.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get('#input_account_field').type('unit-test-0001');
cy.get('#input_name_field').type('unit-test-0001');
cy.get('#input_first_pwd').type('aaaaaa');
cy.get("#input_account_field").type("unit-test-0001");
cy.get("#input_name_field").type("unit-test-0001");
cy.get("#input_first_pwd").type("aaaaaa");
cy.contains('button', 'Confirm').should('be.enabled');
cy.contains("button", "Confirm").should("be.enabled");
});
});

View File

@@ -4,39 +4,39 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/07/02
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../../support/intercept';
import { loginWithFixtures } from "../../support/intercept";
describe('Create an Account', () => {
describe("Create an Account", () => {
beforeEach(() => {
loginWithFixtures();
// Override: new usernames should return 404 (account doesn't exist yet)
cy.intercept('GET', '/api/users/unit-test-*', {
cy.intercept("GET", "/api/users/unit-test-*", {
statusCode: 404,
body: { detail: 'Not found' },
}).as('checkNewUser');
cy.visit('/account-admin');
cy.wait('@getUsers');
body: { detail: "Not found" },
}).as("checkNewUser");
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('Create a new account with admin role; should show saved message.', () => {
cy.contains('button', 'Create New').should('be.visible').click();
it("Create a new account with admin role; should show saved message.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get('#input_account_field').type('unit-test-0001');
cy.get('#input_name_field').type('unit-test-0001');
cy.get('#input_first_pwd').type('aaaaaa');
cy.get('.checkbox-and-text').first().find('div').first().click();
cy.get("#input_account_field").type("unit-test-0001");
cy.get("#input_name_field").type("unit-test-0001");
cy.get("#input_first_pwd").type("aaaaaa");
cy.get(".checkbox-and-text").first().find("div").first().click();
cy.contains('button', 'Confirm')
.should('be.visible')
.and('be.enabled')
cy.contains("button", "Confirm")
.should("be.visible")
.and("be.enabled")
.click();
cy.wait('@postUser');
cy.contains('Account added').should('be.visible');
cy.wait("@postUser");
cy.contains("Account added").should("be.visible");
});
it('Confirm button is disabled when required fields are empty.', () => {
cy.contains('button', 'Create New').should('be.visible').click();
cy.get('#input_account_field').type('test');
cy.contains('button', 'Confirm').should('be.disabled');
it("Confirm button is disabled when required fields are empty.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type("test");
cy.contains("button", "Confirm").should("be.disabled");
});
});

View File

@@ -4,27 +4,27 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/07/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../../support/intercept';
import { loginWithFixtures } from "../../support/intercept";
describe('Delete an Account', () => {
describe("Delete an Account", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('Delete button opens confirmation modal and deletes on confirm.', () => {
cy.get('img.delete-account').first().click();
cy.contains('ARE YOU SURE TO DELETE').should('be.visible');
cy.get('#sure_to_delete_acct_btn').click();
cy.wait('@deleteUser');
cy.contains('Account deleted').should('be.visible');
it("Delete button opens confirmation modal and deletes on confirm.", () => {
cy.get("img.delete-account").first().click();
cy.contains("ARE YOU SURE TO DELETE").should("be.visible");
cy.get("#sure_to_delete_acct_btn").click();
cy.wait("@deleteUser");
cy.contains("Account deleted").should("be.visible");
});
it('Cancel button closes the delete confirmation modal.', () => {
cy.get('img.delete-account').first().click();
cy.contains('ARE YOU SURE TO DELETE').should('be.visible');
cy.get('#calcel_delete_acct_btn').click();
cy.contains('ARE YOU SURE TO DELETE').should('not.exist');
it("Cancel button closes the delete confirmation modal.", () => {
cy.get("img.delete-account").first().click();
cy.contains("ARE YOU SURE TO DELETE").should("be.visible");
cy.get("#calcel_delete_acct_btn").click();
cy.contains("ARE YOU SURE TO DELETE").should("not.exist");
});
});

View File

@@ -4,30 +4,30 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/07/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../../support/intercept';
import { loginWithFixtures } from "../../support/intercept";
const MODAL_TITLE_ACCOUNT_EDIT = 'Account Edit';
const MSG_ACCOUNT_EDITED = 'Saved';
const MODAL_TITLE_ACCOUNT_EDIT = "Account Edit";
const MSG_ACCOUNT_EDITED = "Saved";
describe('Edit an account', () => {
describe("Edit an account", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('Edit an account; modify name and see saved message.', () => {
cy.get('.btn-edit').first().click();
cy.wait('@getUserDetail');
it("Edit an account; modify name and see saved message.", () => {
cy.get(".btn-edit").first().click();
cy.wait("@getUserDetail");
cy.contains('h1', MODAL_TITLE_ACCOUNT_EDIT).should('exist');
cy.get('#input_name_field').clear().type('Updated Name');
cy.contains("h1", MODAL_TITLE_ACCOUNT_EDIT).should("exist");
cy.get("#input_name_field").clear().type("Updated Name");
cy.contains('button', 'Confirm')
.should('be.visible')
.and('be.enabled')
cy.contains("button", "Confirm")
.should("be.visible")
.and("be.enabled")
.click();
cy.wait('@putUser');
cy.contains(MSG_ACCOUNT_EDITED).should('be.visible');
cy.wait("@putUser");
cy.contains(MSG_ACCOUNT_EDITED).should("be.visible");
});
});

View File

@@ -3,94 +3,94 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Account Management CRUD', () => {
describe("Account Management CRUD", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('shows Create New button', () => {
cy.get('#create_new_acct_btn').should('exist');
cy.get('#create_new_acct_btn').should('contain', 'Create New');
it("shows Create New button", () => {
cy.get("#create_new_acct_btn").should("exist");
cy.get("#create_new_acct_btn").should("contain", "Create New");
});
it('opens create new account modal', () => {
cy.get('#create_new_acct_btn').click();
cy.get('#modal_container').should('be.visible');
cy.get('#modal_account_edit_or_create_new').should('be.visible');
it("opens create new account modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#modal_container").should("be.visible");
cy.get("#modal_account_edit_or_create_new").should("be.visible");
// Should show account, name, password fields
cy.get('#input_account_field').should('exist');
cy.get('#input_name_field').should('exist');
cy.get('#input_first_pwd').should('exist');
cy.get("#input_account_field").should("exist");
cy.get("#input_name_field").should("exist");
cy.get("#input_first_pwd").should("exist");
});
it('create account confirm is disabled when fields are empty', () => {
cy.get('#create_new_acct_btn').click();
cy.get('.confirm-btn').should('be.disabled');
it("create account confirm is disabled when fields are empty", () => {
cy.get("#create_new_acct_btn").click();
cy.get(".confirm-btn").should("be.disabled");
});
it('create account confirm enables when fields are filled', () => {
cy.get('#create_new_acct_btn').click();
cy.get('#input_account_field').type('newuser');
cy.get('#input_name_field').type('New User');
cy.get('#input_first_pwd').type('password1234');
cy.get('.confirm-btn').should('not.be.disabled');
it("create account confirm enables when fields are filled", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#input_account_field").type("newuser");
cy.get("#input_name_field").type("New User");
cy.get("#input_first_pwd").type("password1234");
cy.get(".confirm-btn").should("not.be.disabled");
});
it('cancel button closes the modal', () => {
cy.get('#create_new_acct_btn').click();
cy.get('#modal_container').should('be.visible');
cy.get('.cancel-btn').click();
cy.get('#modal_container').should('not.exist');
it("cancel button closes the modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#modal_container").should("be.visible");
cy.get(".cancel-btn").click();
cy.get("#modal_container").should("not.exist");
});
it('close (X) button closes the modal', () => {
cy.get('#create_new_acct_btn').click();
cy.get('#modal_container').should('be.visible');
it("close (X) button closes the modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#modal_container").should("be.visible");
cy.get('img[alt="X"]').click();
cy.get('#modal_container').should('not.exist');
cy.get("#modal_container").should("not.exist");
});
it('double-click username opens account info modal', () => {
it("double-click username opens account info modal", () => {
// Double-click on the first account username
cy.get('.account-cell').first().dblclick();
cy.get('#modal_container').should('be.visible');
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
});
it('delete button opens delete confirmation modal', () => {
it("delete button opens delete confirmation modal", () => {
// Click the delete icon for a non-current user
cy.get('.delete-account').first().click();
cy.get('#modal_container').should('be.visible');
cy.get('#modal_delete_acct_alert').should('be.visible');
cy.get(".delete-account").first().click();
cy.get("#modal_container").should("be.visible");
cy.get("#modal_delete_acct_alert").should("be.visible");
});
it('delete modal has Yes and No buttons', () => {
cy.get('.delete-account').first().click();
cy.get('#calcel_delete_acct_btn').should('exist');
cy.get('#sure_to_delete_acct_btn').should('exist');
it("delete modal has Yes and No buttons", () => {
cy.get(".delete-account").first().click();
cy.get("#calcel_delete_acct_btn").should("exist");
cy.get("#sure_to_delete_acct_btn").should("exist");
});
it('delete modal No button closes the modal', () => {
cy.get('.delete-account').first().click();
cy.get('#calcel_delete_acct_btn').click();
cy.get('#modal_container').should('not.exist');
it("delete modal No button closes the modal", () => {
cy.get(".delete-account").first().click();
cy.get("#calcel_delete_acct_btn").click();
cy.get("#modal_container").should("not.exist");
});
it('shows checkboxes for Set as Admin and Activate in create modal', () => {
cy.get('#create_new_acct_btn').click();
cy.get('#account_create_checkboxes_section').should('be.visible');
cy.contains('Set as admin.').should('exist');
cy.contains('Activate now.').should('exist');
it("shows checkboxes for Set as Admin and Activate in create modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#account_create_checkboxes_section").should("be.visible");
cy.contains("Set as admin.").should("exist");
cy.contains("Activate now.").should("exist");
});
it('search bar filters user list', () => {
it("search bar filters user list", () => {
// Search filters by username, not display name
cy.get('#input_search').type('user1');
cy.get("#input_search").type("user1");
cy.get('img[alt="search"]').click();
// Should only show user1 (Alice Wang)
cy.contains('user1').should('exist');
cy.contains("user1").should("exist");
});
});

View File

@@ -3,38 +3,38 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Account Info Modal', () => {
describe("Account Info Modal", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('double-click username opens info modal with user data', () => {
cy.get('.account-cell').first().dblclick();
cy.get('#modal_container').should('be.visible');
cy.get('#acct_info_user_name').should('exist');
it("double-click username opens info modal with user data", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.get("#acct_info_user_name").should("exist");
});
it('info modal shows Account Information header', () => {
cy.get('.account-cell').first().dblclick();
cy.get('#modal_container').should('be.visible');
cy.contains('Account Information').should('exist');
it("info modal shows Account Information header", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.contains("Account Information").should("exist");
});
it('info modal shows account visit info', () => {
cy.get('.account-cell').first().dblclick();
cy.get('#modal_container').should('be.visible');
cy.get('#account_visit_info').should('exist');
cy.get('#account_visit_info').should('contain', 'Account:');
it("info modal shows account visit info", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.get("#account_visit_info").should("exist");
cy.get("#account_visit_info").should("contain", "Account:");
});
it('info modal can be closed via X button', () => {
cy.get('.account-cell').first().dblclick();
cy.get('#modal_container').should('be.visible');
it("info modal can be closed via X button", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.get('img[alt="X"]').click();
cy.get('#modal_container').should('not.exist');
cy.get("#modal_container").should("not.exist");
});
});

View File

@@ -5,78 +5,86 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/05/30
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Compare', () => {
describe("Compare", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/files');
cy.wait('@getFiles');
cy.contains('li', 'COMPARE').click();
cy.visit("/files");
cy.wait("@getFiles");
cy.contains("li", "COMPARE").click();
});
it('Compare dropdown sorting options', () => {
it("Compare dropdown sorting options", () => {
const expectedOptions = [
'By File Name (A to Z)', 'By File Name (Z to A)',
'By Dependency (A to Z)', 'By Dependency (Z to A)',
'By File Type (A to Z)', 'By File Type (Z to A)',
'By Last Update (A to Z)', 'By Last Update (Z to A)',
"By File Name (A to Z)",
"By File Name (Z to A)",
"By Dependency (A to Z)",
"By Dependency (Z to A)",
"By File Type (A to Z)",
"By File Type (Z to A)",
"By Last Update (A to Z)",
"By Last Update (Z to A)",
];
cy.get('.p-select').click();
cy.get('.p-select-list')
.find('.p-select-option')
.then($options => {
cy.get(".p-select").click();
cy.get(".p-select-list")
.find(".p-select-option")
.then(($options) => {
const actualOptions = $options
.map((index, elem) => Cypress.$(elem).find('.p-select-option-label').text())
.map((index, elem) =>
Cypress.$(elem).find(".p-select-option-label").text(),
)
.get();
expect(actualOptions).to.deep.equal(expectedOptions);
});
});
it('Grid cards are rendered for compare file selection', () => {
cy.get('#compareGridCards').find('li').should('have.length.greaterThan', 0);
it("Grid cards are rendered for compare file selection", () => {
cy.get("#compareGridCards").find("li").should("have.length.greaterThan", 0);
});
it('Compare button is disabled until two files are dragged', () => {
cy.contains('button', 'Compare').should('be.disabled');
cy.get('#compareFile0').drag('#primaryDragCard');
cy.get('#compareFile1').drag('#secondaryDragCard');
cy.contains('button', 'Compare').should('be.enabled');
it("Compare button is disabled until two files are dragged", () => {
cy.contains("button", "Compare").should("be.disabled");
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").should("be.enabled");
});
it('Enter Compare dashboard and see charts', () => {
cy.get('#compareFile0').drag('#primaryDragCard');
cy.get('#compareFile1').drag('#secondaryDragCard');
cy.contains('button', 'Compare').click();
cy.wait('@getCompare');
cy.url().should('include', 'compare');
it("Enter Compare dashboard and see charts", () => {
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").click();
cy.wait("@getCompare");
cy.url().should("include", "compare");
// Assert chart title spans are visible
cy.contains('span', 'Average Cycle Time').should('exist');
cy.contains('span', 'Cycle Efficiency').should('exist');
cy.contains('span', 'Average Processing Time').should('exist');
cy.contains('span', 'Average Processing Time by Activity').should('exist');
cy.contains('span', 'Average Waiting Time').should('exist');
cy.contains('span', 'Average Waiting Time between Activity').should('exist');
cy.contains("span", "Average Cycle Time").should("exist");
cy.contains("span", "Cycle Efficiency").should("exist");
cy.contains("span", "Average Processing Time").should("exist");
cy.contains("span", "Average Processing Time by Activity").should("exist");
cy.contains("span", "Average Waiting Time").should("exist");
cy.contains("span", "Average Waiting Time between Activity").should(
"exist",
);
});
it('Compare State button exists on dashboard', () => {
cy.get('#compareFile0').drag('#primaryDragCard');
cy.get('#compareFile1').drag('#secondaryDragCard');
cy.contains('button', 'Compare').click();
cy.wait('@getCompare');
it("Compare State button exists on dashboard", () => {
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").click();
cy.wait("@getCompare");
cy.get('#compareState').should('exist').and('be.visible');
cy.get("#compareState").should("exist").and("be.visible");
});
it('Sidebar shows time usage and frequency sections', () => {
cy.get('#compareFile0').drag('#primaryDragCard');
cy.get('#compareFile1').drag('#secondaryDragCard');
cy.contains('button', 'Compare').click();
cy.wait('@getCompare');
it("Sidebar shows time usage and frequency sections", () => {
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").click();
cy.wait("@getCompare");
cy.get('aside').should('exist');
cy.get('aside li').should('have.length.greaterThan', 0);
cy.get("aside").should("exist");
cy.get("aside li").should("have.length.greaterThan", 0);
});
});

View File

@@ -3,53 +3,53 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Discover Conformance Page', () => {
describe("Discover Conformance Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/discover/log/297310264/conformance');
cy.wait('@getLogCheckParams');
cy.visit("/discover/log/297310264/conformance");
cy.wait("@getLogCheckParams");
});
it('page loads and loading overlay disappears', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
it("page loads and loading overlay disappears", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
});
it('displays Rule Settings sidebar', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Rule Settings').should('be.visible');
it("displays Rule Settings sidebar", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Rule Settings").should("be.visible");
});
it('displays Conformance Checking Results heading', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Conformance Checking Results').should('be.visible');
it("displays Conformance Checking Results heading", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Conformance Checking Results").should("be.visible");
});
it('displays rule type radio options', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Have activity').should('be.visible');
cy.contains('Activity sequence').should('be.visible');
cy.contains('Activity duration').should('be.visible');
cy.contains('Processing time').should('be.visible');
cy.contains('Waiting time').should('be.visible');
cy.contains('Cycle time').should('be.visible');
it("displays rule type radio options", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Have activity").should("be.visible");
cy.contains("Activity sequence").should("be.visible");
cy.contains("Activity duration").should("be.visible");
cy.contains("Processing time").should("be.visible");
cy.contains("Waiting time").should("be.visible");
cy.contains("Cycle time").should("be.visible");
});
it('displays Clear and Apply buttons', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('button', 'Clear').should('be.visible');
cy.contains('button', 'Apply').should('exist');
it("displays Clear and Apply buttons", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("button", "Clear").should("be.visible");
cy.contains("button", "Apply").should("exist");
});
it('displays Activity list area', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Activity list').should('be.visible');
it("displays Activity list area", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Activity list").should("be.visible");
});
it('displays default placeholder values in results', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Conformance Rate').should('be.visible');
cy.contains('Cases').should('be.visible');
it("displays default placeholder values in results", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Conformance Rate").should("be.visible");
cy.contains("Cases").should("be.visible");
});
});

View File

@@ -3,49 +3,55 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Discover Map Page', () => {
describe("Discover Map Page", () => {
beforeEach(() => {
loginWithFixtures();
// Suppress Cytoscape rendering errors in headless mode
cy.on('uncaught:exception', () => false);
cy.visit('/discover/log/297310264/map');
cy.wait('@getDiscover');
cy.on("uncaught:exception", () => false);
cy.visit("/discover/log/297310264/map");
cy.wait("@getDiscover");
});
it('page loads and cytoscape container exists', () => {
cy.get('#cy').should('exist');
it("page loads and cytoscape container exists", () => {
cy.get("#cy").should("exist");
});
it('displays left sidebar buttons', () => {
it("displays left sidebar buttons", () => {
// Visualization Setting, Filter, Traces buttons
cy.get('.material-symbols-outlined').contains('track_changes').should('exist');
cy.get('.material-symbols-outlined').contains('tornado').should('exist');
cy.get('.material-symbols-outlined').contains('rebase').should('exist');
cy.get(".material-symbols-outlined")
.contains("track_changes")
.should("exist");
cy.get(".material-symbols-outlined").contains("tornado").should("exist");
cy.get(".material-symbols-outlined").contains("rebase").should("exist");
});
it('displays right sidebar Summary button', () => {
cy.get('#sidebar_state').should('exist');
cy.get('#iconState').should('exist');
it("displays right sidebar Summary button", () => {
cy.get("#sidebar_state").should("exist");
cy.get("#iconState").should("exist");
});
it('clicking Visualization Setting button toggles sidebar', () => {
it("clicking Visualization Setting button toggles sidebar", () => {
// Click the track_changes icon (Visualization Setting)
cy.contains('span.material-symbols-outlined', 'track_changes').parent('li').click();
cy.contains("span.material-symbols-outlined", "track_changes")
.parent("li")
.click();
// SidebarView should open
cy.contains('Visualization Setting').should('be.visible');
cy.contains("Visualization Setting").should("be.visible");
});
it('clicking Summary button toggles sidebar', () => {
cy.get('#iconState').click();
it("clicking Summary button toggles sidebar", () => {
cy.get("#iconState").click();
// SidebarState should open with insights/stats
cy.contains('Summary').should('be.visible');
cy.contains("Summary").should("be.visible");
});
it('clicking Traces button toggles sidebar', () => {
cy.contains('span.material-symbols-outlined', 'rebase').parent('li').click();
it("clicking Traces button toggles sidebar", () => {
cy.contains("span.material-symbols-outlined", "rebase")
.parent("li")
.click();
// SidebarTraces should open
cy.contains('Traces').should('be.visible');
cy.contains("Traces").should("be.visible");
});
});

View File

@@ -3,64 +3,64 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Discover Performance Page', () => {
describe("Discover Performance Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/discover/log/297310264/performance');
cy.wait('@getPerformance');
cy.visit("/discover/log/297310264/performance");
cy.wait("@getPerformance");
});
it('page loads and loading overlay disappears', () => {
it("page loads and loading overlay disappears", () => {
// Loading overlay should not be visible after data loads
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
});
it('displays Time Usage sidebar section', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Time Usage').should('be.visible');
it("displays Time Usage sidebar section", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Time Usage").should("be.visible");
});
it('displays Frequency sidebar section', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Frequency').should('be.visible');
it("displays Frequency sidebar section", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Frequency").should("be.visible");
});
it('displays sidebar navigation items', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Cycle Time & Efficiency').should('be.visible');
cy.contains('Processing Time').should('be.visible');
cy.contains('Waiting Time').should('be.visible');
cy.contains('Number of Cases').should('be.visible');
it("displays sidebar navigation items", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Cycle Time & Efficiency").should("be.visible");
cy.contains("Processing Time").should("be.visible");
cy.contains("Waiting Time").should("be.visible");
cy.contains("Number of Cases").should("be.visible");
});
it('displays chart titles', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Average Cycle Time').should('be.visible');
cy.contains('Cycle Efficiency').should('be.visible');
cy.contains('Average Processing Time').should('be.visible');
cy.contains('Average Processing Time by Activity').should('be.visible');
cy.contains('Average Waiting Time').should('be.visible');
it("displays chart titles", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Average Cycle Time").should("be.visible");
cy.contains("Cycle Efficiency").should("be.visible");
cy.contains("Average Processing Time").should("be.visible");
cy.contains("Average Processing Time by Activity").should("be.visible");
cy.contains("Average Waiting Time").should("be.visible");
});
it('displays frequency chart titles', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('New Cases').should('be.visible');
cy.contains('Number of Cases by Activity').should('be.visible');
it("displays frequency chart titles", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("New Cases").should("be.visible");
cy.contains("Number of Cases by Activity").should("be.visible");
});
it('renders canvas elements for charts', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
it("renders canvas elements for charts", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
// Chart.js renders into canvas elements
cy.get('canvas').should('have.length.at.least', 5);
cy.get("canvas").should("have.length.at.least", 5);
});
it('sidebar navigation scrolls to section', () => {
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
it("sidebar navigation scrolls to section", () => {
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
// Click on "Waiting Time" in sidebar
cy.contains('li', 'Waiting Time').click();
cy.contains("li", "Waiting Time").click();
// The Waiting Time section should be in view
cy.get('#waitingTime').should('exist');
cy.get("#waitingTime").should("exist");
});
});

View File

@@ -3,94 +3,94 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Discover Tab Navigation', () => {
describe("Discover Tab Navigation", () => {
beforeEach(() => {
loginWithFixtures();
// Suppress Cytoscape rendering errors in headless mode
cy.on('uncaught:exception', () => false);
cy.on("uncaught:exception", () => false);
});
describe('navigating from Map page', () => {
describe("navigating from Map page", () => {
beforeEach(() => {
cy.visit('/discover/log/297310264/map');
cy.wait('@getDiscover');
cy.visit("/discover/log/297310264/map");
cy.wait("@getDiscover");
});
it('shows DISCOVER heading and MAP/CONFORMANCE/PERFORMANCE tabs', () => {
cy.get('#nav_bar').contains('DISCOVER').should('be.visible');
cy.get('.nav-item').should('have.length', 3);
cy.get('.nav-item').eq(0).should('contain', 'MAP');
cy.get('.nav-item').eq(1).should('contain', 'CONFORMANCE');
cy.get('.nav-item').eq(2).should('contain', 'PERFORMANCE');
it("shows DISCOVER heading and MAP/CONFORMANCE/PERFORMANCE tabs", () => {
cy.get("#nav_bar").contains("DISCOVER").should("be.visible");
cy.get(".nav-item").should("have.length", 3);
cy.get(".nav-item").eq(0).should("contain", "MAP");
cy.get(".nav-item").eq(1).should("contain", "CONFORMANCE");
cy.get(".nav-item").eq(2).should("contain", "PERFORMANCE");
});
it('clicking PERFORMANCE tab navigates to performance page', () => {
cy.get('.nav-item').contains('PERFORMANCE').click();
cy.url().should('include', '/performance');
cy.wait('@getPerformance');
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Time Usage').should('be.visible');
it("clicking PERFORMANCE tab navigates to performance page", () => {
cy.get(".nav-item").contains("PERFORMANCE").click();
cy.url().should("include", "/performance");
cy.wait("@getPerformance");
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Time Usage").should("be.visible");
});
it('clicking CONFORMANCE tab navigates to conformance page', () => {
cy.get('.nav-item').contains('CONFORMANCE').click();
cy.url().should('include', '/conformance');
cy.wait('@getLogCheckParams');
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Rule Settings').should('be.visible');
it("clicking CONFORMANCE tab navigates to conformance page", () => {
cy.get(".nav-item").contains("CONFORMANCE").click();
cy.url().should("include", "/conformance");
cy.wait("@getLogCheckParams");
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Rule Settings").should("be.visible");
});
it('shows back arrow to return to Files', () => {
cy.get('#backPage').should('exist');
cy.get('#backPage').should('have.attr', 'href', '/files');
it("shows back arrow to return to Files", () => {
cy.get("#backPage").should("exist");
cy.get("#backPage").should("have.attr", "href", "/files");
});
});
describe('navigating from Performance page', () => {
describe("navigating from Performance page", () => {
beforeEach(() => {
cy.visit('/discover/log/297310264/performance');
cy.wait('@getPerformance');
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.visit("/discover/log/297310264/performance");
cy.wait("@getPerformance");
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
});
it('clicking MAP tab navigates to map page', () => {
cy.get('.nav-item').contains('MAP').click();
cy.url().should('include', '/map');
cy.wait('@getDiscover');
cy.get('#cy').should('exist');
it("clicking MAP tab navigates to map page", () => {
cy.get(".nav-item").contains("MAP").click();
cy.url().should("include", "/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
it('clicking CONFORMANCE tab navigates to conformance page', () => {
cy.get('.nav-item').contains('CONFORMANCE').click();
cy.url().should('include', '/conformance');
cy.wait('@getLogCheckParams');
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Rule Settings').should('be.visible');
it("clicking CONFORMANCE tab navigates to conformance page", () => {
cy.get(".nav-item").contains("CONFORMANCE").click();
cy.url().should("include", "/conformance");
cy.wait("@getLogCheckParams");
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Rule Settings").should("be.visible");
});
});
describe('navigating from Conformance page', () => {
describe("navigating from Conformance page", () => {
beforeEach(() => {
cy.visit('/discover/log/297310264/conformance');
cy.wait('@getLogCheckParams');
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.visit("/discover/log/297310264/conformance");
cy.wait("@getLogCheckParams");
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
});
it('clicking MAP tab navigates to map page', () => {
cy.get('.nav-item').contains('MAP').click();
cy.url().should('include', '/map');
cy.wait('@getDiscover');
cy.get('#cy').should('exist');
it("clicking MAP tab navigates to map page", () => {
cy.get(".nav-item").contains("MAP").click();
cy.url().should("include", "/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
it('clicking PERFORMANCE tab navigates to performance page', () => {
cy.get('.nav-item').contains('PERFORMANCE').click();
cy.url().should('include', '/performance');
cy.wait('@getPerformance');
cy.get('.z-\\[9999\\]', { timeout: 10000 }).should('not.exist');
cy.contains('Time Usage').should('be.visible');
it("clicking PERFORMANCE tab navigates to performance page", () => {
cy.get(".nav-item").contains("PERFORMANCE").click();
cy.url().should("include", "/performance");
cy.wait("@getPerformance");
cy.get(".z-\\[9999\\]", { timeout: 10000 }).should("not.exist");
cy.contains("Time Usage").should("be.visible");
});
});
});

View File

@@ -3,92 +3,92 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Edge Cases', () => {
describe('Empty states', () => {
it('files page handles empty file list', () => {
describe("Edge Cases", () => {
describe("Empty states", () => {
it("files page handles empty file list", () => {
loginWithFixtures();
// Override files intercept with empty array
cy.intercept('GET', '/api/files', {
cy.intercept("GET", "/api/files", {
statusCode: 200,
body: [],
}).as('getEmptyFiles');
cy.visit('/files');
cy.wait('@getEmptyFiles');
}).as("getEmptyFiles");
cy.visit("/files");
cy.wait("@getEmptyFiles");
// Table should exist but have no file data
cy.get('table').should('exist');
cy.contains('sample-process.xes').should('not.exist');
cy.get("table").should("exist");
cy.contains("sample-process.xes").should("not.exist");
});
it('account admin handles empty user list', () => {
it("account admin handles empty user list", () => {
loginWithFixtures();
cy.intercept('GET', '/api/users', {
cy.intercept("GET", "/api/users", {
statusCode: 200,
body: [],
}).as('getEmptyUsers');
cy.visit('/account-admin');
cy.wait('@getEmptyUsers');
}).as("getEmptyUsers");
cy.visit("/account-admin");
cy.wait("@getEmptyUsers");
// Create New button should still work
cy.get('#create_new_acct_btn').should('exist');
cy.get("#create_new_acct_btn").should("exist");
});
});
describe('Authentication guard', () => {
it('unauthenticated user is redirected to login', () => {
describe("Authentication guard", () => {
it("unauthenticated user is redirected to login", () => {
// No loginWithFixtures - not logged in
cy.visit('/files');
cy.url().should('include', '/login');
cy.visit("/files");
cy.url().should("include", "/login");
});
it('unauthenticated user cannot access account-admin', () => {
cy.visit('/account-admin');
cy.url().should('include', '/login');
it("unauthenticated user cannot access account-admin", () => {
cy.visit("/account-admin");
cy.url().should("include", "/login");
});
it('unauthenticated user cannot access my-account', () => {
cy.visit('/my-account');
cy.url().should('include', '/login');
it("unauthenticated user cannot access my-account", () => {
cy.visit("/my-account");
cy.url().should("include", "/login");
});
});
describe('Login validation', () => {
it('shows error on failed login', () => {
cy.intercept('POST', '/api/oauth/token', {
describe("Login validation", () => {
it("shows error on failed login", () => {
cy.intercept("POST", "/api/oauth/token", {
statusCode: 401,
body: { detail: 'Invalid credentials' },
}).as('failedLogin');
cy.visit('/login');
cy.get('#account').type('wrong');
cy.get('#password').type('wrong');
cy.get('form').submit();
cy.wait('@failedLogin');
body: { detail: "Invalid credentials" },
}).as("failedLogin");
cy.visit("/login");
cy.get("#account").type("wrong");
cy.get("#password").type("wrong");
cy.get("form").submit();
cy.wait("@failedLogin");
// Should stay on login page
cy.url().should('include', '/login');
cy.url().should("include", "/login");
});
});
describe('Account creation validation', () => {
describe("Account creation validation", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.get('#create_new_acct_btn').click();
cy.visit("/account-admin");
cy.wait("@getUsers");
cy.get("#create_new_acct_btn").click();
});
it('confirm stays disabled with only account field filled', () => {
cy.get('#input_account_field').type('newuser');
cy.get('.confirm-btn').should('be.disabled');
it("confirm stays disabled with only account field filled", () => {
cy.get("#input_account_field").type("newuser");
cy.get(".confirm-btn").should("be.disabled");
});
it('confirm stays disabled with only name field filled', () => {
cy.get('#input_name_field').type('New User');
cy.get('.confirm-btn').should('be.disabled');
it("confirm stays disabled with only name field filled", () => {
cy.get("#input_name_field").type("New User");
cy.get(".confirm-btn").should("be.disabled");
});
it('confirm stays disabled with only password field filled', () => {
cy.get('#input_first_pwd').type('password1234');
cy.get('.confirm-btn').should('be.disabled');
it("confirm stays disabled with only password field filled", () => {
cy.get("#input_first_pwd").type("password1234");
cy.get(".confirm-btn").should("be.disabled");
});
});
});

View File

@@ -3,72 +3,72 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('File Operations', () => {
describe("File Operations", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/files');
cy.wait('@getFiles');
cy.visit("/files");
cy.wait("@getFiles");
});
it('file list table has sortable columns', () => {
it("file list table has sortable columns", () => {
// Check that table headers exist with expected columns
cy.get('table').within(() => {
cy.contains('th', 'Name').should('exist');
cy.contains('th', 'Dependency').should('exist');
cy.contains('th', 'File Type').should('exist');
cy.contains('th', 'Owner').should('exist');
cy.contains('th', 'Last Update').should('exist');
cy.get("table").within(() => {
cy.contains("th", "Name").should("exist");
cy.contains("th", "Dependency").should("exist");
cy.contains("th", "File Type").should("exist");
cy.contains("th", "Owner").should("exist");
cy.contains("th", "Last Update").should("exist");
});
});
it('clicking column header sorts the table', () => {
it("clicking column header sorts the table", () => {
// Click "Name" header to sort
cy.contains('th', 'Name').click();
cy.contains("th", "Name").click();
// After sorting, table should still have data
cy.get('table tbody tr').should('have.length.greaterThan', 0);
cy.get("table tbody tr").should("have.length.greaterThan", 0);
});
it('table rows show file data from fixture', () => {
cy.get('table tbody').within(() => {
cy.contains('sample-process.xes').should('exist');
cy.contains('filtered-sample').should('exist');
cy.contains('production-log.csv').should('exist');
it("table rows show file data from fixture", () => {
cy.get("table tbody").within(() => {
cy.contains("sample-process.xes").should("exist");
cy.contains("filtered-sample").should("exist");
cy.contains("production-log.csv").should("exist");
});
});
it('table shows owner names', () => {
cy.get('table tbody').within(() => {
cy.contains('Test Admin').should('exist');
cy.contains('Alice Wang').should('exist');
it("table shows owner names", () => {
cy.get("table tbody").within(() => {
cy.contains("Test Admin").should("exist");
cy.contains("Alice Wang").should("exist");
});
});
it('table shows file types', () => {
cy.get('table tbody').within(() => {
cy.contains('log').should('exist');
it("table shows file types", () => {
cy.get("table tbody").within(() => {
cy.contains("log").should("exist");
});
});
it('right-click on file row shows context menu', () => {
it("right-click on file row shows context menu", () => {
// PrimeVue DataTable with contextmenu
cy.get('table tbody tr').first().rightclick();
cy.get("table tbody tr").first().rightclick();
// Context menu behavior depends on implementation
// Just verify the right-click doesn't break anything
cy.get('table tbody tr').should('have.length.greaterThan', 0);
cy.get("table tbody tr").should("have.length.greaterThan", 0);
});
it('grid view shows file cards', () => {
it("grid view shows file cards", () => {
// Switch to grid view
cy.get('svg').parent('li.cursor-pointer').last().click();
cy.get("svg").parent("li.cursor-pointer").last().click();
// Grid cards should be visible
cy.get('li[title]').should('have.length.greaterThan', 0);
cy.get("li[title]").should("have.length.greaterThan", 0);
});
it('Import button opens upload modal', () => {
cy.get('#import_btn').click();
it("Import button opens upload modal", () => {
cy.get("#import_btn").click();
// Upload modal should appear
cy.get('#import_btn').should('exist');
cy.get("#import_btn").should("exist");
});
});

View File

@@ -3,58 +3,58 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Files Page', () => {
describe("Files Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/files');
cy.visit("/files");
});
it('displays the file list after login', () => {
cy.wait('@getFiles');
cy.contains('h2', 'All Files').should('exist');
it("displays the file list after login", () => {
cy.wait("@getFiles");
cy.contains("h2", "All Files").should("exist");
// Should display file names from fixture
cy.contains('sample-process.xes').should('exist');
cy.contains('filtered-sample').should('exist');
cy.contains('production-log.csv').should('exist');
cy.contains("sample-process.xes").should("exist");
cy.contains("filtered-sample").should("exist");
cy.contains("production-log.csv").should("exist");
});
it('shows Recently Used section', () => {
cy.wait('@getFiles');
cy.contains('h2', 'Recently Used').should('exist');
it("shows Recently Used section", () => {
cy.wait("@getFiles");
cy.contains("h2", "Recently Used").should("exist");
});
it('switches to DISCOVER tab', () => {
cy.wait('@getFiles');
cy.contains('.nav-item', 'DISCOVER').click();
it("switches to DISCOVER tab", () => {
cy.wait("@getFiles");
cy.contains(".nav-item", "DISCOVER").click();
// DISCOVER tab shows filtered file types
cy.contains('h2', 'All Files').should('exist');
cy.contains("h2", "All Files").should("exist");
});
it('switches to COMPARE tab and shows drag zones', () => {
cy.wait('@getFiles');
cy.contains('.nav-item', 'COMPARE').click();
cy.contains('Performance Comparison').should('exist');
cy.contains('Drag and drop a file here').should('exist');
it("switches to COMPARE tab and shows drag zones", () => {
cy.wait("@getFiles");
cy.contains(".nav-item", "COMPARE").click();
cy.contains("Performance Comparison").should("exist");
cy.contains("Drag and drop a file here").should("exist");
});
it('shows Import button on FILES tab', () => {
cy.wait('@getFiles');
cy.get('#import_btn').should('contain', 'Import');
it("shows Import button on FILES tab", () => {
cy.wait("@getFiles");
cy.get("#import_btn").should("contain", "Import");
});
it('can switch between list and grid view', () => {
cy.wait('@getFiles');
it("can switch between list and grid view", () => {
cy.wait("@getFiles");
// DataTable (list view) should be visible by default
cy.get('table').should('exist');
cy.get("table").should("exist");
});
it('double-click file navigates to discover page', () => {
cy.wait('@getFiles');
it("double-click file navigates to discover page", () => {
cy.wait("@getFiles");
// Double-click the first file row in the table
// The actual route depends on file type (log→map, log-check→conformance, etc.)
cy.get('table tbody tr').first().dblclick();
cy.url().should('include', '/discover');
cy.get("table tbody tr").first().dblclick();
cy.url().should("include", "/discover");
});
});

View File

@@ -3,47 +3,47 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Files Page - COMPARE Tab', () => {
describe("Files Page - COMPARE Tab", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/files');
cy.wait('@getFiles');
cy.visit("/files");
cy.wait("@getFiles");
// Switch to COMPARE tab
cy.contains('li', 'COMPARE').click();
cy.contains("li", "COMPARE").click();
});
it('shows Performance Comparison heading', () => {
cy.contains('h2', 'Performance Comparison').should('be.visible');
it("shows Performance Comparison heading", () => {
cy.contains("h2", "Performance Comparison").should("be.visible");
});
it('shows two drag-and-drop slots', () => {
cy.get('#primaryDragCard').should('exist');
cy.get('#secondaryDragCard').should('exist');
it("shows two drag-and-drop slots", () => {
cy.get("#primaryDragCard").should("exist");
cy.get("#secondaryDragCard").should("exist");
});
it('drag slots show placeholder text', () => {
cy.get('#primaryDragCard').should('contain', 'Drag and drop a file here');
cy.get('#secondaryDragCard').should('contain', 'Drag and drop a file here');
it("drag slots show placeholder text", () => {
cy.get("#primaryDragCard").should("contain", "Drag and drop a file here");
cy.get("#secondaryDragCard").should("contain", "Drag and drop a file here");
});
it('Compare button is disabled when no files are dragged', () => {
cy.contains('button', 'Compare').should('be.disabled');
it("Compare button is disabled when no files are dragged", () => {
cy.contains("button", "Compare").should("be.disabled");
});
it('shows sorting dropdown', () => {
cy.get('.p-select').should('exist');
it("shows sorting dropdown", () => {
cy.get(".p-select").should("exist");
});
it('grid cards display file names', () => {
cy.get('#compareGridCards').should('exist');
cy.get('#compareGridCards li').should('have.length.greaterThan', 0);
it("grid cards display file names", () => {
cy.get("#compareGridCards").should("exist");
cy.get("#compareGridCards li").should("have.length.greaterThan", 0);
});
it('clicking sorting dropdown shows sort options', () => {
cy.get('.p-select').click();
cy.get('.p-select-list').should('be.visible');
cy.contains('.p-select-option', 'By File Name').should('exist');
it("clicking sorting dropdown shows sort options", () => {
cy.get(".p-select").click();
cy.get(".p-select-list").should("be.visible");
cy.contains(".p-select-option", "By File Name").should("exist");
});
});

View File

@@ -3,72 +3,72 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Files to Discover Entry Flow', () => {
describe("Files to Discover Entry Flow", () => {
beforeEach(() => {
loginWithFixtures();
// Suppress Cytoscape rendering errors in headless mode
cy.on('uncaught:exception', () => false);
cy.visit('/files');
cy.wait('@getFiles');
cy.on("uncaught:exception", () => false);
cy.visit("/files");
cy.wait("@getFiles");
});
describe('double-click table row to enter Discover', () => {
it('double-click log file navigates to Map page', () => {
describe("double-click table row to enter Discover", () => {
it("double-click log file navigates to Map page", () => {
// Target the Name column (has class .fileName) to avoid matching Dependency column
cy.contains('td.fileName', 'sample-process.xes').parent('tr').dblclick();
cy.url().should('include', '/discover/log/1/map');
cy.wait('@getDiscover');
cy.get('#cy').should('exist');
cy.contains("td.fileName", "sample-process.xes").parent("tr").dblclick();
cy.url().should("include", "/discover/log/1/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
it('double-click filter file navigates to Map page', () => {
cy.contains('td.fileName', 'filtered-sample').parent('tr').dblclick();
cy.url().should('include', '/discover/filter/10/map');
cy.wait('@getFilterDiscover');
cy.get('#cy').should('exist');
it("double-click filter file navigates to Map page", () => {
cy.contains("td.fileName", "filtered-sample").parent("tr").dblclick();
cy.url().should("include", "/discover/filter/10/map");
cy.wait("@getFilterDiscover");
cy.get("#cy").should("exist");
});
});
describe('double-click grid card to enter Discover', () => {
describe("double-click grid card to enter Discover", () => {
beforeEach(() => {
// Switch to grid view
cy.get('svg').parent('li.cursor-pointer').last().click();
cy.get("svg").parent("li.cursor-pointer").last().click();
});
it('double-click log file grid card navigates to Map page', () => {
it("double-click log file grid card navigates to Map page", () => {
// Use last() to target the All Files grid section (not Recently Used)
cy.get('li[title="sample-process.xes"]').last().dblclick();
cy.url().should('include', '/discover/log/1/map');
cy.wait('@getDiscover');
cy.get('#cy').should('exist');
cy.url().should("include", "/discover/log/1/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
});
describe('DISCOVER tab filters files', () => {
it('clicking DISCOVER tab shows only Log, Filter, and Rule files', () => {
cy.get('.nav-item').contains('DISCOVER').click();
cy.contains('td.fileName', 'sample-process.xes').should('exist');
cy.contains('td.fileName', 'filtered-sample').should('exist');
cy.contains('td.fileName', 'conformance-check-1').should('exist');
describe("DISCOVER tab filters files", () => {
it("clicking DISCOVER tab shows only Log, Filter, and Rule files", () => {
cy.get(".nav-item").contains("DISCOVER").click();
cy.contains("td.fileName", "sample-process.xes").should("exist");
cy.contains("td.fileName", "filtered-sample").should("exist");
cy.contains("td.fileName", "conformance-check-1").should("exist");
});
});
describe('Navbar state after entering Discover', () => {
it('shows DISCOVER heading and tabs after entering from Files', () => {
cy.contains('td.fileName', 'sample-process.xes').parent('tr').dblclick();
cy.url().should('include', '/discover/');
cy.get('#nav_bar').contains('DISCOVER').should('be.visible');
cy.get('.nav-item').contains('MAP').should('exist');
cy.get('.nav-item').contains('CONFORMANCE').should('exist');
cy.get('.nav-item').contains('PERFORMANCE').should('exist');
describe("Navbar state after entering Discover", () => {
it("shows DISCOVER heading and tabs after entering from Files", () => {
cy.contains("td.fileName", "sample-process.xes").parent("tr").dblclick();
cy.url().should("include", "/discover/");
cy.get("#nav_bar").contains("DISCOVER").should("be.visible");
cy.get(".nav-item").contains("MAP").should("exist");
cy.get(".nav-item").contains("CONFORMANCE").should("exist");
cy.get(".nav-item").contains("PERFORMANCE").should("exist");
});
it('shows back arrow pointing to /files', () => {
cy.contains('td.fileName', 'sample-process.xes').parent('tr').dblclick();
cy.url().should('include', '/discover/');
cy.get('#backPage').should('have.attr', 'href', '/files');
it("shows back arrow pointing to /files", () => {
cy.contains("td.fileName", "sample-process.xes").parent("tr").dblclick();
cy.url().should("include", "/discover/");
cy.get("#backPage").should("have.attr", "href", "/files");
});
});
});

View File

@@ -3,74 +3,74 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { setupApiIntercepts, loginWithFixtures } from '../support/intercept';
import { setupApiIntercepts, loginWithFixtures } from "../support/intercept";
describe('Login Flow', () => {
describe("Login Flow", () => {
beforeEach(() => {
setupApiIntercepts();
});
it('renders the login form', () => {
cy.visit('/login');
cy.get('h2').should('contain', 'LOGIN');
cy.get('#account').should('exist');
cy.get('#password').should('exist');
cy.get('#login_btn_main_btn').should('be.disabled');
it("renders the login form", () => {
cy.visit("/login");
cy.get("h2").should("contain", "LOGIN");
cy.get("#account").should("exist");
cy.get("#password").should("exist");
cy.get("#login_btn_main_btn").should("be.disabled");
});
it('login button is disabled when fields are empty', () => {
cy.visit('/login');
cy.get('#login_btn_main_btn').should('be.disabled');
it("login button is disabled when fields are empty", () => {
cy.visit("/login");
cy.get("#login_btn_main_btn").should("be.disabled");
// Only username filled — still disabled
cy.get('#account').type('testuser');
cy.get('#login_btn_main_btn').should('be.disabled');
cy.get("#account").type("testuser");
cy.get("#login_btn_main_btn").should("be.disabled");
});
it('login button enables when both fields are filled', () => {
cy.visit('/login');
cy.get('#account').type('testadmin');
cy.get('#password').type('password123');
cy.get('#login_btn_main_btn').should('not.be.disabled');
it("login button enables when both fields are filled", () => {
cy.visit("/login");
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("#login_btn_main_btn").should("not.be.disabled");
});
it('successful login redirects to /files', () => {
cy.visit('/login');
cy.get('#account').type('testadmin');
cy.get('#password').type('password123');
cy.get('#login_btn_main_btn').click();
it("successful login redirects to /files", () => {
cy.visit("/login");
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("#login_btn_main_btn").click();
cy.wait('@postToken');
cy.url().should('include', '/files');
cy.wait("@postToken");
cy.url().should("include", "/files");
});
it('failed login shows error message', () => {
it("failed login shows error message", () => {
// Override the token intercept to return 401
cy.intercept('POST', '/api/oauth/token', {
cy.intercept("POST", "/api/oauth/token", {
statusCode: 401,
body: { detail: 'Incorrect username or password' },
}).as('postTokenFail');
body: { detail: "Incorrect username or password" },
}).as("postTokenFail");
cy.visit('/login');
cy.get('#account').type('wronguser');
cy.get('#password').type('wrongpass');
cy.get('#login_btn_main_btn').click();
cy.visit("/login");
cy.get("#account").type("wronguser");
cy.get("#password").type("wrongpass");
cy.get("#login_btn_main_btn").click();
cy.wait('@postTokenFail');
cy.contains('Incorrect account or password').should('be.visible');
cy.wait("@postTokenFail");
cy.contains("Incorrect account or password").should("be.visible");
});
it('toggles password visibility', () => {
cy.visit('/login');
cy.get('#password').type('secret123');
cy.get('#password').should('have.attr', 'type', 'password');
it("toggles password visibility", () => {
cy.visit("/login");
cy.get("#password").type("secret123");
cy.get("#password").should("have.attr", "type", "password");
// Click the eye icon to show password
cy.get('label[for="passwordt"] span.cursor-pointer').click();
cy.get('#password').should('have.attr', 'type', 'text');
cy.get("#password").should("have.attr", "type", "text");
// Click again to hide
cy.get('label[for="passwordt"] span.cursor-pointer').click();
cy.get('#password').should('have.attr', 'type', 'password');
cy.get("#password").should("have.attr", "type", "password");
});
});

View File

@@ -3,65 +3,65 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Logout Flow', () => {
describe("Logout Flow", () => {
beforeEach(() => {
loginWithFixtures();
});
it('shows account menu when head icon is clicked', () => {
cy.visit('/files');
cy.wait('@getFiles');
it("shows account menu when head icon is clicked", () => {
cy.visit("/files");
cy.wait("@getFiles");
// Click the head icon to open account menu
cy.get('#acct_mgmt_button').click();
cy.get('#account_menu').should('be.visible');
cy.get('#greeting').should('contain', 'Test Admin');
cy.get("#acct_mgmt_button").click();
cy.get("#account_menu").should("be.visible");
cy.get("#greeting").should("contain", "Test Admin");
});
it('account menu shows admin management link for admin user', () => {
cy.visit('/files');
cy.wait('@getFiles');
it("account menu shows admin management link for admin user", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get('#acct_mgmt_button').click();
cy.get('#account_menu').should('be.visible');
cy.get("#acct_mgmt_button").click();
cy.get("#account_menu").should("be.visible");
// Admin user should see account management option
cy.get('#btn_acct_mgmt').should('exist');
cy.get("#btn_acct_mgmt").should("exist");
});
it('account menu has logout button', () => {
cy.visit('/files');
cy.wait('@getFiles');
it("account menu has logout button", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get('#acct_mgmt_button').click();
cy.get('#btn_logout_in_menu').should('exist');
cy.get("#acct_mgmt_button").click();
cy.get("#btn_logout_in_menu").should("exist");
});
it('clicking My Account navigates to /my-account', () => {
cy.visit('/files');
cy.wait('@getFiles');
it("clicking My Account navigates to /my-account", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get('#acct_mgmt_button').click();
cy.get('#btn_mang_ur_acct').click();
cy.url().should('include', '/my-account');
cy.get("#acct_mgmt_button").click();
cy.get("#btn_mang_ur_acct").click();
cy.url().should("include", "/my-account");
});
it('clicking Account Management navigates to /account-admin', () => {
cy.visit('/files');
cy.wait('@getFiles');
it("clicking Account Management navigates to /account-admin", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get('#acct_mgmt_button').click();
cy.get('#btn_acct_mgmt').click();
cy.url().should('include', '/account-admin');
cy.get("#acct_mgmt_button").click();
cy.get("#btn_acct_mgmt").click();
cy.url().should("include", "/account-admin");
});
it('logout redirects to login page', () => {
cy.visit('/files');
cy.wait('@getFiles');
it("logout redirects to login page", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get('#acct_mgmt_button').click();
cy.get('#btn_logout_in_menu').click();
cy.url().should('include', '/login');
cy.get("#acct_mgmt_button").click();
cy.get("#btn_logout_in_menu").click();
cy.url().should("include", "/login");
});
});

View File

@@ -3,71 +3,71 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('My Account Page', () => {
describe("My Account Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/my-account');
cy.wait('@getUserDetail');
cy.visit("/my-account");
cy.wait("@getUserDetail");
});
it('displays user name heading', () => {
cy.get('#general_acct_info_user_name').should('exist');
cy.get('#general_acct_info_user_name').should('contain', 'Test Admin');
it("displays user name heading", () => {
cy.get("#general_acct_info_user_name").should("exist");
cy.get("#general_acct_info_user_name").should("contain", "Test Admin");
});
it('shows Admin badge for admin user', () => {
cy.contains('Admin').should('exist');
it("shows Admin badge for admin user", () => {
cy.contains("Admin").should("exist");
});
it('shows visit count info', () => {
cy.get('#general_account_visit_info').should('exist');
cy.get('#general_account_visit_info').should('contain', 'Total visits');
it("shows visit count info", () => {
cy.get("#general_account_visit_info").should("exist");
cy.get("#general_account_visit_info").should("contain", "Total visits");
});
it('displays account username (read-only)', () => {
cy.contains('Test Admin').should('exist');
it("displays account username (read-only)", () => {
cy.contains("Test Admin").should("exist");
});
it('shows Edit button for name field', () => {
cy.contains('button', 'Edit').should('exist');
it("shows Edit button for name field", () => {
cy.contains("button", "Edit").should("exist");
});
it('clicking Edit shows input field and Save/Cancel buttons', () => {
cy.contains('button', 'Edit').first().click();
cy.get('#input_name_field').should('exist');
cy.contains('button', 'Save').should('exist');
cy.contains('button', 'Cancel').should('exist');
it("clicking Edit shows input field and Save/Cancel buttons", () => {
cy.contains("button", "Edit").first().click();
cy.get("#input_name_field").should("exist");
cy.contains("button", "Save").should("exist");
cy.contains("button", "Cancel").should("exist");
});
it('clicking Cancel reverts name field to read-only', () => {
cy.contains('button', 'Edit').first().click();
cy.get('#input_name_field').should('exist');
cy.contains('button', 'Cancel').click();
cy.get('#input_name_field').should('not.exist');
it("clicking Cancel reverts name field to read-only", () => {
cy.contains("button", "Edit").first().click();
cy.get("#input_name_field").should("exist");
cy.contains("button", "Cancel").click();
cy.get("#input_name_field").should("not.exist");
});
it('shows Reset button for password field', () => {
cy.contains('button', 'Reset').should('exist');
it("shows Reset button for password field", () => {
cy.contains("button", "Reset").should("exist");
});
it('clicking Reset shows password input and Save/Cancel', () => {
cy.contains('button', 'Reset').click();
cy.get('input[type="password"]').should('exist');
cy.contains('button', 'Save').should('exist');
cy.contains('button', 'Cancel').should('exist');
it("clicking Reset shows password input and Save/Cancel", () => {
cy.contains("button", "Reset").click();
cy.get('input[type="password"]').should("exist");
cy.contains("button", "Save").should("exist");
cy.contains("button", "Cancel").should("exist");
});
it('clicking Cancel on password field hides the input', () => {
cy.contains('button', 'Reset').click();
cy.get('input[type="password"]').should('exist');
it("clicking Cancel on password field hides the input", () => {
cy.contains("button", "Reset").click();
cy.get('input[type="password"]').should("exist");
// The Cancel button for password is the second one
cy.get('.cancel-btn').click();
cy.get('input[type="password"]').should('not.exist');
cy.get(".cancel-btn").click();
cy.get('input[type="password"]').should("not.exist");
});
it('shows Session section', () => {
cy.contains('Session').should('exist');
it("shows Session section", () => {
cy.contains("Session").should("exist");
});
});

View File

@@ -3,60 +3,60 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures, setupApiIntercepts } from '../support/intercept';
import { loginWithFixtures, setupApiIntercepts } from "../support/intercept";
describe('Navigation and Routing', () => {
it('redirects / to /files when logged in', () => {
describe("Navigation and Routing", () => {
it("redirects / to /files when logged in", () => {
loginWithFixtures();
cy.visit('/');
cy.url().should('include', '/files');
cy.visit("/");
cy.url().should("include", "/files");
});
it('shows 404 page for unknown routes', () => {
it("shows 404 page for unknown routes", () => {
loginWithFixtures();
cy.visit('/nonexistent-page');
cy.contains('404').should('exist');
cy.visit("/nonexistent-page");
cy.contains("404").should("exist");
});
it('navbar shows correct view name', () => {
it("navbar shows correct view name", () => {
loginWithFixtures();
cy.visit('/files');
cy.wait('@getFiles');
cy.get('#nav_bar').should('exist');
cy.get('#nav_bar h2').should('contain', 'FILES');
cy.visit("/files");
cy.wait("@getFiles");
cy.get("#nav_bar").should("exist");
cy.get("#nav_bar h2").should("contain", "FILES");
});
it('navbar shows back arrow on non-files pages', () => {
it("navbar shows back arrow on non-files pages", () => {
loginWithFixtures();
cy.visit('/discover/log/1/map');
cy.visit("/discover/log/1/map");
// Back arrow should be visible on discover pages
cy.get('#backPage').should('exist');
cy.get("#backPage").should("exist");
});
it('navbar tabs are clickable on discover page', () => {
it("navbar tabs are clickable on discover page", () => {
loginWithFixtures();
cy.visit('/discover/log/1/map');
cy.visit("/discover/log/1/map");
// Discover navbar should show MAP, CONFORMANCE, PERFORMANCE tabs
cy.contains('.nav-item', 'MAP').should('exist');
cy.contains('.nav-item', 'CONFORMANCE').should('exist');
cy.contains('.nav-item', 'PERFORMANCE').should('exist');
cy.contains(".nav-item", "MAP").should("exist");
cy.contains(".nav-item", "CONFORMANCE").should("exist");
cy.contains(".nav-item", "PERFORMANCE").should("exist");
// Click CONFORMANCE tab
cy.contains('.nav-item', 'CONFORMANCE').click();
cy.url().should('include', '/conformance');
cy.contains(".nav-item", "CONFORMANCE").click();
cy.url().should("include", "/conformance");
// Click PERFORMANCE tab
cy.contains('.nav-item', 'PERFORMANCE').click();
cy.url().should('include', '/performance');
cy.contains(".nav-item", "PERFORMANCE").click();
cy.url().should("include", "/performance");
// Click MAP tab to go back
cy.contains('.nav-item', 'MAP').click();
cy.url().should('include', '/map');
cy.contains(".nav-item", "MAP").click();
cy.url().should("include", "/map");
});
it('login page is accessible at /login', () => {
it("login page is accessible at /login", () => {
setupApiIntercepts();
cy.visit('/login');
cy.get('h2').should('contain', 'LOGIN');
cy.visit("/login");
cy.get("h2").should("contain", "LOGIN");
});
});

View File

@@ -3,36 +3,38 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('404 Not Found Page', () => {
describe("404 Not Found Page", () => {
beforeEach(() => {
// Suppress Navbar error on 404 page (route.matched[1] is null)
cy.on('uncaught:exception', () => false);
cy.on("uncaught:exception", () => false);
});
it('displays 404 page for non-existent route', () => {
it("displays 404 page for non-existent route", () => {
loginWithFixtures();
cy.visit('/this-page-does-not-exist');
cy.contains('404').should('be.visible');
cy.contains('The page you are looking for does not exist.').should('be.visible');
cy.visit("/this-page-does-not-exist");
cy.contains("404").should("be.visible");
cy.contains("The page you are looking for does not exist.").should(
"be.visible",
);
});
it('has a link back to Files page', () => {
it("has a link back to Files page", () => {
loginWithFixtures();
cy.visit('/some/random/path');
cy.contains('a', 'Go to Files')
.should('be.visible')
.should('have.attr', 'href', '/files');
cy.visit("/some/random/path");
cy.contains("a", "Go to Files")
.should("be.visible")
.should("have.attr", "href", "/files");
});
it('displays 404 for unauthenticated user on invalid route', () => {
cy.visit('/not-a-real-page');
it("displays 404 for unauthenticated user on invalid route", () => {
cy.visit("/not-a-real-page");
cy.url().then((url) => {
if (url.includes('/login')) {
cy.url().should('include', '/login');
if (url.includes("/login")) {
cy.url().should("include", "/login");
} else {
cy.contains('404').should('be.visible');
cy.contains("404").should("be.visible");
}
});
});

View File

@@ -4,35 +4,35 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/06/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('Discover page navigation tabs', () => {
describe("Discover page navigation tabs", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/files');
cy.wait('@getFiles');
cy.visit("/files");
cy.wait("@getFiles");
});
it('Double-clicking a log file enters the MAP page.', () => {
cy.contains('td.fileName', 'sample-process.xes').dblclick();
cy.url().should('include', 'map');
it("Double-clicking a log file enters the MAP page.", () => {
cy.contains("td.fileName", "sample-process.xes").dblclick();
cy.url().should("include", "map");
// MAP tab should exist in the navbar
cy.contains('.nav-item', 'MAP').should('exist');
cy.contains(".nav-item", "MAP").should("exist");
});
it('Clicking CONFORMANCE tab switches active page.', () => {
cy.contains('td.fileName', 'sample-process.xes').dblclick();
cy.url().should('include', 'map');
cy.contains('.nav-item', 'CONFORMANCE').click();
cy.url().should('include', 'conformance');
cy.contains('.nav-item', 'CONFORMANCE').should('have.class', 'active');
it("Clicking CONFORMANCE tab switches active page.", () => {
cy.contains("td.fileName", "sample-process.xes").dblclick();
cy.url().should("include", "map");
cy.contains(".nav-item", "CONFORMANCE").click();
cy.url().should("include", "conformance");
cy.contains(".nav-item", "CONFORMANCE").should("have.class", "active");
});
it('Clicking PERFORMANCE tab switches active page.', () => {
cy.contains('td.fileName', 'sample-process.xes').dblclick();
cy.url().should('include', 'map');
cy.contains('.nav-item', 'PERFORMANCE').click();
cy.url().should('include', 'performance');
cy.contains('.nav-item', 'PERFORMANCE').should('have.class', 'active');
it("Clicking PERFORMANCE tab switches active page.", () => {
cy.contains("td.fileName", "sample-process.xes").dblclick();
cy.url().should("include", "map");
cy.contains(".nav-item", "PERFORMANCE").click();
cy.url().should("include", "performance");
cy.contains(".nav-item", "PERFORMANCE").should("have.class", "active");
});
});

View File

@@ -4,54 +4,55 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/06/11
// imacat.yang@dsp.im (imacat), 2026/03/05
import { setupApiIntercepts } from '../support/intercept';
import { setupApiIntercepts } from "../support/intercept";
describe('Paste URL login redirect', () => {
it('After login with return-to param, redirects to the remembered page', () => {
describe("Paste URL login redirect", () => {
it("After login with return-to param, redirects to the remembered page", () => {
setupApiIntercepts();
// Visit login page with a return-to query param (base64-encoded URL)
const targetUrl = 'http://localhost:4173/discover/conformance/log/1/conformance';
const targetUrl =
"http://localhost:4173/discover/conformance/log/1/conformance";
const encodedUrl = btoa(targetUrl);
cy.visit(`/login?return-to=${encodedUrl}`);
// Fill in login form
cy.get('#account').type('testadmin');
cy.get('#password').type('password123');
cy.get('form').submit();
cy.wait('@postToken');
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("form").submit();
cy.wait("@postToken");
// After login, the app should attempt to redirect to the return-to URL.
// Since window.location.href is used (not router.push), we verify the
// login form disappears and the token cookie is set.
cy.getCookie('luciaToken').should('exist');
cy.getCookie("luciaToken").should("exist");
});
it('Login without return-to param redirects to /files', () => {
it("Login without return-to param redirects to /files", () => {
setupApiIntercepts();
cy.visit('/login');
cy.visit("/login");
cy.get('#account').type('testadmin');
cy.get('#password').type('password123');
cy.get('form').submit();
cy.wait('@postToken');
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("form").submit();
cy.wait("@postToken");
cy.url().should('include', '/files');
cy.url().should("include", "/files");
});
it('Unauthenticated user cannot access inner pages', () => {
it("Unauthenticated user cannot access inner pages", () => {
setupApiIntercepts();
// Override my-account to return 401 (simulate logged-out state)
cy.intercept('GET', '/api/my-account', {
cy.intercept("GET", "/api/my-account", {
statusCode: 401,
body: { detail: 'Not authenticated' },
}).as('getMyAccountUnauth');
body: { detail: "Not authenticated" },
}).as("getMyAccountUnauth");
cy.visit('/files');
cy.visit("/files");
// Should be redirected to login page
cy.url().should('include', '/login');
cy.get('#account').should('exist');
cy.get('#password').should('exist');
cy.url().should("include", "/login");
cy.get("#account").should("exist");
cy.get("#password").should("exist");
});
});

View File

@@ -3,113 +3,113 @@
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from '../support/intercept';
import { loginWithFixtures } from "../support/intercept";
describe('SweetAlert2 Modals', () => {
describe('File Context Menu - Rename', () => {
describe("SweetAlert2 Modals", () => {
describe("File Context Menu - Rename", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/files');
cy.wait('@getFiles');
cy.visit("/files");
cy.wait("@getFiles");
});
it('right-click on table row shows context menu with Rename', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Rename').should('be.visible');
it("right-click on table row shows context menu with Rename", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").should("be.visible");
});
it('right-click context menu shows Download option', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Download').should('be.visible');
it("right-click context menu shows Download option", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Download").should("be.visible");
});
it('right-click context menu shows Delete option', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Delete').should('be.visible');
it("right-click context menu shows Delete option", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").should("be.visible");
});
it('clicking Rename opens SweetAlert rename dialog', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Rename').click();
it("clicking Rename opens SweetAlert rename dialog", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").click();
// SweetAlert popup should appear with RENAME title
cy.get('.swal2-popup').should('be.visible');
cy.get('.swal2-title').should('contain', 'RENAME');
cy.get('.swal2-input').should('exist');
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-title").should("contain", "RENAME");
cy.get(".swal2-input").should("exist");
});
it('rename dialog has pre-filled file name', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Rename').click();
cy.get('.swal2-input').should('not.have.value', '');
it("rename dialog has pre-filled file name", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").click();
cy.get(".swal2-input").should("not.have.value", "");
});
it('rename dialog can be cancelled', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Rename').click();
cy.get('.swal2-popup').should('be.visible');
cy.get('.swal2-cancel').click();
cy.get('.swal2-popup').should('not.exist');
it("rename dialog can be cancelled", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-cancel").click();
cy.get(".swal2-popup").should("not.exist");
});
it('clicking Delete opens SweetAlert delete confirmation', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Delete').click();
it("clicking Delete opens SweetAlert delete confirmation", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").click();
// SweetAlert popup should appear with CONFIRM DELETION
cy.get('.swal2-popup').should('be.visible');
cy.get('.swal2-title').should('contain', 'CONFIRM DELETION');
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-title").should("contain", "CONFIRM DELETION");
});
it('delete confirmation shows file name', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Delete').click();
cy.get('.swal2-popup').should('be.visible');
cy.get('.swal2-html-container').should('contain', 'delete');
it("delete confirmation shows file name", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-html-container").should("contain", "delete");
});
it('delete confirmation can be cancelled', () => {
cy.get('table tbody tr').first().rightclick();
cy.contains('Delete').click();
cy.get('.swal2-popup').should('be.visible');
cy.get('.swal2-cancel').click();
cy.get('.swal2-popup').should('not.exist');
it("delete confirmation can be cancelled", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-cancel").click();
cy.get(".swal2-popup").should("not.exist");
});
});
describe('File Context Menu on Grid View', () => {
describe("File Context Menu on Grid View", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/files');
cy.wait('@getFiles');
cy.visit("/files");
cy.wait("@getFiles");
// Switch to grid view
cy.get('svg').parent('li.cursor-pointer').last().click();
cy.get("svg").parent("li.cursor-pointer").last().click();
});
it('right-click on grid card shows context menu', () => {
cy.get('li[title]').first().rightclick();
cy.contains('Rename').should('be.visible');
cy.contains('Delete').should('be.visible');
it("right-click on grid card shows context menu", () => {
cy.get("li[title]").first().rightclick();
cy.contains("Rename").should("be.visible");
cy.contains("Delete").should("be.visible");
});
it('grid card rename opens SweetAlert dialog', () => {
cy.get('li[title]').first().rightclick();
cy.contains('Rename').click();
cy.get('.swal2-popup').should('be.visible');
cy.get('.swal2-title').should('contain', 'RENAME');
it("grid card rename opens SweetAlert dialog", () => {
cy.get("li[title]").first().rightclick();
cy.contains("Rename").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-title").should("contain", "RENAME");
});
});
describe('Account Delete Confirmation', () => {
describe("Account Delete Confirmation", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit('/account-admin');
cy.wait('@getUsers');
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it('delete confirmation Yes button triggers delete API', () => {
cy.get('.delete-account').first().click();
cy.get('#modal_container').should('be.visible');
cy.get('#sure_to_delete_acct_btn').click();
cy.wait('@deleteUser');
it("delete confirmation Yes button triggers delete API", () => {
cy.get(".delete-account").first().click();
cy.get("#modal_container").should("be.visible");
cy.get("#sure_to_delete_acct_btn").click();
cy.wait("@deleteUser");
// Modal should close after deletion
});
});

View File

@@ -14,16 +14,16 @@
// https://on.cypress.io/custom-commands
// ***********************************************
// -- This is a parent command --
import '@4tw/cypress-drag-drop'
import "@4tw/cypress-drag-drop";
/**
* Sets authentication cookies to simulate a logged-in user.
*
* @returns {void}
*/
Cypress.Commands.add('login', () => {
cy.setCookie('luciaToken', 'fake-access-token-for-testing');
cy.setCookie('isLuciaLoggedIn', 'true');
Cypress.Commands.add("login", () => {
cy.setCookie("luciaToken", "fake-access-token-for-testing");
cy.setCookie("isLuciaLoggedIn", "true");
});
// Usage: cy.login()
// -- This is a child command --
@@ -33,8 +33,8 @@ Cypress.Commands.add('login', () => {
*
* @returns {void}
*/
Cypress.Commands.add('closePopup', () => {
Cypress.Commands.add("closePopup", () => {
// Trigger a forced click to close modal overlays consistently.
cy.get('body').click({ position: 'topLeft'});
cy.get("body").click({ position: "topLeft" });
cy.wait(1000);
})
});

View File

@@ -20,11 +20,11 @@
// Import commands.js using ES2015 syntax:
import "./commands";
Cypress.on('uncaught:exception', (err, runnable) => {
Cypress.on("uncaught:exception", (err, runnable) => {
// returning false here prevents Cypress from failing the test
return false
})
return false;
});
// Alternatively you can use CommonJS syntax:
// require('./commands')
require('cypress-xpath') // Enables xpath helpers used in pointer event checks.
require("cypress-xpath"); // Enables xpath helpers used in pointer event checks.

View File

@@ -9,174 +9,174 @@
*/
export function setupApiIntercepts() {
// Auth
cy.intercept('POST', '/api/oauth/token', {
fixture: 'api/token.json',
}).as('postToken');
cy.intercept("POST", "/api/oauth/token", {
fixture: "api/token.json",
}).as("postToken");
// User account
cy.intercept('GET', '/api/my-account', {
fixture: 'api/my-account.json',
}).as('getMyAccount');
cy.intercept("GET", "/api/my-account", {
fixture: "api/my-account.json",
}).as("getMyAccount");
cy.intercept('PUT', '/api/my-account', {
cy.intercept("PUT", "/api/my-account", {
statusCode: 200,
body: { success: true },
}).as('putMyAccount');
}).as("putMyAccount");
// Files
cy.intercept('GET', '/api/files', {
fixture: 'api/files.json',
}).as('getFiles');
cy.intercept("GET", "/api/files", {
fixture: "api/files.json",
}).as("getFiles");
// Users (account management)
cy.intercept('GET', '/api/users', {
fixture: 'api/users.json',
}).as('getUsers');
cy.intercept("GET", "/api/users", {
fixture: "api/users.json",
}).as("getUsers");
cy.intercept('POST', '/api/users', {
cy.intercept("POST", "/api/users", {
statusCode: 200,
body: { success: true },
}).as('postUser');
}).as("postUser");
cy.intercept('DELETE', '/api/users/*', {
cy.intercept("DELETE", "/api/users/*", {
statusCode: 200,
body: { success: true },
}).as('deleteUser');
}).as("deleteUser");
cy.intercept('PUT', '/api/users/*', {
cy.intercept("PUT", "/api/users/*", {
statusCode: 200,
body: { success: true },
}).as('putUser');
}).as("putUser");
// User detail (GET /api/users/:username)
cy.intercept('GET', '/api/users/*', {
fixture: 'api/user-detail.json',
}).as('getUserDetail');
cy.intercept("GET", "/api/users/*", {
fixture: "api/user-detail.json",
}).as("getUserDetail");
// User roles
cy.intercept('PUT', '/api/users/*/roles/*', {
cy.intercept("PUT", "/api/users/*/roles/*", {
statusCode: 200,
body: { success: true },
}).as('putUserRole');
}).as("putUserRole");
cy.intercept('DELETE', '/api/users/*/roles/*', {
cy.intercept("DELETE", "/api/users/*/roles/*", {
statusCode: 200,
body: { success: true },
}).as('deleteUserRole');
}).as("deleteUserRole");
// Filter detail (for fetchFunnel when entering filter from Files)
cy.intercept('GET', /\/api\/filters\/\d+$/, {
cy.intercept("GET", /\/api\/filters\/\d+$/, {
statusCode: 200,
body: { rules: [], log: { id: 1 }, name: 'filtered-sample' },
}).as('getFilterDetail');
body: { rules: [], log: { id: 1 }, name: "filtered-sample" },
}).as("getFilterDetail");
// Discover (map data)
cy.intercept('GET', '/api/logs/*/discover', {
fixture: 'api/discover.json',
}).as('getDiscover');
cy.intercept("GET", "/api/logs/*/discover", {
fixture: "api/discover.json",
}).as("getDiscover");
cy.intercept('GET', '/api/filters/*/discover', {
fixture: 'api/discover.json',
}).as('getFilterDiscover');
cy.intercept("GET", "/api/filters/*/discover", {
fixture: "api/discover.json",
}).as("getFilterDiscover");
// Performance
cy.intercept('GET', '/api/logs/*/performance', {
fixture: 'api/performance.json',
}).as('getPerformance');
cy.intercept("GET", "/api/logs/*/performance", {
fixture: "api/performance.json",
}).as("getPerformance");
cy.intercept('GET', '/api/filters/*/performance', {
fixture: 'api/performance.json',
}).as('getFilterPerformance');
cy.intercept("GET", "/api/filters/*/performance", {
fixture: "api/performance.json",
}).as("getFilterPerformance");
// Traces
cy.intercept('GET', '/api/logs/*/traces', {
fixture: 'api/traces.json',
}).as('getTraces');
cy.intercept("GET", "/api/logs/*/traces", {
fixture: "api/traces.json",
}).as("getTraces");
cy.intercept('GET', '/api/filters/*/traces', {
fixture: 'api/traces.json',
}).as('getFilterTraces');
cy.intercept("GET", "/api/filters/*/traces", {
fixture: "api/traces.json",
}).as("getFilterTraces");
// Trace detail (must be after traces list intercepts)
cy.intercept('GET', /\/api\/logs\/.*\/traces\/\d+/, {
fixture: 'api/trace-detail.json',
}).as('getTraceDetail');
cy.intercept("GET", /\/api\/logs\/.*\/traces\/\d+/, {
fixture: "api/trace-detail.json",
}).as("getTraceDetail");
cy.intercept('GET', /\/api\/filters\/.*\/traces\/\d+/, {
fixture: 'api/trace-detail.json',
}).as('getFilterTraceDetail');
cy.intercept("GET", /\/api\/filters\/.*\/traces\/\d+/, {
fixture: "api/trace-detail.json",
}).as("getFilterTraceDetail");
// Temp filters
cy.intercept('GET', '/api/temp-filters/*/discover', {
fixture: 'api/discover.json',
}).as('getTempFilterDiscover');
cy.intercept("GET", "/api/temp-filters/*/discover", {
fixture: "api/discover.json",
}).as("getTempFilterDiscover");
cy.intercept('GET', '/api/temp-filters/*/traces', {
fixture: 'api/traces.json',
}).as('getTempFilterTraces');
cy.intercept("GET", "/api/temp-filters/*/traces", {
fixture: "api/traces.json",
}).as("getTempFilterTraces");
// Filter params
cy.intercept('GET', '/api/filters/params*', {
cy.intercept("GET", "/api/filters/params*", {
statusCode: 200,
body: {},
}).as('getFilterParams');
}).as("getFilterParams");
cy.intercept('GET', '/api/filters/has-result*', {
cy.intercept("GET", "/api/filters/has-result*", {
statusCode: 200,
body: false,
}).as('getFilterHasResult');
}).as("getFilterHasResult");
// Conformance check params
cy.intercept('GET', '/api/log-checks/params*', {
fixture: 'api/filter-params.json',
}).as('getLogCheckParams');
cy.intercept("GET", "/api/log-checks/params*", {
fixture: "api/filter-params.json",
}).as("getLogCheckParams");
cy.intercept('GET', '/api/filter-checks/params*', {
fixture: 'api/filter-params.json',
}).as('getFilterCheckParams');
cy.intercept("GET", "/api/filter-checks/params*", {
fixture: "api/filter-params.json",
}).as("getFilterCheckParams");
// Compare dashboard
cy.intercept('GET', /\/api\/compare\?datasets=/, {
fixture: 'api/compare.json',
}).as('getCompare');
cy.intercept("GET", /\/api\/compare\?datasets=/, {
fixture: "api/compare.json",
}).as("getCompare");
// Dependents (for delete confirmation)
cy.intercept('GET', '/api/logs/*/dependents', {
cy.intercept("GET", "/api/logs/*/dependents", {
statusCode: 200,
body: [],
}).as('getLogDependents');
}).as("getLogDependents");
cy.intercept('GET', '/api/filters/*/dependents', {
cy.intercept("GET", "/api/filters/*/dependents", {
statusCode: 200,
body: [],
}).as('getFilterDependents');
}).as("getFilterDependents");
cy.intercept('GET', '/api/log-checks/*/dependents', {
cy.intercept("GET", "/api/log-checks/*/dependents", {
statusCode: 200,
body: [],
}).as('getLogCheckDependents');
}).as("getLogCheckDependents");
cy.intercept('GET', '/api/filter-checks/*/dependents', {
cy.intercept("GET", "/api/filter-checks/*/dependents", {
statusCode: 200,
body: [],
}).as('getFilterCheckDependents');
}).as("getFilterCheckDependents");
// Rename
cy.intercept('PUT', '/api/logs/*/rename', {
cy.intercept("PUT", "/api/logs/*/rename", {
statusCode: 200,
body: { success: true },
}).as('renameLog');
}).as("renameLog");
cy.intercept('PUT', '/api/filters/*/rename', {
cy.intercept("PUT", "/api/filters/*/rename", {
statusCode: 200,
body: { success: true },
}).as('renameFilter');
}).as("renameFilter");
// Deletion
cy.intercept('DELETE', '/api/deletion/*', {
cy.intercept("DELETE", "/api/deletion/*", {
statusCode: 200,
body: { success: true },
}).as('deleteDeletion');
}).as("deleteDeletion");
}
/**
@@ -185,6 +185,6 @@ export function setupApiIntercepts() {
*/
export function loginWithFixtures() {
setupApiIntercepts();
cy.setCookie('luciaToken', 'fake-access-token-for-testing');
cy.setCookie('isLuciaLoggedIn', 'true');
cy.setCookie("luciaToken", "fake-access-token-for-testing");
cy.setCookie("isLuciaLoggedIn", "true");
}

View File

@@ -4,8 +4,12 @@
// imacat.yang@dsp.im (imacat), 2023/9/23
/** @module auth Authentication token refresh utilities. */
import axios from 'axios';
import { getCookie, setCookie, setCookieWithoutExpiration } from '@/utils/cookieUtil.js';
import axios from "axios";
import {
getCookie,
setCookie,
setCookieWithoutExpiration,
} from "@/utils/cookieUtil.js";
/**
* Refreshes the access token using the stored refresh token cookie.
@@ -18,27 +22,29 @@ import { getCookie, setCookie, setCookieWithoutExpiration } from '@/utils/cookie
* @throws {Error} If the refresh request fails.
*/
export async function refreshTokenAndGetNew() {
const api = '/api/oauth/token';
const api = "/api/oauth/token";
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
"Content-Type": "application/x-www-form-urlencoded",
},
};
const data = {
grant_type: 'refresh_token',
refresh_token: getCookie('luciaRefreshToken'),
grant_type: "refresh_token",
refresh_token: getCookie("luciaRefreshToken"),
};
const response = await axios.post(api, data, config);
const newAccessToken = response.data.access_token;
const newRefreshToken = response.data.refresh_token;
setCookieWithoutExpiration('luciaToken', newAccessToken);
setCookieWithoutExpiration("luciaToken", newAccessToken);
// Expire in ~6 months
const expiredMs = new Date();
expiredMs.setMonth(expiredMs.getMonth() + 6);
const days = Math.ceil((expiredMs.getTime() - Date.now()) / (24 * 60 * 60 * 1000));
setCookie('luciaRefreshToken', newRefreshToken, days);
const days = Math.ceil(
(expiredMs.getTime() - Date.now()) / (24 * 60 * 60 * 1000),
);
setCookie("luciaRefreshToken", newRefreshToken, days);
return newAccessToken;
}

View File

@@ -8,15 +8,15 @@
* 401 token refresh with request queuing.
*/
import axios from 'axios';
import { getCookie, deleteCookie } from '@/utils/cookieUtil.js';
import axios from "axios";
import { getCookie, deleteCookie } from "@/utils/cookieUtil.js";
/** Axios instance configured with auth interceptors. */
const apiClient = axios.create();
// Request interceptor: automatically attach Authorization header
apiClient.interceptors.request.use((config) => {
const token = getCookie('luciaToken');
const token = getCookie("luciaToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
@@ -54,7 +54,7 @@ apiClient.interceptors.response.use(
if (
error.response?.status !== 401 ||
originalRequest._retried ||
originalRequest.url === '/api/oauth/token'
originalRequest.url === "/api/oauth/token"
) {
return Promise.reject(error);
}
@@ -76,7 +76,7 @@ apiClient.interceptors.response.use(
try {
// Dynamic import to avoid circular dependency with login store
const { refreshTokenAndGetNew } = await import('@/api/auth.js');
const { refreshTokenAndGetNew } = await import("@/api/auth.js");
const newToken = await refreshTokenAndGetNew();
isRefreshing = false;
onRefreshSuccess(newToken);
@@ -87,11 +87,11 @@ apiClient.interceptors.response.use(
onRefreshFailure(refreshError);
// Refresh failed: clear auth and redirect to login
deleteCookie('luciaToken');
window.location.href = '/login';
deleteCookie("luciaToken");
window.location.href = "/login";
return Promise.reject(refreshError);
}
}
},
);
export default apiClient;

View File

@@ -1,32 +1,51 @@
<template>
<div id="account_menu" v-if="isAcctMenuOpen" class="absolute top-0 w-[232px] bg-white right-[0px] rounded shadow-lg bg-[#ffffff]">
<div id="greeting" class="w-full border-b border-[#CBD5E1]">
<span class="m-4 h-[48px]">
{{ i18next.t("AcctMgmt.hi") }}{{ userData.name }}
</span>
</div>
<ul class="w-full min-h-10">
<!-- Not using a loop here because SVGs won't display if src is a variable -->
<li v-if="isAdmin" id="btn_acct_mgmt"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onBtnAcctMgmtClick">
<span class="w-[24px] h-[24px] flex"><img src="@/assets/icon-crown.svg" alt="accountManagement"></span>
<span class="flex ml-[8px]">{{i18next.t("AcctMgmt.acctMgmt")}}</span>
</li>
<li id="btn_mang_ur_acct"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onBtnMyAccountClick">
<span class="w-[24px] h-[24px] flex"><img src="@/assets/icon-head-black.svg" alt="head-black"></span>
<span class="flex ml-[8px]">{{i18next.t("AcctMgmt.mangUrAcct")}}</span>
</li>
<li id="btn_logout_in_menu"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onLogoutBtnClick">
<span class="w-[24px] h-[24px] flex"><img src="@/assets/icon-logout.svg" alt="logout"></span>
<span class="flex ml-[8px]">{{i18next.t("AcctMgmt.Logout")}}</span>
</li>
</ul>
<div
id="account_menu"
v-if="isAcctMenuOpen"
class="absolute top-0 w-[232px] bg-white right-[0px] rounded shadow-lg bg-[#ffffff]"
>
<div id="greeting" class="w-full border-b border-[#CBD5E1]">
<span class="m-4 h-[48px]">
{{ i18next.t("AcctMgmt.hi") }}{{ userData.name }}
</span>
</div>
<ul class="w-full min-h-10">
<!-- Not using a loop here because SVGs won't display if src is a variable -->
<li
v-if="isAdmin"
id="btn_acct_mgmt"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer items-center"
@click="onBtnAcctMgmtClick"
>
<span class="w-[24px] h-[24px] flex"
><img src="@/assets/icon-crown.svg" alt="accountManagement"
/></span>
<span class="flex ml-[8px]">{{ i18next.t("AcctMgmt.acctMgmt") }}</span>
</li>
<li
id="btn_mang_ur_acct"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer items-center"
@click="onBtnMyAccountClick"
>
<span class="w-[24px] h-[24px] flex"
><img src="@/assets/icon-head-black.svg" alt="head-black"
/></span>
<span class="flex ml-[8px]">{{
i18next.t("AcctMgmt.mangUrAcct")
}}</span>
</li>
<li
id="btn_logout_in_menu"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer items-center"
@click="onLogoutBtnClick"
>
<span class="w-[24px] h-[24px] flex"
><img src="@/assets/icon-logout.svg" alt="logout"
/></span>
<span class="flex ml-[8px]">{{ i18next.t("AcctMgmt.Logout") }}</span>
</li>
</ul>
</div>
</template>
<script setup>
@@ -40,16 +59,16 @@
* with links to account management, my account, and logout.
*/
import { computed, onMounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import i18next from '@/i18n/i18n';
import { useRouter, useRoute } from 'vue-router';
import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import emitter from '@/utils/emitter';
import { computed, onMounted, ref } from "vue";
import { storeToRefs } from "pinia";
import i18next from "@/i18n/i18n";
import { useRouter, useRoute } from "vue-router";
import { useLoginStore } from "@/stores/login";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { leaveFilter, leaveConformance } from "@/module/alertModal.js";
import emitter from "@/utils/emitter";
const router = useRouter();
const route = useRoute();
@@ -60,12 +79,15 @@ const acctMgmtStore = useAcctMgmtStore();
const { logOut } = loginStore;
const { tempFilterId } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } =
storeToRefs(conformanceStore);
const { userData } = storeToRefs(loginStore);
const { isAcctMenuOpen } = storeToRefs(acctMgmtStore);
const loginUserData = ref(null);
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail);
const currentViewingUserDetail = computed(
() => acctMgmtStore.currentViewingUser.detail,
);
const isAdmin = ref(false);
/** Fetches user data and determines if the current user is an admin. */
@@ -81,16 +103,21 @@ const onBtnMyAccountClick = async () => {
acctMgmtStore.closeAcctMenu();
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
await router.push('/my-account');
await router.push("/my-account");
};
/** Registers a click listener to close the menu when clicking outside. */
const clickOtherPlacesThenCloseMenu = () => {
const acctMgmtButton = document.getElementById('acct_mgmt_button');
const acctMgmtMenu = document.getElementById('account_menu');
const acctMgmtButton = document.getElementById("acct_mgmt_button");
const acctMgmtMenu = document.getElementById("account_menu");
document.addEventListener('click', (event) => {
if (acctMgmtMenu && acctMgmtButton && !acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) {
document.addEventListener("click", (event) => {
if (
acctMgmtMenu &&
acctMgmtButton &&
!acctMgmtMenu.contains(event.target) &&
!acctMgmtButton.contains(event.target)
) {
acctMgmtStore.closeAcctMenu();
}
});
@@ -98,19 +125,29 @@ const clickOtherPlacesThenCloseMenu = () => {
/** Navigates to the Account Admin page. */
const onBtnAcctMgmtClick = () => {
router.push({name: 'AcctAdmin'});
router.push({ name: "AcctAdmin" });
acctMgmtStore.closeAcctMenu();
};
/** Handles logout with unsaved-changes confirmation for Map and Conformance pages. */
const onLogoutBtnClick = () => {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
if (
(route.name === "Map" || route.name === "CheckMap") &&
tempFilterId.value
) {
// Notify Map to close the Sidebar.
emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
emitter.emit("leaveFilter", false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut);
} else if (
(route.name === "Conformance" || route.name === "CheckConformance") &&
(conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)
) {
leaveConformance(
false,
conformanceStore.addConformanceCreateCheckId,
false,
logOut,
);
} else {
logOut();
}

View File

@@ -1,12 +1,22 @@
<template>
<div id="search_bar_container" class="flex w-[280px] h-8 px-4 items-center border-[#64748B] border-[1px] rounded-full
justify-between">
<input id="input_search" class="w-full outline-0" :placeholder="i18next.t('AcctMgmt.Search')"
v-model="inputQuery" @keypress="handleKeyPressOfSearch"
/>
<img src="@/assets/icon-search.svg" class="w-[17px] h-[17px] flex cursor-pointer" @click="onSearchClick"
alt="search"/>
</div>
<div
id="search_bar_container"
class="flex w-[280px] h-8 px-4 items-center border-[#64748B] border-[1px] rounded-full justify-between"
>
<input
id="input_search"
class="w-full outline-0"
:placeholder="i18next.t('AcctMgmt.Search')"
v-model="inputQuery"
@keypress="handleKeyPressOfSearch"
/>
<img
src="@/assets/icon-search.svg"
class="w-[17px] h-[17px] flex cursor-pointer"
@click="onSearchClick"
alt="search"
/>
</div>
</template>
<script setup>
@@ -20,10 +30,10 @@
* filtering accounts, emits search query on click or Enter key.
*/
import { ref } from 'vue';
import i18next from '@/i18n/i18n.js';
import { ref } from "vue";
import i18next from "@/i18n/i18n.js";
const emit = defineEmits(['on-search-account-button-click']);
const emit = defineEmits(["on-search-account-button-click"]);
const inputQuery = ref("");
@@ -32,8 +42,8 @@ const inputQuery = ref("");
* @param {Event} event - The click event.
*/
const onSearchClick = (event) => {
event.preventDefault();
emit('on-search-account-button-click', inputQuery.value);
event.preventDefault();
emit("on-search-account-button-click", inputQuery.value);
};
/**
@@ -41,8 +51,8 @@ const onSearchClick = (event) => {
* @param {KeyboardEvent} event - The keypress event.
*/
const handleKeyPressOfSearch = (event) => {
if (event.key === 'Enter') {
emit('on-search-account-button-click', inputQuery.value);
}
if (event.key === "Enter") {
emit("on-search-account-button-click", inputQuery.value);
}
};
</script>

View File

@@ -1,18 +1,18 @@
<template>
<div
class="status-badge rounded-full max-w-[95px] w-fit px-3 inline-flex items-center text-[#ffffff] text-[14px] mr-2"
:class="{
'badge-activated': isActivated,
'badge-deactivated': !isActivated,
'bg-[#0099FF]': isActivated,
'bg-[#C9CDD4]': !isActivated,
}"
>
{{ displayText }}
</div>
</template>
<div
class="status-badge rounded-full max-w-[95px] w-fit px-3 inline-flex items-center text-[#ffffff] text-[14px] mr-2"
:class="{
'badge-activated': isActivated,
'badge-deactivated': !isActivated,
'bg-[#0099FF]': isActivated,
'bg-[#C9CDD4]': !isActivated,
}"
>
{{ displayText }}
</div>
</template>
<script setup>
<script setup>
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
@@ -23,16 +23,16 @@
* an activated/deactivated state with colored background.
*/
defineProps({
isActivated: {
type: Boolean,
required: true,
default: true,
},
displayText: {
type: String,
required: true,
default: "Status",
}
});
</script>
defineProps({
isActivated: {
type: Boolean,
required: true,
default: true,
},
displayText: {
type: String,
required: true,
default: "Status",
},
});
</script>

View File

@@ -1,18 +1,16 @@
<template>
<button class="button-component w-[80px] h-[32px] rounded-full
flex text-[#666666] border-[1px] border-[#666666]
justify-center items-center bg-[#FFFFFF]
hover:text-[#0099FF] hover:border-[#0099FF]
focus:text-[#0099FF] focus:border-[#0099FF]
cursor-pointer"
:class="{
'ring': isPressed,
'ring-[#0099FF]' : isPressed,
'ring-opacity-30': isPressed,
}"
@mousedown="onMousedown" @mouseup="onMouseup">
{{ buttonText }}
</button>
<button
class="button-component w-[80px] h-[32px] rounded-full flex text-[#666666] border-[1px] border-[#666666] justify-center items-center bg-[#FFFFFF] hover:text-[#0099FF] hover:border-[#0099FF] focus:text-[#0099FF] focus:border-[#0099FF] cursor-pointer"
:class="{
ring: isPressed,
'ring-[#0099FF]': isPressed,
'ring-opacity-30': isPressed,
}"
@mousedown="onMousedown"
@mouseup="onMouseup"
>
{{ buttonText }}
</button>
</template>
<script setup>
@@ -26,22 +24,22 @@
* press-state ring effect.
*/
import { ref } from 'vue';
import { ref } from "vue";
defineProps({
buttonText: {
type: String,
required: false,
},
buttonText: {
type: String,
required: false,
},
});
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
isPressed.value = false;
};
</script>

View File

@@ -1,19 +1,18 @@
<!-- The filled version of the button has a solid background -->
<template>
<button class="button-filled-component w-[80px] h-[32px] rounded-full
flex text-[#FFFFFF]
justify-center items-center bg-[#0099FF]
hover:text-[#FFFFFF] hover:bg-[#0080D5]
cursor-pointer"
:class="{
'ring': isPressed,
'ring-[#0099FF]' : isPressed,
'ring-opacity-30': isPressed,
'bg-[#0099FF]': isPressed,
}"
@mousedown="onMousedown" @mouseup="onMouseup">
{{ buttonText }}
</button>
<button
class="button-filled-component w-[80px] h-[32px] rounded-full flex text-[#FFFFFF] justify-center items-center bg-[#0099FF] hover:text-[#FFFFFF] hover:bg-[#0080D5] cursor-pointer"
:class="{
ring: isPressed,
'ring-[#0099FF]': isPressed,
'ring-opacity-30': isPressed,
'bg-[#0099FF]': isPressed,
}"
@mousedown="onMousedown"
@mouseup="onMouseup"
>
{{ buttonText }}
</button>
</template>
<script setup>
@@ -27,22 +26,22 @@
* solid background and press-state ring effect.
*/
import { ref } from 'vue';
import { ref } from "vue";
defineProps({
buttonText: {
type: String,
required: false,
},
buttonText: {
type: String,
required: false,
},
});
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
isPressed.value = false;
};
</script>

View File

@@ -1,5 +1,14 @@
<template>
<Sidebar :visible="sidebarState" :closeIcon="'pi pi-angle-right'" :modal="false" position="right" :dismissable="false" class="!w-[440px]" @hide="hide" @show="show">
<Sidebar
:visible="sidebarState"
:closeIcon="'pi pi-angle-right'"
:modal="false"
position="right"
:dismissable="false"
class="!w-[440px]"
@hide="hide"
@show="show"
>
<template #header>
<p class="pl-2 text-base font-bold text-neutral-900">Summary</p>
</template>
@@ -9,7 +18,12 @@
<section class="w-[204px] box-border pr-4">
<div class="mb-4">
<p class="h2">File Name</p>
<p class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis" :title="primaryStatData.name">{{ primaryStatData.name }}</p>
<p
class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis"
:title="primaryStatData.name"
>
{{ primaryStatData.name }}
</p>
</div>
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
@@ -17,40 +31,80 @@
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.cases.count }} / {{ primaryStatData.cases.total }}</span>
<ProgressBar :value="primaryValueCases" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.cases.count }} /
{{ primaryStatData.cases.total }}</span
>
<ProgressBar
:value="primaryValueCases"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.cases.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.cases.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Traces</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.traces.count }} / {{ primaryStatData.traces.total }}</span>
<ProgressBar :value="primaryValueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.traces.count }} /
{{ primaryStatData.traces.total }}</span
>
<ProgressBar
:value="primaryValueTraces"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.traces.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.traces.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activity Instances</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.task_instances.count }} / {{ primaryStatData.task_instances.total }}</span>
<ProgressBar :value="primaryValueTaskInstances" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.task_instances.count }} /
{{ primaryStatData.task_instances.total }}</span
>
<ProgressBar
:value="primaryValueTaskInstances"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.task_instances.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.task_instances.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activities</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.tasks.count }} / {{ primaryStatData.tasks.total }}</span>
<ProgressBar :value="primaryValueTasks" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.tasks.count }} /
{{ primaryStatData.tasks.total }}</span
>
<ProgressBar
:value="primaryValueTasks"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.tasks.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.tasks.ratio }}%</span
>
</div>
</li>
</ul>
@@ -67,10 +121,34 @@
<div class="pt-1 pb-4">
<p class="h2">Case Duration</p>
<ul class="space-y-1 text-sm">
<li><Tag value="MIN" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.min }}</li>
<li><Tag value="AVG" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.average }}</li>
<li><Tag value="MED" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.median }}</li>
<li><Tag value="MAX" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.max }}</li>
<li>
<Tag
value="MIN"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.min }}
</li>
<li>
<Tag
value="AVG"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.average }}
</li>
<li>
<Tag
value="MED"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.median }}
</li>
<li>
<Tag
value="MAX"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.max }}
</li>
</ul>
</div>
</section>
@@ -78,7 +156,12 @@
<section class="w-[204px] box-border pl-4">
<div class="mb-4">
<p class="h2">File Name</p>
<p class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis" :title="secondaryStatData.name">{{ secondaryStatData.name }}</p>
<p
class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis"
:title="secondaryStatData.name"
>
{{ secondaryStatData.name }}
</p>
</div>
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
@@ -86,40 +169,80 @@
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.cases.count }} / {{ secondaryStatData.cases.total }}</span>
<ProgressBar :value="secondaryValueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.cases.count }} /
{{ secondaryStatData.cases.total }}</span
>
<ProgressBar
:value="secondaryValueTraces"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.cases.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.cases.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Traces</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.traces.count }} / {{ secondaryStatData.traces.total }}</span>
<ProgressBar :value="secondaryValueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.traces.count }} /
{{ secondaryStatData.traces.total }}</span
>
<ProgressBar
:value="secondaryValueTraces"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.traces.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.traces.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activity Instances</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.task_instances.count }} / {{ secondaryStatData.task_instances.total }}</span>
<ProgressBar :value="secondaryValueTaskInstances" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.task_instances.count }} /
{{ secondaryStatData.task_instances.total }}</span
>
<ProgressBar
:value="secondaryValueTaskInstances"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.task_instances.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.task_instances.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activities</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.tasks.count }} / {{ secondaryStatData.tasks.total }}</span>
<ProgressBar :value="secondaryValueTasks" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.tasks.count }} /
{{ secondaryStatData.tasks.total }}</span
>
<ProgressBar
:value="secondaryValueTasks"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.tasks.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.tasks.ratio }}%</span
>
</div>
</li>
</ul>
@@ -136,10 +259,34 @@
<div class="pt-1 pb-4">
<p class="h2">Case Duration</p>
<ul class="space-y-1 text-sm">
<li><Tag value="MIN" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.min }}</li>
<li><Tag value="AVG" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.average }}</li>
<li><Tag value="MED" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.median }}</li>
<li><Tag value="MAX" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.max }}</li>
<li>
<Tag
value="MIN"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.min }}
</li>
<li>
<Tag
value="AVG"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.average }}
</li>
<li>
<Tag
value="MED"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.median }}
</li>
<li>
<Tag
value="MAX"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.max }}
</li>
</ul>
</div>
</section>
@@ -158,11 +305,11 @@
* traces, activities, timeframes, durations) of two files.
*/
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useCompareStore } from '@/stores/compare';
import { getTimeLabel } from '@/module/timeLabel.js';
import getMoment from 'moment';
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { useCompareStore } from "@/stores/compare";
import { getTimeLabel } from "@/module/timeLabel.js";
import getMoment from "moment";
const props = defineProps({
sidebarState: {
@@ -191,7 +338,7 @@ const secondaryStatData = ref(null);
* @returns {number} The percentage value.
*/
const getPercentLabel = (val) => {
if((val * 100).toFixed(1) >= 100) return 100;
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
};
@@ -205,34 +352,34 @@ const getStatData = (data, fileName) => {
return {
name: fileName,
cases: {
count: data.cases.count.toLocaleString('en-US'),
total: data.cases.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.cases.ratio)
count: data.cases.count.toLocaleString("en-US"),
total: data.cases.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.cases.ratio),
},
traces: {
count: data.traces.count.toLocaleString('en-US'),
total: data.traces.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.traces.ratio)
count: data.traces.count.toLocaleString("en-US"),
total: data.traces.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.traces.ratio),
},
task_instances: {
count: data.task_instances.count.toLocaleString('en-US'),
total: data.task_instances.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.task_instances.ratio)
count: data.task_instances.count.toLocaleString("en-US"),
total: data.task_instances.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.task_instances.ratio),
},
tasks: {
count: data.tasks.count.toLocaleString('en-US'),
total: data.tasks.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.tasks.ratio)
count: data.tasks.count.toLocaleString("en-US"),
total: data.tasks.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.tasks.ratio),
},
started_at: getMoment(data.started_at).format('YYYY.MM.DD HH:mm'),
completed_at: getMoment(data.completed_at).format('YYYY.MM.DD HH:mm'),
started_at: getMoment(data.started_at).format("YYYY.MM.DD HH:mm"),
completed_at: getMoment(data.completed_at).format("YYYY.MM.DD HH:mm"),
case_duration: {
min: getTimeLabel(data.case_duration.min, 2),
max: getTimeLabel(data.case_duration.max, 2),
average: getTimeLabel(data.case_duration.average, 2),
median: getTimeLabel(data.case_duration.median, 2),
}
}
},
};
};
/** Populates progress bar values when the sidebar is shown. */
@@ -243,7 +390,8 @@ const show = () => {
primaryValueTasks.value = primaryStatData.value.tasks.ratio;
secondaryValueCases.value = secondaryStatData.value.cases.ratio;
secondaryValueTraces.value = secondaryStatData.value.traces.ratio;
secondaryValueTaskInstances.value = secondaryStatData.value.task_instances.ratio;
secondaryValueTaskInstances.value =
secondaryStatData.value.task_instances.ratio;
secondaryValueTasks.value = secondaryStatData.value.tasks.ratio;
};
@@ -266,10 +414,13 @@ onMounted(async () => {
const primaryId = routeParams.primaryId;
const secondaryId = routeParams.secondaryId;
const primaryData = await compareStore.getStateData(primaryType, primaryId);
const secondaryData = await compareStore.getStateData(secondaryType, secondaryId);
const secondaryData = await compareStore.getStateData(
secondaryType,
secondaryId,
);
const primaryFileName = await compareStore.getFileName(primaryId)
const secondaryFileName = await compareStore.getFileName(secondaryId)
const primaryFileName = await compareStore.getFileName(primaryId);
const secondaryFileName = await compareStore.getFileName(secondaryId);
primaryStatData.value = await getStatData(primaryData, primaryFileName);
secondaryStatData.value = await getStatData(secondaryData, secondaryFileName);
});
@@ -279,9 +430,9 @@ onMounted(async () => {
background-color: var(--bg-color);
}
.progressbar-primary {
--bg-color: #0099FF;
--bg-color: #0099ff;
}
.progressbar-secondary {
--bg-color: #FFAA44;
--bg-color: #ffaa44;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,30 @@
<template>
<div class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2">
<div
class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2"
>
<p class="h2 pl-2 border-b mb-3">Activity list</p>
<div class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]" id="cyp-conformance-list-checkbox">
<div class="flex items-center w-[166px]" v-for="(act, index) in sortData" :key="index" :title="act">
<Checkbox v-model="actList" :inputId="index.toString()" name="actList" :value="act" @change="actListData"/>
<label :for="index" class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</label>
<div
class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]"
id="cyp-conformance-list-checkbox"
>
<div
class="flex items-center w-[166px]"
v-for="(act, index) in sortData"
:key="index"
:title="act"
>
<Checkbox
v-model="actList"
:inputId="index.toString()"
name="actList"
:value="act"
@change="actListData"
/>
<label
:for="index"
class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>{{ act }}</label
>
</div>
</div>
</div>
@@ -20,30 +40,37 @@
* Checkbox-based activity list for conformance checking input.
*/
import { ref, watch } from 'vue';
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
import { ref, watch } from "vue";
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
const props = defineProps(['data', 'select']);
const props = defineProps(["data", "select"]);
const sortData = ref([]);
const actList = ref(props.select);
watch(() => props.data, (newValue) => {
sortData.value = sortNumEngZhtw(newValue);
}, { immediate: true });
watch(
() => props.data,
(newValue) => {
sortData.value = sortNumEngZhtw(newValue);
},
{ immediate: true },
);
watch(() => props.select, (newValue) => {
actList.value = newValue;
});
watch(
() => props.select,
(newValue) => {
actList.value = newValue;
},
);
/** Emits the selected activities list via the event bus. */
function actListData() {
emitter.emit('actListData', actList.value);
emitter.emit("actListData", actList.value);
}
// created
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
actList.value = data;
});
</script>

View File

@@ -1,10 +1,29 @@
<template>
<div class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2">
<div
class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2"
>
<p class="h2 pl-2 border-b mb-3">{{ title }}</p>
<div class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]">
<div class="flex items-center w-[166px]" v-for="(act, index) in sortData" :key="index" :title="act">
<RadioButton v-model="selectedRadio" :inputId="index + act" :name="select" :value="act" @change="actRadioData" />
<label :for="index + act" class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</label>
<div
class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]"
>
<div
class="flex items-center w-[166px]"
v-for="(act, index) in sortData"
:key="index"
:title="act"
>
<RadioButton
v-model="selectedRadio"
:inputId="index + act"
:name="select"
:value="act"
@change="actRadioData"
/>
<label
:for="index + act"
class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>{{ act }}</label
>
</div>
</div>
</div>
@@ -22,13 +41,20 @@
* start/end activity input.
*/
import { ref, computed, watch } from 'vue';
import { ref, computed, watch } from "vue";
import { useConformanceInputStore } from "@/stores/conformanceInput";
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
const props = defineProps(['title', 'select', 'data', 'category', 'task', 'isSubmit']);
const emit = defineEmits(['selected-task']);
const props = defineProps([
"title",
"select",
"data",
"category",
"task",
"isSubmit",
]);
const emit = defineEmits(["selected-task"]);
const conformanceInputStore = useConformanceInputStore();
@@ -36,13 +62,20 @@ const sortData = ref([]);
const localSelect = ref(null);
const selectedRadio = ref(null);
watch(() => props.data, (newValue) => {
sortData.value = sortNumEngZhtw(newValue);
}, { immediate: true });
watch(
() => props.data,
(newValue) => {
sortData.value = sortNumEngZhtw(newValue);
},
{ immediate: true },
);
watch(() => props.task, (newValue) => {
selectedRadio.value = newValue;
});
watch(
() => props.task,
(newValue) => {
selectedRadio.value = newValue;
},
);
const inputActivityRadioData = computed(() => ({
category: props.category,
@@ -52,22 +85,27 @@ const inputActivityRadioData = computed(() => ({
/** Emits the selected activity via event bus and updates the store. */
function actRadioData() {
localSelect.value = null;
emitter.emit('actRadioData', inputActivityRadioData.value);
emit('selected-task', selectedRadio.value);
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task);
emitter.emit("actRadioData", inputActivityRadioData.value);
emit("selected-task", selectedRadio.value);
conformanceInputStore.setActivityRadioStartEndData(
inputActivityRadioData.value.task,
);
}
/** Sets the global activity radio data state in the conformance input store. */
function setGlobalActivityRadioDataState() {
//this.title: value might be "From" or "To"
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task, props.title);
conformanceInputStore.setActivityRadioStartEndData(
inputActivityRadioData.value.task,
props.title,
);
}
// created
sortNumEngZhtw(sortData.value);
localSelect.value = props.isSubmit ? props.select : null;
selectedRadio.value = localSelect.value;
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
selectedRadio.value = data;
});
setGlobalActivityRadioDataState();

View File

@@ -1,38 +1,99 @@
<template>
<div class="h-full w-full flex justify-between items-center">
<!-- Activity List -->
<div class="h-full w-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2">
<div
class="h-full w-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2"
>
<p class="h2 pl-2 border-b mb-3">Activity list</p>
<div class="h-[calc(100%_-_56px)]">
<Draggable :list="datadata" :group="{name: 'activity', pull: 'clone' }" itemKey="name" animation="300" :fallbackTolerance="5" :forceFallback="true" :ghostClass="'ghostSelected'" :dragClass="'dragSelected'" :sort="false" @end="onEnd" class="h-full flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar">
<Draggable
:list="datadata"
:group="{ name: 'activity', pull: 'clone' }"
itemKey="name"
animation="300"
:fallbackTolerance="5"
:forceFallback="true"
:ghostClass="'ghostSelected'"
:dragClass="'dragSelected'"
:sort="false"
@end="onEnd"
class="h-full flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar"
>
<template #item="{ element, index }">
<div :class="listSequence.includes(element) ? 'border-primary text-primary' : ''" class="flex items-center w-[166px] border rounded p-2 bg-neutral-10 cursor-pointer hover:bg-primary/20" @dblclick="moveActItem(index, element)" :title="element">
<span class="whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ element }}</span>
<div
:class="
listSequence.includes(element)
? 'border-primary text-primary'
: ''
"
class="flex items-center w-[166px] border rounded p-2 bg-neutral-10 cursor-pointer hover:bg-primary/20"
@dblclick="moveActItem(index, element)"
:title="element"
>
<span
class="whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>{{ element }}</span
>
</div>
</template>
</Draggable>
</div>
</div>
<!-- sequence -->
<div class="w-full h-full relative bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2 text-sm">
<div
class="w-full h-full relative bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2 text-sm"
>
<p class="h2 border-b border-500 mb-3">Sequence</p>
<!-- No Data -->
<div v-if="listSequence && listSequence.length === 0" class="p-4 w-[calc(100%_-_32px)] h-5/6 flex justify-center items-center absolute">
<p class="text-neutral-500">Please drag and drop at least two activities here and sort.</p>
<div
v-if="listSequence && listSequence.length === 0"
class="p-4 w-[calc(100%_-_32px)] h-5/6 flex justify-center items-center absolute"
>
<p class="text-neutral-500">
Please drag and drop at least two activities here and sort.
</p>
</div>
<!-- Have Data -->
<div class="m-auto w-full h-[calc(100%_-_56px)]">
<div class="w-full h-full overflow-y-auto overflow-x-auto scrollbar px-4 text-center">
<draggable class="h-full" :group="{name: 'activity'}" :list="listSequence" itemKey="name" animation="300" :forceFallback="true" :dragClass="'dragSelected'" :fallbackTolerance="5" @start="onStart" @end="onEnd" :component-data="getComponentData()" :ghostClass="'!opacity-0'">
<div class="m-auto w-full h-[calc(100%_-_56px)]">
<div
class="w-full h-full overflow-y-auto overflow-x-auto scrollbar px-4 text-center"
>
<draggable
class="h-full"
:group="{ name: 'activity' }"
:list="listSequence"
itemKey="name"
animation="300"
:forceFallback="true"
:dragClass="'dragSelected'"
:fallbackTolerance="5"
@start="onStart"
@end="onEnd"
:component-data="getComponentData()"
:ghostClass="'!opacity-0'"
>
<template #item="{ element, index }">
<div :title="element">
<div class="flex justify-center items-center">
<div class="w-full p-2 border rounded bg-neutral-10 cursor-pointer hover:bg-primary/20" @dblclick="moveSeqItem(index, element)">
<div
class="w-full p-2 border rounded bg-neutral-10 cursor-pointer hover:bg-primary/20"
@dblclick="moveSeqItem(index, element)"
>
<span>{{ element }}</span>
</div>
<span class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger" @click.stop.native="moveSeqItem(index, element)">close</span>
<span
class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger"
@click.stop.native="moveSeqItem(index, element)"
>close</span
>
</div>
<span v-show="index !== listSequence.length - 1 && index !== lastItemIndex - 1" class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"></span>
<span
v-show="
index !== listSequence.length - 1 &&
index !== lastItemIndex - 1
"
class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"
></span>
</div>
</template>
</draggable>
@@ -54,11 +115,11 @@
* conformance rule configuration.
*/
import { ref, computed } from 'vue';
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
import { ref, computed } from "vue";
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
const props = defineProps(['data', 'listSeq', 'isSubmit', 'category']);
const props = defineProps(["data", "listSeq", "isSubmit", "category"]);
const listSequence = ref([]);
const lastItemIndex = ref(null);
@@ -67,7 +128,7 @@ const isSelect = ref(true);
const datadata = computed(() => {
// Sort the Activity List
let newData;
if(props.data !== null) {
if (props.data !== null) {
newData = JSON.parse(JSON.stringify(props.data));
sortNumEngZhtw(newData);
}
@@ -96,7 +157,7 @@ function moveSeqItem(index, element) {
* get listSequence
*/
function getComponentData() {
emitter.emit('getListSequence', {
emitter.emit("getListSequence", {
category: props.category,
task: listSequence.value,
});
@@ -107,13 +168,13 @@ function getComponentData() {
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
lastChild.style.display = "none";
// Hide the dragged element at its original position
const originalElement = evt.item;
originalElement.style.display = 'none';
originalElement.style.display = "none";
// When dragging the last element, hide the arrow of the second-to-last element
const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
if (evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
}
/**
@@ -122,12 +183,12 @@ function onStart(evt) {
function onEnd(evt) {
// Show the dragged element
const originalElement = evt.item;
originalElement.style.display = '';
originalElement.style.display = "";
// Show the arrow after drag ends, except for the last element
const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1;
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
lastChild.style.display = "";
}
// Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null;
@@ -136,16 +197,16 @@ function onEnd(evt) {
// created
const newlist = JSON.parse(JSON.stringify(props.listSeq));
listSequence.value = props.isSubmit ? newlist : [];
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
listSequence.value = [];
});
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
.ghostSelected {
@apply bg-primary/20
@apply bg-primary/20;
}
.dragSelected {
@apply !opacity-100
@apply !opacity-100;
}
</style>

View File

@@ -1,50 +1,117 @@
<template>
<section class="space-y-2 text-sm">
<section class="space-y-2 text-sm">
<!-- Rule Type -->
<div id="cyp-conformance-type-radio">
<p class="h2">Rule Type</p>
<div v-for="rule in ruleType" :key="rule.id" class="ml-4 mb-2">
<RadioButton v-model="selectedRuleType" :inputId="rule.id + rule.name" name="ruleType" :value="rule.name" @change="changeRadio"/>
<RadioButton
v-model="selectedRuleType"
:inputId="rule.id + rule.name"
name="ruleType"
:value="rule.name"
@change="changeRadio"
/>
<label :for="rule.id + rule.name" class="ml-2">{{ rule.name }}</label>
</div>
</div>
<!-- Activity Sequence (2 item) -->
<div v-show="selectedRuleType === 'Activity sequence'" id="cyp-conformance-sequence-radio">
<div
v-show="selectedRuleType === 'Activity sequence'"
id="cyp-conformance-sequence-radio"
>
<p class="h2">Activity Sequence</p>
<div v-for="act in activitySequence" :key="act.id" class="ml-4 mb-2">
<RadioButton v-model="selectedActivitySequence" :inputId="act.id + act.name" name="activitySequence" :value="act.name" @change="changeRadioSeq"/>
<RadioButton
v-model="selectedActivitySequence"
:inputId="act.id + act.name"
name="activitySequence"
:value="act.name"
@change="changeRadioSeq"
/>
<label :for="act.id + act.name" class="ml-2">{{ act.name }}</label>
</div>
</div>
<!-- Mode -->
<div v-show="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence'" id="cyp-conformance-Mode-radio">
<div
v-show="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence'
"
id="cyp-conformance-Mode-radio"
>
<p class="h2">Mode</p>
<div v-for="mode in mode" :key="mode.id" class="ml-4 mb-2">
<RadioButton v-model="selectedMode" :inputId="mode.id + mode.name" name="mode" :value="mode.name" />
<RadioButton
v-model="selectedMode"
:inputId="mode.id + mode.name"
name="mode"
:value="mode.name"
/>
<label :for="mode.id + mode.name" class="ml-2">{{ mode.name }}</label>
</div>
</div>
<!-- Process Scope -->
<div v-show="selectedRuleType === 'Processing time' || selectedRuleType === 'Waiting time'" id="cyp-conformance-procss-radio">
<div
v-show="
selectedRuleType === 'Processing time' ||
selectedRuleType === 'Waiting time'
"
id="cyp-conformance-procss-radio"
>
<p class="h2">Process Scope</p>
<div v-for="pro in processScope" :key="pro.id" class="ml-4 mb-2">
<RadioButton v-model="selectedProcessScope" :inputId="pro.id + pro.name" name="processScope" :value="pro.name" @change="changeRadioProcessScope"/>
<RadioButton
v-model="selectedProcessScope"
:inputId="pro.id + pro.name"
name="processScope"
:value="pro.name"
@change="changeRadioProcessScope"
/>
<label :for="pro.id + pro.name" class="ml-2">{{ pro.name }}</label>
</div>
</div>
<!-- Activity Sequence (4 item) -->
<div v-show="(selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end') || (selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end') || selectedRuleType === 'Cycle time'" id="cyp-conformance-actseq-radio">
<div
v-show="
(selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end') ||
(selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end') ||
selectedRuleType === 'Cycle time'
"
id="cyp-conformance-actseq-radio"
>
<p class="h2">Activity Sequence</p>
<div v-for="act in actSeqMore" :key="act.id" class="ml-4 mb-2">
<RadioButton v-model="selectedActSeqMore" :inputId="act.id + act.name" name="activitySequenceMore" :value="act.name" @change="changeRadioActSeqMore"/>
<RadioButton
v-model="selectedActSeqMore"
:inputId="act.id + act.name"
name="activitySequenceMore"
:value="act.name"
@change="changeRadioActSeqMore"
/>
<label :for="act.id + act.name" class="ml-2">{{ act.name }}</label>
</div>
</div>
<!-- Activity Sequence (3 item) -->
<div v-show="(selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial') || (selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial')" id="cyp-conformance-actseqfromto-radio">
<div
v-show="
(selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial') ||
(selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial')
"
id="cyp-conformance-actseqfromto-radio"
>
<p class="h2">Activity Sequence</p>
<div v-for="act in actSeqFromTo" :key="act.id" class="ml-4 mb-2">
<RadioButton v-model="selectedActSeqFromTo" :inputId="act.id + act.name" name="activitySequenceFromTo" :value="act.name" @change="changeRadioActSeqFromTo"/>
<RadioButton
v-model="selectedActSeqFromTo"
:inputId="act.id + act.name"
name="activitySequenceFromTo"
:value="act.name"
@change="changeRadioActSeqFromTo"
/>
<label :for="act.id + act.name" class="ml-2">{{ act.name }}</label>
</div>
</div>
@@ -62,70 +129,77 @@
* sequence, mode, and process scope selection.
*/
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo } = storeToRefs(conformanceStore);
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
} = storeToRefs(conformanceStore);
const ruleType = [
{id: 1, name: 'Have activity'},
{id: 2, name: 'Activity sequence'},
{id: 3, name: 'Activity duration'},
{id: 4, name: 'Processing time'},
{id: 5, name: 'Waiting time'},
{id: 6, name: 'Cycle time'},
{ id: 1, name: "Have activity" },
{ id: 2, name: "Activity sequence" },
{ id: 3, name: "Activity duration" },
{ id: 4, name: "Processing time" },
{ id: 5, name: "Waiting time" },
{ id: 6, name: "Cycle time" },
];
const activitySequence = [
{id: 1, name: 'Start & End'},
{id: 2, name: 'Sequence'},
{ id: 1, name: "Start & End" },
{ id: 2, name: "Sequence" },
];
const mode = [
{id: 1, name: 'Directly follows'},
{id: 2, name: 'Eventually follows'},
{id: 3, name: 'Short loop(s)'},
{id: 4, name: 'Self loop(s)'},
{ id: 1, name: "Directly follows" },
{ id: 2, name: "Eventually follows" },
{ id: 3, name: "Short loop(s)" },
{ id: 4, name: "Self loop(s)" },
];
const processScope = [
{id: 1, name: 'End to end'},
{id: 2, name: 'Partial'},
{ id: 1, name: "End to end" },
{ id: 2, name: "Partial" },
];
const actSeqMore = [
{id: 1, name: 'All'},
{id: 2, name: 'Start'},
{id: 3, name: 'End'},
{id: 4, name: 'Start & End'},
{ id: 1, name: "All" },
{ id: 2, name: "Start" },
{ id: 3, name: "End" },
{ id: 4, name: "Start & End" },
];
const actSeqFromTo = [
{id: 1, name: 'From'},
{id: 2, name: 'To'},
{id: 3, name: 'From & To'},
{ id: 1, name: "From" },
{ id: 2, name: "To" },
{ id: 3, name: "From & To" },
];
/** Resets dependent selections when the rule type radio changes. */
function changeRadio() {
selectedActivitySequence.value = 'Start & End';
selectedMode.value = 'Directly follows';
selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All';
selectedActSeqFromTo.value = 'From';
emitter.emit('isRadioChange', true); // Clear data when switching radio buttons
selectedActivitySequence.value = "Start & End";
selectedMode.value = "Directly follows";
selectedProcessScope.value = "End to end";
selectedActSeqMore.value = "All";
selectedActSeqFromTo.value = "From";
emitter.emit("isRadioChange", true); // Clear data when switching radio buttons
}
/** Emits event when the activity sequence radio changes. */
function changeRadioSeq() {
emitter.emit('isRadioSeqChange',true);
emitter.emit("isRadioSeqChange", true);
}
/** Emits event when the process scope radio changes. */
function changeRadioProcessScope() {
emitter.emit('isRadioProcessScopeChange', true);
emitter.emit("isRadioProcessScopeChange", true);
}
/** Emits event when the extended activity sequence radio changes. */
function changeRadioActSeqMore() {
emitter.emit('isRadioActSeqMoreChange', true);
emitter.emit("isRadioActSeqMoreChange", true);
}
/** Emits event when the from/to activity sequence radio changes. */
function changeRadioActSeqFromTo() {
emitter.emit('isRadioActSeqFromToChange', true);
emitter.emit("isRadioActSeqFromToChange", true);
}
</script>

View File

@@ -1,31 +1,183 @@
<template>
<div class="px-4 text-sm">
<!-- Have activity -->
<ResultCheck v-if="selectedRuleType === 'Have activity'" :data="state.containstTasksData" :select="isSubmitTask"></ResultCheck>
<ResultCheck
v-if="selectedRuleType === 'Have activity'"
:data="state.containstTasksData"
:select="isSubmitTask"
></ResultCheck>
<!-- Activity sequence -->
<ResultDot v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Start & End'" :timeResultData="selectCfmSeqSE" :select="isSubmitStartAndEnd"></ResultDot>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Directly follows'" :data="state.selectCfmSeqDirectly" :select="isSubmitCfmSeqDirectly"></ResultArrow>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Eventually follows'" :data="state.selectCfmSeqEventually" :select="isSubmitCfmSeqEventually"></ResultArrow>
<ResultDot
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Start & End'
"
:timeResultData="selectCfmSeqSE"
:select="isSubmitStartAndEnd"
></ResultDot>
<ResultArrow
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Directly follows'
"
:data="state.selectCfmSeqDirectly"
:select="isSubmitCfmSeqDirectly"
></ResultArrow>
<ResultArrow
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Eventually follows'
"
:data="state.selectCfmSeqEventually"
:select="isSubmitCfmSeqEventually"
></ResultArrow>
<!-- Activity duration -->
<ResultCheck v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'" :data="state.durationData" :select="isSubmitDurationData"></ResultCheck>
<ResultCheck
v-if="selectedRuleType === 'Activity duration'"
:title="'Activities include'"
:data="state.durationData"
:select="isSubmitDurationData"
></ResultCheck>
<!-- Processing time -->
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmPtEteStart" :select="isSubmitCfmPtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmPtEteEnd" :select="isSubmitCfmPtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmPtEteSE" :select="isSubmitCfmPtEteSE"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmPtPStart" :select="isSubmitCfmPtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmPtPEnd" :select="isSubmitCfmPtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmPtPSE" :select="isSubmitCfmPtPSE"></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:timeResultData="state.selectCfmPtEteStart"
:select="isSubmitCfmPtEteStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:timeResultData="state.selectCfmPtEteEnd"
:select="isSubmitCfmPtEteEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:timeResultData="selectCfmPtEteSE"
:select="isSubmitCfmPtEteSE"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:timeResultData="state.selectCfmPtPStart"
:select="isSubmitCfmPtPStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:timeResultData="state.selectCfmPtPEnd"
:select="isSubmitCfmPtPEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:timeResultData="selectCfmPtPSE"
:select="isSubmitCfmPtPSE"
></ResultDot>
<!-- Waiting time -->
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmWtEteStart" :select="isSubmitCfmWtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmWtEteEnd" :select="isSubmitCfmWtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmWtEteSE" :select="isSubmitCfmWtEteSE"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmWtPStart" :select="isSubmitCfmWtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmWtPEnd" :select="isSubmitCfmWtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmWtPSE" :select="isSubmitCfmWtPSE"></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:timeResultData="state.selectCfmWtEteStart"
:select="isSubmitCfmWtEteStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:timeResultData="state.selectCfmWtEteEnd"
:select="isSubmitCfmWtEteEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:timeResultData="selectCfmWtEteSE"
:select="isSubmitCfmWtEteSE"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:timeResultData="state.selectCfmWtPStart"
:select="isSubmitCfmWtPStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:timeResultData="state.selectCfmWtPEnd"
:select="isSubmitCfmWtPEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:timeResultData="selectCfmWtPSE"
:select="isSubmitCfmWtPSE"
></ResultDot>
<!-- Cycle time -->
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmCtEteStart" :select="isSubmitCfmCtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmCtEteEnd" :select="isSubmitCfmCtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmCtEteSE" :select="isSubmitCfmCtEteSE"></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:timeResultData="state.selectCfmCtEteStart"
:select="isSubmitCfmCtEteStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:timeResultData="state.selectCfmCtEteEnd"
:select="isSubmitCfmCtEteEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:timeResultData="selectCfmCtEteSE"
:select="isSubmitCfmCtEteSE"
></ResultDot>
</div>
</template>
<script setup>
@@ -41,18 +193,49 @@
* scrollable display of check results.
*/
import { reactive, computed, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import ResultCheck from '@/components/Discover/Conformance/ConformanceSidebar/ResultCheck.vue';
import ResultArrow from '@/components/Discover/Conformance/ConformanceSidebar/ResultArrow.vue';
import ResultDot from '@/components/Discover/Conformance/ConformanceSidebar/ResultDot.vue';
import { reactive, computed, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
import ResultCheck from "@/components/Discover/Conformance/ConformanceSidebar/ResultCheck.vue";
import ResultArrow from "@/components/Discover/Conformance/ConformanceSidebar/ResultArrow.vue";
import ResultDot from "@/components/Discover/Conformance/ConformanceSidebar/ResultDot.vue";
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected } = storeToRefs(conformanceStore);
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
isStartSelected,
isEndSelected,
} = storeToRefs(conformanceStore);
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually', 'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE', 'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart', 'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd', 'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE']);
const props = defineProps([
"isSubmit",
"isSubmitTask",
"isSubmitStartAndEnd",
"isSubmitCfmSeqDirectly",
"isSubmitCfmSeqEventually",
"isSubmitDurationData",
"isSubmitCfmPtEteStart",
"isSubmitCfmPtEteEnd",
"isSubmitCfmPtEteSE",
"isSubmitCfmPtPStart",
"isSubmitCfmPtPEnd",
"isSubmitCfmPtPSE",
"isSubmitCfmWtEteStart",
"isSubmitCfmWtEteEnd",
"isSubmitCfmWtEteSE",
"isSubmitCfmWtPStart",
"isSubmitCfmWtPEnd",
"isSubmitCfmWtPSE",
"isSubmitCfmCtEteStart",
"isSubmitCfmCtEteEnd",
"isSubmitCfmCtEteSE",
]);
const state = reactive({
containstTasksData: null,
@@ -87,10 +270,10 @@ const state = reactive({
const selectCfmSeqSE = computed(() => {
const data = [];
if(state.selectCfmSeqStart) data.push(state.selectCfmSeqStart);
if(state.selectCfmSeqEnd) data.push(state.selectCfmSeqEnd);
if (state.selectCfmSeqStart) data.push(state.selectCfmSeqStart);
if (state.selectCfmSeqEnd) data.push(state.selectCfmSeqEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -98,10 +281,10 @@ const selectCfmSeqSE = computed(() => {
const selectCfmPtEteSE = computed(() => {
const data = [];
if(state.selectCfmPtEteSEStart) data.push(state.selectCfmPtEteSEStart);
if(state.selectCfmPtEteSEEnd) data.push(state.selectCfmPtEteSEEnd);
if (state.selectCfmPtEteSEStart) data.push(state.selectCfmPtEteSEStart);
if (state.selectCfmPtEteSEEnd) data.push(state.selectCfmPtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -109,10 +292,10 @@ const selectCfmPtEteSE = computed(() => {
const selectCfmPtPSE = computed(() => {
const data = [];
if(state.selectCfmPtPSEStart) data.push(state.selectCfmPtPSEStart);
if(state.selectCfmPtPSEEnd) data.push(state.selectCfmPtPSEEnd);
if (state.selectCfmPtPSEStart) data.push(state.selectCfmPtPSEStart);
if (state.selectCfmPtPSEEnd) data.push(state.selectCfmPtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
const order = { From: 1, To: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -120,10 +303,10 @@ const selectCfmPtPSE = computed(() => {
const selectCfmWtEteSE = computed(() => {
const data = [];
if(state.selectCfmWtEteSEStart) data.push(state.selectCfmWtEteSEStart);
if(state.selectCfmWtEteSEEnd) data.push(state.selectCfmWtEteSEEnd);
if (state.selectCfmWtEteSEStart) data.push(state.selectCfmWtEteSEStart);
if (state.selectCfmWtEteSEEnd) data.push(state.selectCfmWtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -131,10 +314,10 @@ const selectCfmWtEteSE = computed(() => {
const selectCfmWtPSE = computed(() => {
const data = [];
if(state.selectCfmWtPSEStart) data.push(state.selectCfmWtPSEStart);
if(state.selectCfmWtPSEEnd) data.push(state.selectCfmWtPSEEnd);
if (state.selectCfmWtPSEStart) data.push(state.selectCfmWtPSEStart);
if (state.selectCfmWtPSEEnd) data.push(state.selectCfmWtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
const order = { From: 1, To: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -142,10 +325,10 @@ const selectCfmWtPSE = computed(() => {
const selectCfmCtEteSE = computed(() => {
const data = [];
if(state.selectCfmCtEteSEStart) data.push(state.selectCfmCtEteSEStart);
if(state.selectCfmCtEteSEEnd) data.push(state.selectCfmCtEteSEEnd);
if (state.selectCfmCtEteSEStart) data.push(state.selectCfmCtEteSEStart);
if (state.selectCfmCtEteSEEnd) data.push(state.selectCfmCtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -186,35 +369,35 @@ function reset() {
}
// created() logic
emitter.on('actListData', (data) => {
emitter.on("actListData", (data) => {
state.containstTasksData = data;
});
emitter.on('actRadioData', (newData) => {
emitter.on("actRadioData", (newData) => {
const data = JSON.parse(JSON.stringify(newData)); // Deep copy the original cases data
const categoryMapping = {
'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'],
'cfmSeqEnd': ['End', 'selectCfmSeqEnd', 'selectCfmSeqStart'],
'cfmPtEteStart': ['Start', 'selectCfmPtEteStart'],
'cfmPtEteEnd': ['End', 'selectCfmPtEteEnd'],
'cfmPtEteSEStart': ['Start', 'selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd'],
'cfmPtEteSEEnd': ['End', 'selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart'],
'cfmPtPStart': ['From', 'selectCfmPtPStart'],
'cfmPtPEnd': ['To', 'selectCfmPtPEnd'],
'cfmPtPSEStart': ['From', 'selectCfmPtPSEStart', 'selectCfmPtPSEEnd'],
'cfmPtPSEEnd': ['To', 'selectCfmPtPSEEnd', 'selectCfmPtPSEStart'],
'cfmWtEteStart': ['Start', 'selectCfmWtEteStart'],
'cfmWtEteEnd': ['End', 'selectCfmWtEteEnd'],
'cfmWtEteSEStart': ['Start', 'selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd'],
'cfmWtEteSEEnd': ['End', 'selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart'],
'cfmWtPStart': ['From', 'selectCfmWtPStart'],
'cfmWtPEnd': ['To', 'selectCfmWtPEnd'],
'cfmWtPSEStart': ['From', 'selectCfmWtPSEStart', 'selectCfmWtPSEEnd'],
'cfmWtPSEEnd': ['To', 'selectCfmWtPSEEnd', 'selectCfmWtPSEStart'],
'cfmCtEteStart': ['Start', 'selectCfmCtEteStart'],
'cfmCtEteEnd': ['End', 'selectCfmCtEteEnd'],
'cfmCtEteSEStart': ['Start', 'selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd'],
'cfmCtEteSEEnd': ['End', 'selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart']
cfmSeqStart: ["Start", "selectCfmSeqStart", "selectCfmSeqEnd"],
cfmSeqEnd: ["End", "selectCfmSeqEnd", "selectCfmSeqStart"],
cfmPtEteStart: ["Start", "selectCfmPtEteStart"],
cfmPtEteEnd: ["End", "selectCfmPtEteEnd"],
cfmPtEteSEStart: ["Start", "selectCfmPtEteSEStart", "selectCfmPtEteSEEnd"],
cfmPtEteSEEnd: ["End", "selectCfmPtEteSEEnd", "selectCfmPtEteSEStart"],
cfmPtPStart: ["From", "selectCfmPtPStart"],
cfmPtPEnd: ["To", "selectCfmPtPEnd"],
cfmPtPSEStart: ["From", "selectCfmPtPSEStart", "selectCfmPtPSEEnd"],
cfmPtPSEEnd: ["To", "selectCfmPtPSEEnd", "selectCfmPtPSEStart"],
cfmWtEteStart: ["Start", "selectCfmWtEteStart"],
cfmWtEteEnd: ["End", "selectCfmWtEteEnd"],
cfmWtEteSEStart: ["Start", "selectCfmWtEteSEStart", "selectCfmWtEteSEEnd"],
cfmWtEteSEEnd: ["End", "selectCfmWtEteSEEnd", "selectCfmWtEteSEStart"],
cfmWtPStart: ["From", "selectCfmWtPStart"],
cfmWtPEnd: ["To", "selectCfmWtPEnd"],
cfmWtPSEStart: ["From", "selectCfmWtPSEStart", "selectCfmWtPSEEnd"],
cfmWtPSEEnd: ["To", "selectCfmWtPSEEnd", "selectCfmWtPSEStart"],
cfmCtEteStart: ["Start", "selectCfmCtEteStart"],
cfmCtEteEnd: ["End", "selectCfmCtEteEnd"],
cfmCtEteSEStart: ["Start", "selectCfmCtEteSEStart", "selectCfmCtEteSEEnd"],
cfmCtEteSEEnd: ["End", "selectCfmCtEteSEEnd", "selectCfmCtEteSEStart"],
};
const updateSelection = (key, mainSelector, secondarySelector) => {
@@ -225,64 +408,65 @@ emitter.on('actRadioData', (newData) => {
state[mainSelector] = data;
};
if (categoryMapping[data.category]) {
const [category, mainSelector, secondarySelector] = categoryMapping[data.category];
if (secondarySelector) {
updateSelection(data.category, mainSelector, secondarySelector);
} else {
data.category = category;
state[mainSelector] = [data];
}
} else if (selectedRuleType.value === 'Activity duration') {
state.durationData = [data.task];
if (categoryMapping[data.category]) {
const [category, mainSelector, secondarySelector] =
categoryMapping[data.category];
if (secondarySelector) {
updateSelection(data.category, mainSelector, secondarySelector);
} else {
data.category = category;
state[mainSelector] = [data];
}
} else if (selectedRuleType.value === "Activity duration") {
state.durationData = [data.task];
}
});
emitter.on('getListSequence', (data) => {
emitter.on("getListSequence", (data) => {
switch (data.category) {
case 'cfmSeqDirectly':
case "cfmSeqDirectly":
state.selectCfmSeqDirectly = data.task;
break;
case 'cfmSeqEventually':
case "cfmSeqEventually":
state.selectCfmSeqEventually = data.task;
break;
default:
break;
}
});
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
reset();
});
// Clear data when switching radio buttons
emitter.on('isRadioChange', (data) => {
if(data) reset();
emitter.on("isRadioChange", (data) => {
if (data) reset();
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) reset();
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) reset();
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) reset();
emitter.on("isRadioActSeqMoreChange", (data) => {
if (data) reset();
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) reset();
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) reset();
});
onBeforeUnmount(() => {
emitter.off('actListData');
emitter.off('actRadioData');
emitter.off('getListSequence');
emitter.off('reset');
emitter.off('isRadioChange');
emitter.off('isRadioProcessScopeChange');
emitter.off('isRadioActSeqMoreChange');
emitter.off('isRadioActSeqFromToChange');
emitter.off("actListData");
emitter.off("actRadioData");
emitter.off("getListSequence");
emitter.off("reset");
emitter.off("isRadioChange");
emitter.off("isRadioProcessScopeChange");
emitter.off("isRadioActSeqMoreChange");
emitter.off("isRadioActSeqFromToChange");
});
</script>
<style scoped>
:deep(.disc) {
font-variation-settings:
'FILL' 1,
'wght' 100,
'GRAD' 0,
'opsz' 20
"FILL" 1,
"wght" 100,
"GRAD" 0,
"opsz" 20;
}
</style>

View File

@@ -1,99 +1,345 @@
<template>
<section class="animate-fadein w-full h-full" >
<section class="animate-fadein w-full h-full">
<!-- Have activity -->
<ActList v-if="selectedRuleType === 'Have activity'" :data="conformanceTask" :select="isSubmitTask"></ActList>
<ActList
v-if="selectedRuleType === 'Have activity'"
:data="conformanceTask"
:select="isSubmitTask"
></ActList>
<!-- Activity sequence -->
<div v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Start & End'"
class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start activity'" :select="isSubmitStartAndEnd?.[0].task" :data="cfmSeqStartData"
:category="'cfmSeqStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" class="w-1/2" />
<ActRadio :title="'End activity'" :select="isSubmitStartAndEnd?.[1].task" :data="cfmSeqEndData"
:category="'cfmSeqEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" class="w-1/2" />
<div
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start activity'"
:select="isSubmitStartAndEnd?.[0].task"
:data="cfmSeqStartData"
:category="'cfmSeqStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
class="w-1/2"
/>
<ActRadio
:title="'End activity'"
:select="isSubmitStartAndEnd?.[1].task"
:data="cfmSeqEndData"
:category="'cfmSeqEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
class="w-1/2"
/>
</div>
<!-- actSeqDrag -->
<ActSeqDrag v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence'
&& selectedMode === 'Directly follows'" :data="conformanceTask" :listSeq="isSubmitCfmSeqDirectly"
:isSubmit="isSubmit" :category="'cfmSeqDirectly'"></ActSeqDrag>
<ActSeqDrag v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence'
&& selectedMode === 'Eventually follows'" :data="conformanceTask" :listSeq="isSubmitCfmSeqEventually"
:isSubmit="isSubmit" :category="'cfmSeqEventually'"></ActSeqDrag>
<ActSeqDrag
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Directly follows'
"
:data="conformanceTask"
:listSeq="isSubmitCfmSeqDirectly"
:isSubmit="isSubmit"
:category="'cfmSeqDirectly'"
></ActSeqDrag>
<ActSeqDrag
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Eventually follows'
"
:data="conformanceTask"
:listSeq="isSubmitCfmSeqEventually"
:isSubmit="isSubmit"
:category="'cfmSeqEventually'"
></ActSeqDrag>
<!-- Activity duration -->
<ActRadio v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'"
:select="isSubmitDurationData?.[0]" :data="conformanceTask" :category="'cfmDur'" :isSubmit="isSubmit"/>
<ActRadio
v-if="selectedRuleType === 'Activity duration'"
:title="'Activities include'"
:select="isSubmitDurationData?.[0]"
:data="conformanceTask"
:category="'cfmDur'"
:isSubmit="isSubmit"
/>
<!-- Processing time -->
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :title="'Start'" :select="isSubmitCfmPtEteStart?.[0].task"
:data="cfmPtEteStartData" :category="'cfmPtEteStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :title="'End'" :select="isSubmitCfmPtEteEnd?.[0].task" :data="cfmPtEteEndData"
:category="'cfmPtEteEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start'" :select="isSubmitCfmPtEteSE?.[0].task" :data="cfmPtEteSEStartData"
:category="'cfmPtEteSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" class="w-1/2" />
<ActRadio :title="'End'" :select="isSubmitCfmPtEteSE?.[1].task" :data="cfmPtEteSEEndData"
:category="'cfmPtEteSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" class="w-1/2" />
<!-- Processing time -->
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:title="'Start'"
:select="isSubmitCfmPtEteStart?.[0].task"
:data="cfmPtEteStartData"
:category="'cfmPtEteStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:title="'End'"
:select="isSubmitCfmPtEteEnd?.[0].task"
:data="cfmPtEteEndData"
:category="'cfmPtEteEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start'"
:select="isSubmitCfmPtEteSE?.[0].task"
:data="cfmPtEteSEStartData"
:category="'cfmPtEteSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
class="w-1/2"
/>
<ActRadio
:title="'End'"
:select="isSubmitCfmPtEteSE?.[1].task"
:data="cfmPtEteSEEndData"
:category="'cfmPtEteSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
class="w-1/2"
/>
</div>
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :title="'From'" :select="isSubmitCfmPtPStart?.[0].task" :data="cfmPtPStartData"
:category="'cfmPtPStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :title="'To'" :select="isSubmitCfmPtPEnd?.[0].task" :data="cfmPtPEndData"
:category="'cfmPtPEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'From'" :select="isSubmitCfmPtPSE?.[0].task" :data="cfmPtPSEStartData"
class="w-1/2" :category="'cfmPtPSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'To'" :select="isSubmitCfmPtPSE?.[1].task" :data="cfmPtPSEEndData" class="w-1/2"
:category="'cfmPtPSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:title="'From'"
:select="isSubmitCfmPtPStart?.[0].task"
:data="cfmPtPStartData"
:category="'cfmPtPStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:title="'To'"
:select="isSubmitCfmPtPEnd?.[0].task"
:data="cfmPtPEndData"
:category="'cfmPtPEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'From'"
:select="isSubmitCfmPtPSE?.[0].task"
:data="cfmPtPSEStartData"
class="w-1/2"
:category="'cfmPtPSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'To'"
:select="isSubmitCfmPtPSE?.[1].task"
:data="cfmPtPSEEndData"
class="w-1/2"
:category="'cfmPtPSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
<!-- Waiting time -->
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :title="'Start'" :select="isSubmitCfmWtEteStart?.[0].task"
:data="cfmWtEteStartData" :category="'cfmWtEteStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :title="'End'" :select="isSubmitCfmWtEteEnd?.[0].task"
:data="cfmWtEteEndData" :category="'cfmWtEteEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start'" :select="isSubmitCfmWtEteSE?.[0].task" :data="cfmWtEteSEStartData" class="w-1/2"
:category="'cfmWtEteSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'End'" :select="isSubmitCfmWtEteSE?.[1].task" :data="cfmWtEteSEEndData" class="w-1/2"
:category="'cfmWtEteSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:title="'Start'"
:select="isSubmitCfmWtEteStart?.[0].task"
:data="cfmWtEteStartData"
:category="'cfmWtEteStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:title="'End'"
:select="isSubmitCfmWtEteEnd?.[0].task"
:data="cfmWtEteEndData"
:category="'cfmWtEteEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start'"
:select="isSubmitCfmWtEteSE?.[0].task"
:data="cfmWtEteSEStartData"
class="w-1/2"
:category="'cfmWtEteSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'End'"
:select="isSubmitCfmWtEteSE?.[1].task"
:data="cfmWtEteSEEndData"
class="w-1/2"
:category="'cfmWtEteSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :title="'From'" :select="isSubmitCfmWtPStart?.[0].task" :data="cfmWtPStartData"
:category="'cfmWtPStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :title="'To'" :select="isSubmitCfmWtPEnd?.[0].task"
:data="cfmWtPEndData" :category="'cfmWtPEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'From'" :select="isSubmitCfmWtPSE?.[0].task" :data="cfmWtPSEStartData"
class="w-1/2" :category="'cfmWtPSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'To'" :select="isSubmitCfmWtPSE?.[1].task" :data="cfmWtPSEEndData"
class="w-1/2" :category="'cfmWtPSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:title="'From'"
:select="isSubmitCfmWtPStart?.[0].task"
:data="cfmWtPStartData"
:category="'cfmWtPStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:title="'To'"
:select="isSubmitCfmWtPEnd?.[0].task"
:data="cfmWtPEndData"
:category="'cfmWtPEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'From'"
:select="isSubmitCfmWtPSE?.[0].task"
:data="cfmWtPSEStartData"
class="w-1/2"
:category="'cfmWtPSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'To'"
:select="isSubmitCfmWtPSE?.[1].task"
:data="cfmWtPSEEndData"
class="w-1/2"
:category="'cfmWtPSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
<!-- Cycle time -->
<ActRadio v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :title="'Start'" :select="isSubmitCfmCtEteStart?.[0].task"
:data="cfmCtEteStartData" :category="'cfmCtEteStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :title="'End'" :select="isSubmitCfmCtEteEnd?.[0].task" :data="cfmCtEteEndData"
:category="'cfmCtEteEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'"
class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start'" :select="isSubmitCfmCtEteSE?.[0].task" :data="cfmCtEteSEStartData" class="w-1/2"
:category="'cfmCtEteSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'End'" :select="isSubmitCfmCtEteSE?.[1].task" :data="cfmCtEteSEEndData" class="w-1/2"
:category="'cfmCtEteSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:title="'Start'"
:select="isSubmitCfmCtEteStart?.[0].task"
:data="cfmCtEteStartData"
:category="'cfmCtEteStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:title="'End'"
:select="isSubmitCfmCtEteEnd?.[0].task"
:data="cfmCtEteEndData"
:category="'cfmCtEteEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start'"
:select="isSubmitCfmCtEteSE?.[0].task"
:data="cfmCtEteSEStartData"
class="w-1/2"
:category="'cfmCtEteSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'End'"
:select="isSubmitCfmCtEteSE?.[1].task"
:data="cfmCtEteSEEndData"
class="w-1/2"
:category="'cfmCtEteSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
</section>
</template>
@@ -110,31 +356,75 @@
* check result statistics.
*/
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import ActList from './ActList.vue';
import ActRadio from './ActRadio.vue';
import ActSeqDrag from './ActSeqDrag.vue';
import { ref, computed, watch } from "vue";
import { storeToRefs } from "pinia";
import { useLoadingStore } from "@/stores/loading";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
import ActList from "./ActList.vue";
import ActRadio from "./ActRadio.vue";
import ActSeqDrag from "./ActSeqDrag.vue";
const loadingStore = useLoadingStore();
const conformanceStore = useConformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore,
selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE,
cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart, cfmWtPEnd,
cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected, isEndSelected
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
conformanceTask,
cfmSeqStart,
cfmSeqEnd,
cfmPtEteStart,
cfmPtEteEnd,
cfmPtEteSE,
cfmPtPStart,
cfmPtPEnd,
cfmPtPSE,
cfmWtEteStart,
cfmWtEteEnd,
cfmWtEteSE,
cfmWtPStart,
cfmWtPEnd,
cfmWtPSE,
cfmCtEteStart,
cfmCtEteEnd,
cfmCtEteSE,
isStartSelected,
isEndSelected,
} = storeToRefs(conformanceStore);
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually',
'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE',
'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart',
'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd',
'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE',
'isSubmitShowDataSeq', 'isSubmitShowDataPtEte', 'isSubmitShowDataPtP', 'isSubmitShowDataWtEte',
'isSubmitShowDataWtP', 'isSubmitShowDataCt'
const props = defineProps([
"isSubmit",
"isSubmitTask",
"isSubmitStartAndEnd",
"isSubmitCfmSeqDirectly",
"isSubmitCfmSeqEventually",
"isSubmitDurationData",
"isSubmitCfmPtEteStart",
"isSubmitCfmPtEteEnd",
"isSubmitCfmPtEteSE",
"isSubmitCfmPtPStart",
"isSubmitCfmPtPEnd",
"isSubmitCfmPtPSE",
"isSubmitCfmWtEteStart",
"isSubmitCfmWtEteEnd",
"isSubmitCfmWtEteSE",
"isSubmitCfmWtPStart",
"isSubmitCfmWtPEnd",
"isSubmitCfmWtPSE",
"isSubmitCfmCtEteStart",
"isSubmitCfmCtEteEnd",
"isSubmitCfmCtEteSE",
"isSubmitShowDataSeq",
"isSubmitShowDataPtEte",
"isSubmitShowDataPtP",
"isSubmitShowDataWtEte",
"isSubmitShowDataWtP",
"isSubmitShowDataCt",
]);
const task = ref(null);
@@ -143,112 +433,166 @@ const taskEnd = ref(null);
// Activity sequence
const cfmSeqStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
return isEndSelected.value ? setSeqStartAndEndData(cfmSeqEnd.value, 'sources', task.value) : cfmSeqStart.value.map(i => i.label);
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataSeq.task;
return isEndSelected.value
? setSeqStartAndEndData(cfmSeqEnd.value, "sources", task.value)
: cfmSeqStart.value.map((i) => i.label);
});
const cfmSeqEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
return isStartSelected.value ? setSeqStartAndEndData(cfmSeqStart.value, 'sinks', task.value) : cfmSeqEnd.value.map(i => i.label);
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataSeq.task;
return isStartSelected.value
? setSeqStartAndEndData(cfmSeqStart.value, "sinks", task.value)
: cfmSeqEnd.value.map((i) => i.label);
});
// Processing time
const cfmPtEteStartData = computed(() => {
return cfmPtEteStart.value.map(i => i.task);
return cfmPtEteStart.value.map((i) => i.task);
});
const cfmPtEteEndData = computed(() => {
return cfmPtEteEnd.value.map(i => i.task);
return cfmPtEteEnd.value.map((i) => i.task);
});
const cfmPtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
return isEndSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'end', task.value) : setTaskData(cfmPtEteSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtEte.task;
return isEndSelected.value
? setStartAndEndData(cfmPtEteSE.value, "end", task.value)
: setTaskData(cfmPtEteSE.value, "start");
});
const cfmPtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
return isStartSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'start', task.value) : setTaskData(cfmPtEteSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtEte.task;
return isStartSelected.value
? setStartAndEndData(cfmPtEteSE.value, "start", task.value)
: setTaskData(cfmPtEteSE.value, "end");
});
const cfmPtPStartData = computed(() => {
return cfmPtPStart.value.map(i => i.task);
return cfmPtPStart.value.map((i) => i.task);
});
const cfmPtPEndData = computed(() => {
return cfmPtPEnd.value.map(i => i.task);
return cfmPtPEnd.value.map((i) => i.task);
});
const cfmPtPSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
return isEndSelected.value ? setStartAndEndData(cfmPtPSE.value, 'end', task.value) : setTaskData(cfmPtPSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtP.task;
return isEndSelected.value
? setStartAndEndData(cfmPtPSE.value, "end", task.value)
: setTaskData(cfmPtPSE.value, "start");
});
const cfmPtPSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
return isStartSelected.value ? setStartAndEndData(cfmPtPSE.value, 'start', task.value) : setTaskData(cfmPtPSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtP.task;
return isStartSelected.value
? setStartAndEndData(cfmPtPSE.value, "start", task.value)
: setTaskData(cfmPtPSE.value, "end");
});
// Waiting time
const cfmWtEteStartData = computed(() => {
return cfmWtEteStart.value.map(i => i.task);
return cfmWtEteStart.value.map((i) => i.task);
});
const cfmWtEteEndData = computed(() => {
return cfmWtEteEnd.value.map(i => i.task);
return cfmWtEteEnd.value.map((i) => i.task);
});
const cfmWtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
return isEndSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'end', task.value) : setTaskData(cfmWtEteSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtEte.task;
return isEndSelected.value
? setStartAndEndData(cfmWtEteSE.value, "end", task.value)
: setTaskData(cfmWtEteSE.value, "start");
});
const cfmWtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
return isStartSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'start', task.value) : setTaskData(cfmWtEteSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtEte.task;
return isStartSelected.value
? setStartAndEndData(cfmWtEteSE.value, "start", task.value)
: setTaskData(cfmWtEteSE.value, "end");
});
const cfmWtPStartData = computed(() => {
return cfmWtPStart.value.map(i => i.task);
return cfmWtPStart.value.map((i) => i.task);
});
const cfmWtPEndData = computed(() => {
return cfmWtPEnd.value.map(i => i.task);
return cfmWtPEnd.value.map((i) => i.task);
});
const cfmWtPSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
return isEndSelected.value ? setStartAndEndData(cfmWtPSE.value, 'end', task.value) : setTaskData(cfmWtPSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtP.task;
return isEndSelected.value
? setStartAndEndData(cfmWtPSE.value, "end", task.value)
: setTaskData(cfmWtPSE.value, "start");
});
const cfmWtPSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
return isStartSelected.value ? setStartAndEndData(cfmWtPSE.value, 'start', task.value) : setTaskData(cfmWtPSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtP.task;
return isStartSelected.value
? setStartAndEndData(cfmWtPSE.value, "start", task.value)
: setTaskData(cfmWtPSE.value, "end");
});
// Cycle time
const cfmCtEteStartData = computed(() => {
return cfmCtEteStart.value.map(i => i.task);
return cfmCtEteStart.value.map((i) => i.task);
});
const cfmCtEteEndData = computed(() => {
return cfmCtEteEnd.value.map(i => i.task);
return cfmCtEteEnd.value.map((i) => i.task);
});
const cfmCtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
return isEndSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'end', task.value) : setTaskData(cfmCtEteSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataCt.task;
return isEndSelected.value
? setStartAndEndData(cfmCtEteSE.value, "end", task.value)
: setTaskData(cfmCtEteSE.value, "start");
});
const cfmCtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
return isStartSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'start', task.value) : setTaskData(cfmCtEteSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataCt.task;
return isStartSelected.value
? setStartAndEndData(cfmCtEteSE.value, "start", task.value)
: setTaskData(cfmCtEteSE.value, "end");
});
// Watchers - Fix issue where saved rule files could not be re-edited
watch(() => props.isSubmitShowDataSeq, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataPtEte, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataPtP, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataWtEte, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataWtP, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataCt, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(
() => props.isSubmitShowDataSeq,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataPtEte,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataPtP,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataWtEte,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataWtP,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataCt,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
/**
* Sets the start and end radio data.
@@ -257,7 +601,7 @@ watch(() => props.isSubmitShowDataCt, (newValue) => {
* @returns {array}
*/
function setTaskData(data, category) {
let newData = data.map(i => i[category]);
let newData = data.map((i) => i[category]);
newData = [...new Set(newData)]; // Set is a collection type that only stores unique values.
return newData;
}
@@ -269,13 +613,15 @@ function setTaskData(data, category) {
* @returns {array}
*/
function setStartAndEndData(data, category, taskVal) {
let oppositeCategory = '';
if (category === 'start') {
oppositeCategory = 'end';
let oppositeCategory = "";
if (category === "start") {
oppositeCategory = "end";
} else {
oppositeCategory = 'start';
};
let newData = data.filter(i => i[category] === taskVal).map(i => i[oppositeCategory]);
oppositeCategory = "start";
}
let newData = data
.filter((i) => i[category] === taskVal)
.map((i) => i[oppositeCategory]);
newData = [...new Set(newData)];
return newData;
}
@@ -287,7 +633,7 @@ function setStartAndEndData(data, category, taskVal) {
* @returns {array}
*/
function setSeqStartAndEndData(data, category, taskVal) {
let newData = data.filter(i => i.label === taskVal).map(i => i[category]);
let newData = data.filter((i) => i.label === taskVal).map((i) => i[category]);
newData = [...new Set(...newData)];
return newData;
}
@@ -297,16 +643,16 @@ function setSeqStartAndEndData(data, category, taskVal) {
*/
function selectStart(e) {
taskStart.value = e;
if(isStartSelected.value === null || isStartSelected.value === true){
if (isStartSelected.value === null || isStartSelected.value === true) {
isStartSelected.value = true;
isEndSelected.value = false;
task.value = e;
taskEnd.value = null;
emitter.emit('sratrAndEndToStart', {
emitter.emit("sratrAndEndToStart", {
start: true,
end: false,
});
};
}
}
/**
* select End list's task
@@ -314,12 +660,12 @@ function selectStart(e) {
*/
function selectEnd(e) {
taskEnd.value = e;
if(isEndSelected.value === null || isEndSelected.value === true){
if (isEndSelected.value === null || isEndSelected.value === true) {
isEndSelected.value = true;
isStartSelected.value = false;
task.value = e;
taskStart.value = null;
emitter.emit('sratrAndEndToStart', {
emitter.emit("sratrAndEndToStart", {
start: false,
end: true,
});
@@ -340,22 +686,23 @@ function reset() {
* @param {boolean} data - Whether data should be restored from submission state.
*/
function setResetData(data) {
if(data) {
if(props.isSubmit) {
if (data) {
if (props.isSubmit) {
switch (selectedRuleType.value) {
case 'Activity sequence':
case "Activity sequence":
task.value = props.isSubmitShowDataSeq.task;
isStartSelected.value = props.isSubmitShowDataSeq.isStartSelected;
isEndSelected.value = props.isSubmitShowDataSeq.isEndSelected;
break;
case 'Processing time':
case "Processing time":
switch (selectedProcessScope.value) {
case 'End to end':
case "End to end":
task.value = props.isSubmitShowDataPtEte.task;
isStartSelected.value = props.isSubmitShowDataPtEte.isStartSelected;
isStartSelected.value =
props.isSubmitShowDataPtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtEte.isEndSelected;
break;
case 'Partial':
case "Partial":
task.value = props.isSubmitShowDataPtP.task;
isStartSelected.value = props.isSubmitShowDataPtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtP.isEndSelected;
@@ -364,23 +711,24 @@ function setResetData(data) {
break;
}
break;
case 'Waiting time':
case "Waiting time":
switch (selectedProcessScope.value) {
case 'End to end':
task.value = props.isSubmitShowDataWtEte.task;
isStartSelected.value = props.isSubmitShowDataWtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtEte.isEndSelected;
break;
case 'Partial':
task.value = props.isSubmitShowDataWtP.task;
isStartSelected.value = props.isSubmitShowDataWtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtP.isEndSelected;
break;
default:
break;
}
break;
case 'Cycle time':
case "End to end":
task.value = props.isSubmitShowDataWtEte.task;
isStartSelected.value =
props.isSubmitShowDataWtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtEte.isEndSelected;
break;
case "Partial":
task.value = props.isSubmitShowDataWtP.task;
isStartSelected.value = props.isSubmitShowDataWtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtP.isEndSelected;
break;
default:
break;
}
break;
case "Cycle time":
task.value = props.isSubmitShowDataCt.task;
isStartSelected.value = props.isSubmitShowDataCt.isStartSelected;
isEndSelected.value = props.isSubmitShowDataCt.isEndSelected;
@@ -395,28 +743,28 @@ function setResetData(data) {
}
// created() logic
emitter.on('isRadioChange', (data) => {
emitter.on("isRadioChange", (data) => {
setResetData(data);
});
emitter.on('isRadioSeqChange', (data) => {
emitter.on("isRadioSeqChange", (data) => {
setResetData(data);
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) {
setResetData(data);
};
}
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
emitter.on("isRadioActSeqMoreChange", (data) => {
if (data) {
setResetData(data);
};
}
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) {
setResetData(data);
};
}
});
emitter.on('reset', data => {
emitter.on("reset", (data) => {
reset();
});
</script>

View File

@@ -1,71 +1,226 @@
<template>
<div class="mt-2 mb-12" v-if="selectedRuleType === 'Activity duration' || selectedRuleType === 'Waiting time'
|| selectedRuleType === 'Processing time' || selectedRuleType === 'Cycle time'">
<p class="h2">Time Range</p>
<div class=" text-sm leading-normal">
<div
class="mt-2 mb-12"
v-if="
selectedRuleType === 'Activity duration' ||
selectedRuleType === 'Waiting time' ||
selectedRuleType === 'Processing time' ||
selectedRuleType === 'Cycle time'
"
>
<p class="h2">Time Range</p>
<div class="text-sm leading-normal">
<!-- Activity duration -->
<TimeRangeDuration
v-if="selectedRuleType === 'Activity duration'" :time="state.timeDuration" :select="isSubmitDurationTime" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Processing time -->
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmPtEteAll" :select="isSubmitTimeCfmPtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmPtEteStart" :select="isSubmitTimeCfmPtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmPtEteEnd" :select="isSubmitTimeCfmPtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmPtEteSE" :select="isSubmitTimeCfmPtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmPtPStart" :select="isSubmitTimeCfmPtPStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmPtPEnd" :select="isSubmitTimeCfmPtPEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmPtPSE" :select="isSubmitTimeCfmPtPSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Waiting time -->
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmWtEteAll" :select="isSubmitTimeCfmWtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmWtEteStart" :select="isSubmitTimeCfmWtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmWtEteEnd" :select="isSubmitTimeCfmWtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmWtEteSE" :select="isSubmitTimeCfmWtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmWtPStart" :select="isSubmitTimeCfmWtPStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmWtPEnd" :select="isSubmitTimeCfmWtPEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmWtPSE" :select="isSubmitTimeCfmWtPSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Cycle time -->
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmCtEteAll" :select="isSubmitTimeCfmCtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmCtEteStart" :select="isSubmitTimeCfmCtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmCtEteEnd" :select="isSubmitTimeCfmCtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmCtEteSE" :select="isSubmitTimeCfmCtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
v-if="selectedRuleType === 'Activity duration'"
:time="state.timeDuration"
:select="isSubmitDurationTime"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<!-- Processing time -->
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'All'
"
:time="state.timeCfmPtEteAll"
:select="isSubmitTimeCfmPtEteAll"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:time="state.timeCfmPtEteStart"
:select="isSubmitTimeCfmPtEteStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:time="state.timeCfmPtEteEnd"
:select="isSubmitTimeCfmPtEteEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:time="state.timeCfmPtEteSE"
:select="isSubmitTimeCfmPtEteSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:time="state.timeCfmPtPStart"
:select="isSubmitTimeCfmPtPStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:time="state.timeCfmPtPEnd"
:select="isSubmitTimeCfmPtPEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:time="state.timeCfmPtPSE"
:select="isSubmitTimeCfmPtPSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<!-- Waiting time -->
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'All'
"
:time="state.timeCfmWtEteAll"
:select="isSubmitTimeCfmWtEteAll"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:time="state.timeCfmWtEteStart"
:select="isSubmitTimeCfmWtEteStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:time="state.timeCfmWtEteEnd"
:select="isSubmitTimeCfmWtEteEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:time="state.timeCfmWtEteSE"
:select="isSubmitTimeCfmWtEteSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:time="state.timeCfmWtPStart"
:select="isSubmitTimeCfmWtPStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:time="state.timeCfmWtPEnd"
:select="isSubmitTimeCfmWtPEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:time="state.timeCfmWtPSE"
:select="isSubmitTimeCfmWtPSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<!-- Cycle time -->
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'All'
"
:time="state.timeCfmCtEteAll"
:select="isSubmitTimeCfmCtEteAll"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:time="state.timeCfmCtEteStart"
:select="isSubmitTimeCfmCtEteStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:time="state.timeCfmCtEteEnd"
:select="isSubmitTimeCfmCtEteEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:time="state.timeCfmCtEteSE"
:select="isSubmitTimeCfmCtEteSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
</div>
</div>
</div>
</template>
<script setup>
// The Lucia project.
@@ -80,30 +235,67 @@
* configuration with calendar inputs.
*/
import { reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import TimeRangeDuration from '@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue';
import { reactive } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
import TimeRangeDuration from "@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue";
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
cfmWtEteWhole, cfmCtEteWhole
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
conformanceAllTasks,
conformanceTask,
cfmSeqStart,
cfmSeqEnd,
cfmPtEteStart,
cfmPtEteEnd,
cfmPtEteSE,
cfmPtPStart,
cfmPtPEnd,
cfmPtPSE,
cfmWtEteStart,
cfmWtEteEnd,
cfmWtEteSE,
cfmWtPStart,
cfmWtPEnd,
cfmWtPSE,
cfmCtEteStart,
cfmCtEteEnd,
cfmCtEteSE,
cfmPtEteWhole,
cfmWtEteWhole,
cfmCtEteWhole,
} = storeToRefs(conformanceStore);
const props = defineProps(['isSubmitDurationTime', 'isSubmitTimeCfmPtEteAll', 'isSubmitTimeCfmPtEteStart',
'isSubmitTimeCfmPtEteEnd', 'isSubmitTimeCfmPtEteSE', 'isSubmitTimeCfmPtPStart',
'isSubmitTimeCfmPtPEnd', 'isSubmitTimeCfmPtPSE', 'isSubmitTimeCfmWtEteAll',
'isSubmitTimeCfmWtEteStart', 'isSubmitTimeCfmWtEteEnd', 'isSubmitTimeCfmWtEteSE',
'isSubmitTimeCfmWtPStart', 'isSubmitTimeCfmWtPEnd', 'isSubmitTimeCfmWtPSE', 'isSubmitTimeCfmCtEteAll',
'isSubmitTimeCfmCtEteStart', 'isSubmitTimeCfmCtEteEnd', 'isSubmitTimeCfmCtEteSE'
const props = defineProps([
"isSubmitDurationTime",
"isSubmitTimeCfmPtEteAll",
"isSubmitTimeCfmPtEteStart",
"isSubmitTimeCfmPtEteEnd",
"isSubmitTimeCfmPtEteSE",
"isSubmitTimeCfmPtPStart",
"isSubmitTimeCfmPtPEnd",
"isSubmitTimeCfmPtPSE",
"isSubmitTimeCfmWtEteAll",
"isSubmitTimeCfmWtEteStart",
"isSubmitTimeCfmWtEteEnd",
"isSubmitTimeCfmWtEteSE",
"isSubmitTimeCfmWtPStart",
"isSubmitTimeCfmWtPEnd",
"isSubmitTimeCfmWtPSE",
"isSubmitTimeCfmCtEteAll",
"isSubmitTimeCfmCtEteStart",
"isSubmitTimeCfmCtEteEnd",
"isSubmitTimeCfmCtEteSE",
]);
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
const emit = defineEmits(["min-total-seconds", "max-total-seconds"]);
const state = reactive({
timeDuration: null, // Activity duration
@@ -164,14 +356,14 @@ const storeRefs = {
* @param {number} e - The minimum total seconds.
*/
function minTotalSeconds(e) {
emit('min-total-seconds', e);
emit("min-total-seconds", e);
}
/**
* get min total seconds
* @param {number} e - The maximum total seconds.
*/
function maxTotalSeconds(e) {
emit('max-total-seconds', e);
emit("max-total-seconds", e);
}
/**
* get Time Range(duration)
@@ -182,35 +374,35 @@ function maxTotalSeconds(e) {
* @returns {object} {min:12, max:345}
*/
function getDurationTime(data, category, task, taskTwo) {
let result = {min:0, max:0};
let result = { min: 0, max: 0 };
switch (category) {
case 'act':
data.forEach(i => {
if(i.label === task) {
case "act":
data.forEach((i) => {
if (i.label === task) {
result = i.duration;
}
});
break;
case 'single':
data.forEach(i => {
if(i.task === task) {
case "single":
data.forEach((i) => {
if (i.task === task) {
result = i.time;
}
});
break;
case 'double':
data.forEach(i => {
if(i.start === task && i.end === taskTwo) {
case "double":
data.forEach((i) => {
if (i.start === task && i.end === taskTwo) {
result = i.time;
}
});
break;
case 'all':
case "all":
result = data;
break
break;
default:
break;
};
}
return result;
}
/**
@@ -249,148 +441,235 @@ function reset() {
}
// created() logic
emitter.on('actRadioData', (data) => {
emitter.on("actRadioData", (data) => {
const category = data.category;
const task = data.task;
const handleDoubleSelection = (startKey, endKey, timeKey, durationType) => {
state[startKey] = task;
state[timeKey] = { min: 0, max: 0 };
if (state[endKey]) {
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'double', task, state[endKey]);
}
state[startKey] = task;
state[timeKey] = { min: 0, max: 0 };
if (state[endKey]) {
state[timeKey] = getDurationTime(
storeRefs[durationType].value,
"double",
task,
state[endKey],
);
}
};
const handleSingleSelection = (key, timeKey, durationType) => {
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'single', task);
state[timeKey] = getDurationTime(
storeRefs[durationType].value,
"single",
task,
);
};
switch (category) {
// Activity duration
case 'cfmDur':
state.timeDuration = getDurationTime(conformanceAllTasks.value, 'act', task);
break;
// Processing time
case 'cfmPtEteStart':
handleSingleSelection('cfmPtEteStart', 'timeCfmPtEteStart', 'cfmPtEteStart');
break;
case 'cfmPtEteEnd':
handleSingleSelection('cfmPtEteEnd', 'timeCfmPtEteEnd', 'cfmPtEteEnd');
break;
case 'cfmPtEteSEStart':
handleDoubleSelection('selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtEteSEEnd':
handleDoubleSelection('selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtPStart':
handleSingleSelection('cfmPtPStart', 'timeCfmPtPStart', 'cfmPtPStart');
break;
case 'cfmPtPEnd':
handleSingleSelection('cfmPtPEnd', 'timeCfmPtPEnd', 'cfmPtPEnd');
break;
case 'cfmPtPSEStart':
handleDoubleSelection('selectCfmPtPSEStart', 'selectCfmPtPSEEnd', 'timeCfmPtPSE', 'cfmPtPSE');
break;
case 'cfmPtPSEEnd':
handleDoubleSelection('selectCfmPtPSEEnd', 'selectCfmPtPSEStart', 'timeCfmPtPSE', 'cfmPtPSE');
break;
// Waiting time
case 'cfmWtEteStart':
handleSingleSelection('cfmWtEteStart', 'timeCfmWtEteStart', 'cfmWtEteStart');
break;
case 'cfmWtEteEnd':
handleSingleSelection('cfmWtEteEnd', 'timeCfmWtEteEnd', 'cfmWtEteEnd');
break;
case 'cfmWtEteSEStart':
handleDoubleSelection('selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtEteSEEnd':
handleDoubleSelection('selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtPStart':
handleSingleSelection('cfmWtPStart', 'timeCfmWtPStart', 'cfmWtPStart');
break;
case 'cfmWtPEnd':
handleSingleSelection('cfmWtPEnd', 'timeCfmWtPEnd', 'cfmWtPEnd');
break;
case 'cfmWtPSEStart':
handleDoubleSelection('selectCfmWtPSEStart', 'selectCfmWtPSEEnd', 'timeCfmWtPSE', 'cfmWtPSE');
break;
case 'cfmWtPSEEnd':
handleDoubleSelection('selectCfmWtPSEEnd', 'selectCfmWtPSEStart', 'timeCfmWtPSE', 'cfmWtPSE');
break;
// Cycle time
case 'cfmCtEteStart':
handleSingleSelection('cfmCtEteStart', 'timeCfmCtEteStart', 'cfmCtEteStart');
break;
case 'cfmCtEteEnd':
handleSingleSelection('cfmCtEteEnd', 'timeCfmCtEteEnd', 'cfmCtEteEnd');
break;
case 'cfmCtEteSEStart':
handleDoubleSelection('selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
case 'cfmCtEteSEEnd':
handleDoubleSelection('selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
default:
break;
};
});
emitter.on('reset', (data) => {
reset();
});
emitter.on('isRadioChange', (data) => {
if(data) {
reset();
switch (selectedRuleType.value) {
case 'Processing time':
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
break;
case 'Waiting time':
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
break;
case 'Cycle time':
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
break;
default:
break;
};
// Activity duration
case "cfmDur":
state.timeDuration = getDurationTime(
conformanceAllTasks.value,
"act",
task,
);
break;
// Processing time
case "cfmPtEteStart":
handleSingleSelection(
"cfmPtEteStart",
"timeCfmPtEteStart",
"cfmPtEteStart",
);
break;
case "cfmPtEteEnd":
handleSingleSelection("cfmPtEteEnd", "timeCfmPtEteEnd", "cfmPtEteEnd");
break;
case "cfmPtEteSEStart":
handleDoubleSelection(
"selectCfmPtEteSEStart",
"selectCfmPtEteSEEnd",
"timeCfmPtEteSE",
"cfmPtEteSE",
);
break;
case "cfmPtEteSEEnd":
handleDoubleSelection(
"selectCfmPtEteSEEnd",
"selectCfmPtEteSEStart",
"timeCfmPtEteSE",
"cfmPtEteSE",
);
break;
case "cfmPtPStart":
handleSingleSelection("cfmPtPStart", "timeCfmPtPStart", "cfmPtPStart");
break;
case "cfmPtPEnd":
handleSingleSelection("cfmPtPEnd", "timeCfmPtPEnd", "cfmPtPEnd");
break;
case "cfmPtPSEStart":
handleDoubleSelection(
"selectCfmPtPSEStart",
"selectCfmPtPSEEnd",
"timeCfmPtPSE",
"cfmPtPSE",
);
break;
case "cfmPtPSEEnd":
handleDoubleSelection(
"selectCfmPtPSEEnd",
"selectCfmPtPSEStart",
"timeCfmPtPSE",
"cfmPtPSE",
);
break;
// Waiting time
case "cfmWtEteStart":
handleSingleSelection(
"cfmWtEteStart",
"timeCfmWtEteStart",
"cfmWtEteStart",
);
break;
case "cfmWtEteEnd":
handleSingleSelection("cfmWtEteEnd", "timeCfmWtEteEnd", "cfmWtEteEnd");
break;
case "cfmWtEteSEStart":
handleDoubleSelection(
"selectCfmWtEteSEStart",
"selectCfmWtEteSEEnd",
"timeCfmWtEteSE",
"cfmWtEteSE",
);
break;
case "cfmWtEteSEEnd":
handleDoubleSelection(
"selectCfmWtEteSEEnd",
"selectCfmWtEteSEStart",
"timeCfmWtEteSE",
"cfmWtEteSE",
);
break;
case "cfmWtPStart":
handleSingleSelection("cfmWtPStart", "timeCfmWtPStart", "cfmWtPStart");
break;
case "cfmWtPEnd":
handleSingleSelection("cfmWtPEnd", "timeCfmWtPEnd", "cfmWtPEnd");
break;
case "cfmWtPSEStart":
handleDoubleSelection(
"selectCfmWtPSEStart",
"selectCfmWtPSEEnd",
"timeCfmWtPSE",
"cfmWtPSE",
);
break;
case "cfmWtPSEEnd":
handleDoubleSelection(
"selectCfmWtPSEEnd",
"selectCfmWtPSEStart",
"timeCfmWtPSE",
"cfmWtPSE",
);
break;
// Cycle time
case "cfmCtEteStart":
handleSingleSelection(
"cfmCtEteStart",
"timeCfmCtEteStart",
"cfmCtEteStart",
);
break;
case "cfmCtEteEnd":
handleSingleSelection("cfmCtEteEnd", "timeCfmCtEteEnd", "cfmCtEteEnd");
break;
case "cfmCtEteSEStart":
handleDoubleSelection(
"selectCfmCtEteSEStart",
"selectCfmCtEteSEEnd",
"timeCfmCtEteSE",
"cfmCtEteSE",
);
break;
case "cfmCtEteSEEnd":
handleDoubleSelection(
"selectCfmCtEteSEEnd",
"selectCfmCtEteSEStart",
"timeCfmCtEteSE",
"cfmCtEteSE",
);
break;
default:
break;
}
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
reset();
};
emitter.on("reset", (data) => {
reset();
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
if(selectedActSeqMore.value === 'All') {
emitter.on("isRadioChange", (data) => {
if (data) {
reset();
switch (selectedRuleType.value) {
case "Processing time":
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, "all");
state.timeCfmPtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmPtEteAll),
);
break;
case "Waiting time":
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, "all");
state.timeCfmWtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmWtEteAll),
);
break;
case "Cycle time":
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, "all");
state.timeCfmCtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmCtEteAll),
);
break;
default:
break;
}
}
});
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) {
reset();
}
});
emitter.on("isRadioActSeqMoreChange", (data) => {
if (data) {
if (selectedActSeqMore.value === "All") {
switch (selectedRuleType.value) {
case 'Processing time':
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
case "Processing time":
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, "all");
state.timeCfmPtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmPtEteAll),
);
break;
case 'Waiting time':
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
case "Waiting time":
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, "all");
state.timeCfmWtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmWtEteAll),
);
break;
case 'Cycle time':
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
case "Cycle time":
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, "all");
state.timeCfmCtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmCtEteAll),
);
break;
default:
break;
};
}else reset();
};
}
} else reset();
}
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) {
reset();
};
}
});
</script>

View File

@@ -1,10 +1,19 @@
<template>
<ul class="space-y-2" id="cyp-conformance-result-arrow">
<li class="flex justify-start items-center pr-4" v-for="(act, index) in data" :key="index" :title="act">
<li
class="flex justify-start items-center pr-4"
v-for="(act, index) in data"
:key="index"
:title="act"
>
<span class="material-symbols-outlined text-primary mr-2">
arrow_circle_down
</span>
<p class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</p>
<p
class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>
{{ act }}
</p>
</li>
</ul>
</template>
@@ -20,5 +29,5 @@
* sequences.
*/
defineProps(['data', 'select']);
defineProps(["data", "select"]);
</script>

View File

@@ -1,10 +1,19 @@
<template>
<ul class="space-y-2" id="cyp-conformance-result-check">
<li class="flex justify-start items-center pr-4" v-for="(act, index) in datadata" :key="index" :title="act">
<li
class="flex justify-start items-center pr-4"
v-for="(act, index) in datadata"
:key="index"
:title="act"
>
<span class="material-symbols-outlined text-primary mr-2">
check_circle
</span>
<p class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</p>
<p
class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>
{{ act }}
</p>
</li>
</ul>
</template>
@@ -20,20 +29,26 @@
* matched activities.
*/
import { ref, watch } from 'vue';
import emitter from '@/utils/emitter';
import { ref, watch } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps(['data', 'select']);
const props = defineProps(["data", "select"]);
const datadata = ref(props.select);
watch(() => props.data, (newValue) => {
datadata.value = newValue;
});
watch(
() => props.data,
(newValue) => {
datadata.value = newValue;
},
);
watch(() => props.select, (newValue) => {
datadata.value = newValue;
});
watch(
() => props.select,
(newValue) => {
datadata.value = newValue;
},
);
emitter.on('reset', (val) => datadata.value = val);
emitter.on("reset", (val) => (datadata.value = val));
</script>

View File

@@ -1,9 +1,19 @@
<template>
<ul id="cyp-conformance-result-dot">
<li class="flex justify-start items-center py-1 pr-4" v-for="(act, index) in data" :key="index + act" :title="act">
<span class="material-symbols-outlined disc !text-sm align-middle mr-1">fiber_manual_record</span>
<li
class="flex justify-start items-center py-1 pr-4"
v-for="(act, index) in data"
:key="index + act"
:title="act"
>
<span class="material-symbols-outlined disc !text-sm align-middle mr-1"
>fiber_manual_record</span
>
<span class="mr-2 block w-12">{{ act.category }}</span>
<span class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden block">{{ act.task }}</span>
<span
class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden block"
>{{ act.task }}</span
>
</li>
</ul>
</template>
@@ -19,16 +29,20 @@
* and task pairs.
*/
import { ref, watch } from 'vue';
import emitter from '@/utils/emitter';
import { ref, watch } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps(['timeResultData', 'select']);
const props = defineProps(["timeResultData", "select"]);
const data = ref(props.select);
watch(() => props.timeResultData, (newValue) => {
data.value = newValue;
}, { deep: true });
watch(
() => props.timeResultData,
(newValue) => {
data.value = newValue;
},
{ deep: true },
);
emitter.on('reset', (val) => data.value = val);
emitter.on("reset", (val) => (data.value = val));
</script>

View File

@@ -1,11 +1,23 @@
<template>
<div id="timeranges_s_e_container" class="flex justify-between items-center">
<Durationjs :max="minVuemax" :min="minVuemin" :size="'min'" :updateMax="updateMax"
@total-seconds="minTotalSeconds" :value="durationMin">
<Durationjs
:max="minVuemax"
:min="minVuemin"
:size="'min'"
:updateMax="updateMax"
@total-seconds="minTotalSeconds"
:value="durationMin"
>
</Durationjs>
<span>~</span>
<Durationjs :max="maxVuemax" :min="maxVuemin" :size="'max'" :updateMin="updateMin"
@total-seconds="maxTotalSeconds" :value="durationMax">
<span>~</span>
<Durationjs
:max="maxVuemax"
:min="maxVuemin"
:size="'max'"
:updateMin="updateMin"
@total-seconds="maxTotalSeconds"
:value="durationMax"
>
</Durationjs>
</div>
</template>
@@ -22,11 +34,11 @@
* for conformance time-based rules.
*/
import { ref, watch } from 'vue';
import Durationjs from '@/components/durationjs.vue';
import { ref, watch } from "vue";
import Durationjs from "@/components/durationjs.vue";
const props = defineProps(['time', 'select']);
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
const props = defineProps(["time", "select"]);
const emit = defineEmits(["min-total-seconds", "max-total-seconds"]);
const timeData = ref({ min: 0, max: 0 });
const timeRangeMin = ref(0);
@@ -56,7 +68,7 @@ function setTimeValue() {
function minTotalSeconds(e) {
timeRangeMin.value = e;
updateMin.value = e;
emit('min-total-seconds', e);
emit("min-total-seconds", e);
}
/**
@@ -66,29 +78,33 @@ function minTotalSeconds(e) {
function maxTotalSeconds(e) {
timeRangeMax.value = e;
updateMax.value = e;
emit('max-total-seconds', e);
emit("max-total-seconds", e);
}
watch(() => props.time, (newValue, oldValue) => {
durationMax.value = null;
durationMin.value = null;
if(newValue === null) {
timeData.value = { min: 0, max: 0 };
}else if(newValue !== null) {
timeData.value = { min: newValue.min, max: newValue.max };
emit('min-total-seconds', newValue.min);
emit('max-total-seconds', newValue.max);
}
setTimeValue();
}, { deep: true, immediate: true });
watch(
() => props.time,
(newValue, oldValue) => {
durationMax.value = null;
durationMin.value = null;
if (newValue === null) {
timeData.value = { min: 0, max: 0 };
} else if (newValue !== null) {
timeData.value = { min: newValue.min, max: newValue.max };
emit("min-total-seconds", newValue.min);
emit("max-total-seconds", newValue.max);
}
setTimeValue();
},
{ deep: true, immediate: true },
);
// created
if(props.select){
if(Object.keys(props.select.base).length !== 0) {
if (props.select) {
if (Object.keys(props.select.base).length !== 0) {
timeData.value = props.select.base;
setTimeValue();
}
if(Object.keys(props.select.rule).length !== 0) {
if (Object.keys(props.select.rule).length !== 0) {
durationMin.value = props.select.rule.min;
durationMax.value = props.select.rule.max;
}

View File

@@ -1,65 +1,112 @@
<template>
<Dialog :visible="listModal" @update:visible="emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
<template #header>
<div class=" py-5">
<p class="text-base font-bold">Non-conformance Issue</p>
</div>
</template>
<div class="h-full flex items-start justify-start p-4">
<!-- Trace List -->
<section class="w-80 h-full pr-4">
<p class="h2 px-2 mb-2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 px-2 mb-2">
<span class="material-symbols-outlined !text-sm align-[-10%] mr-2">info</span>Click trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<table class="border-separate border-spacing-x-2 text-sm">
<caption class="hidden">Trace List</caption>
<thead class="sticky top-0 z-10 bg-neutral-100">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th class="h2 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id)">
<td class="p-2">#{{ trace.id }}</td>
<td class="p-2 w-24">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(trace.value)"></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}%</td>
</tr>
</tbody>
</table>
<Dialog
:visible="listModal"
@update:visible="emit('closeModal', $event)"
modal
:style="{ width: '90vw', height: '90vh' }"
:contentClass="contentClass"
>
<template #header>
<div class="py-5">
<p class="text-base font-bold">Non-conformance Issue</p>
</div>
</section>
<!-- Trace item Table -->
<section class="px-4 py-2 h-full w-[calc(100%_-_320px)] bg-neutral-10 rounded-xl">
<p class="h2 mb-2 px-4">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cfmTrace" ref="cfmTrace" class="h-full min-w-full relative"></div>
</template>
<div class="h-full flex items-start justify-start p-4">
<!-- Trace List -->
<section class="w-80 h-full pr-4">
<p class="h2 px-2 mb-2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 px-2 mb-2">
<span class="material-symbols-outlined !text-sm align-[-10%] mr-2"
>info</span
>Click trace number to see more.
</p>
<div
class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]"
>
<table class="border-separate border-spacing-x-2 text-sm">
<caption class="hidden">
Trace List
</caption>
<thead class="sticky top-0 z-10 bg-neutral-100">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th
class="h2 px-2 border-b border-neutral-500 text-start"
colspan="3"
>
Occurrences
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(trace, key) in traceList"
:key="key"
class="cursor-pointer hover:text-primary"
@click="switchCaseData(trace.id)"
>
<td class="p-2">#{{ trace.id }}</td>
<td class="p-2 w-24">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div
class="h-full bg-primary"
:style="progressWidth(trace.value)"
></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}%</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable" @scroll="handleScroll">
<DataTable :value="caseData" showGridlines tableClass="text-sm" breakpoint="0">
</section>
<!-- Trace item Table -->
<section
class="px-4 py-2 h-full w-[calc(100%_-_320px)] bg-neutral-10 rounded-xl"
>
<p class="h2 mb-2 px-4">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div
id="cfmTrace"
ref="cfmTrace"
class="h-full min-w-full relative"
></div>
</div>
</div>
<div
class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable"
@scroll="handleScroll"
>
<DataTable
:value="caseData"
showGridlines
tableClass="text-sm"
breakpoint="0"
>
<div v-for="(col, index) in columnData" :key="index">
<Column :field="col.field" :header="col.header">
<template #body="{ data }">
<div :class="data[col.field]?.length > 18 ? 'whitespace-normal' : 'whitespace-nowrap'">
<div
:class="
data[col.field]?.length > 18
? 'whitespace-normal'
: 'whitespace-nowrap'
"
>
{{ data[col.field] }}
</div>
</template>
</Column>
</div>
</DataTable>
</div>
</section>
</div>
</Dialog>
</div>
</section>
</div>
</Dialog>
</template>
<script setup>
// The Lucia project.
@@ -74,30 +121,39 @@
* results with expandable activity sequences.
*/
import { ref, computed, watch, nextTick, useTemplateRef } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
import { ref, computed, watch, nextTick, useTemplateRef } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
const props = defineProps(['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category']);
const emit = defineEmits(['closeModal']);
const props = defineProps([
"listModal",
"listNo",
"traceId",
"firstCases",
"listTraces",
"taskSeq",
"cases",
"category",
]);
const emit = defineEmits(["closeModal"]);
const conformanceStore = useConformanceStore();
const { infinite404 } = storeToRefs(conformanceStore);
// template ref
const cfmTrace = useTemplateRef('cfmTrace');
const cfmTrace = useTemplateRef("cfmTrace");
// data
const contentClass = ref('!bg-neutral-100 border-t border-neutral-300 h-full');
const contentClass = ref("!bg-neutral-100 border-t border-neutral-300 h-full");
const showTraceId = ref(null);
const infiniteData = ref(null);
const maxItems = ref(false);
const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const startNum = ref(0);
const processMap = ref({
nodes:[],
edges:[],
nodes: [],
edges: [],
});
// computed
@@ -106,23 +162,27 @@ const traceTotal = computed(() => {
});
const traceList = computed(() => {
const sum = props.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
const sum = props.listTraces
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
return props.listTraces.map(trace => {
return {
id: trace.id,
value: Number((getPercentLabel(trace.count / sum))),
count: trace.count.toLocaleString('en-US'),
count_base: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
}).sort((x, y) => x.id - y.id);
return props.listTraces
.map((trace) => {
return {
id: trace.id,
value: Number(getPercentLabel(trace.count / sum)),
count: trace.count.toLocaleString("en-US"),
count_base: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
})
.sort((x, y) => x.id - y.id);
});
const caseData = computed(() => {
if(infiniteData.value !== null){
if (infiniteData.value !== null) {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => {
data.forEach((item) => {
item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // Create a new key-value pair
});
@@ -132,47 +192,74 @@ const caseData = computed(() => {
item[`att_${index}`] = attribute.value; // Create a new key-value pair
});
delete item.attributes; // Remove the original attributes property
})
});
return data;
}
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // Deep copy the original cases data
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
const facetName = (facName) =>
facName
.trim()
.replace(
/^(.)(.*)$/,
(match, firstChar, restOfString) =>
firstChar.toUpperCase() + restOfString.toLowerCase(),
);
const result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
...data[0].facets.map((fac, index) => ({
field: `fac_${index}`,
header: facetName(fac.name),
})),
...data[0].attributes.map((att, index) => ({
field: `att_${index}`,
header: att.key,
})),
];
return result
return result;
});
// watch
watch(() => props.listModal, (newValue) => { // Draw the chart when the modal is opened for the first time
if(newValue) createCy();
});
watch(
() => props.listModal,
(newValue) => {
// Draw the chart when the modal is opened for the first time
if (newValue) createCy();
},
);
watch(() => props.taskSeq, (newValue) => {
if (newValue !== null) createCy();
});
watch(
() => props.taskSeq,
(newValue) => {
if (newValue !== null) createCy();
},
);
watch(() => props.traceId, (newValue) => {
// Update showTraceId when the traceId prop changes
showTraceId.value = newValue;
});
watch(
() => props.traceId,
(newValue) => {
// Update showTraceId when the traceId prop changes
showTraceId.value = newValue;
},
);
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && typeof isScrollTop.scrollTop !== "undefined")
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(() => props.firstCases, (newValue) => {
infiniteData.value = newValue;
});
watch(
() => props.firstCases,
(newValue) => {
infiniteData.value = newValue;
},
);
watch(infinite404, (newValue) => {
if (newValue === 404) maxItems.value = true;
@@ -184,8 +271,8 @@ watch(infinite404, (newValue) => {
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
/**
@@ -193,78 +280,93 @@ function getPercentLabel(val){
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
* switch case data
* @param {number} id case id
*/
async function switchCaseData(id) {
if(id == showTraceId.value) return;
if (id == showTraceId.value) return;
infinite404.value = null;
maxItems.value = false;
startNum.value = 0;
let result;
if(props.category === 'issue') result = await conformanceStore.getConformanceTraceDetail(props.listNo, id, 0);
else if(props.category === 'loop') result = await conformanceStore.getConformanceLoopsTraceDetail(props.listNo, id, 0);
if (props.category === "issue")
result = await conformanceStore.getConformanceTraceDetail(
props.listNo,
id,
0,
);
else if (props.category === "loop")
result = await conformanceStore.getConformanceLoopsTraceDetail(
props.listNo,
id,
0,
);
infiniteData.value = await result;
showTraceId.value = id; // Set after getDetail so the case table finishes loading before switching showTraceId
}
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData(){
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
if(props.taskSeq !== null) {
if (props.taskSeq !== null) {
props.taskSeq.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100
}
width: 100,
},
});
});
};
}
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData(){
function setEdgesData() {
processMap.value.edges = [];
if(props.taskSeq !== null) {
if (props.taskSeq !== null) {
props.taskSeq.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
style: "solid",
},
});
});
};
}
// The number of edges is one less than the number of nodes
processMap.value.edges.pop();
}
/**
* create trace cytoscape's map
*/
function createCy(){
function createCy() {
nextTick(() => {
const graphId = cfmTrace.value;
setNodesData();
setEdgesData();
if(graphId !== null) cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
if (graphId !== null)
cytoscapeMapTrace(
processMap.value.nodes,
processMap.value.edges,
graphId,
);
});
}
/**
@@ -273,12 +375,16 @@ function createCy(){
async function fetchData() {
try {
infiniteFinish.value = false;
startNum.value += 20
const result = await conformanceStore.getConformanceTraceDetail(props.listNo, showTraceId.value, startNum.value);
startNum.value += 20;
const result = await conformanceStore.getConformanceTraceDetail(
props.listNo,
showTraceId.value,
startNum.value,
);
infiniteData.value = [...infiniteData.value, ...result];
infiniteFinish.value = true;
} catch(error) {
console.error('Failed to load data:', error);
} catch (error) {
console.error("Failed to load data:", error);
}
}
/**
@@ -286,10 +392,16 @@ async function fetchData() {
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if(maxItems.value || infiniteData.value.length < 20 || infiniteFinish.value === false) return;
if (
maxItems.value ||
infiniteData.value.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
const overScrollHeight =
container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
if (overScrollHeight) fetchData();
}
@@ -298,22 +410,22 @@ function handleScroll(event) {
@reference "../../../assets/tailwind.css";
/* Progress bar color */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
@apply bg-primary;
}
/* Table set */
:deep(.p-datatable-thead) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
@apply sticky top-0 left-0 z-10 bg-neutral-10;
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
@apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500;
white-space: nowrap;
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0 text-center
@apply border-neutral-500 !border-t-0 text-center;
}
/* Center header title */
:deep(.p-column-header-content) {
@apply justify-center
@apply justify-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;

View File

@@ -1,30 +1,70 @@
<template>
<!-- Activity List -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full"
>
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">Activity List&nbsp({{ data.length }})</p>
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<table class="border-separate border-spacing-x-2 table-auto min-w-full text-sm" :class="data.length === 0? 'h-full': null">
<caption class="hidden">Activity List</caption>
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]"
>
<table
class="border-separate border-spacing-x-2 table-auto min-w-full text-sm"
:class="data.length === 0 ? 'h-full' : null"
>
<caption class="hidden">
Activity List
</caption>
<thead class="sticky top-0 left-0 z-10 bg-neutral-10">
<tr>
<th class="text-start font-semibold leading-10 px-2 border-b border-neutral-500">Activity</th>
<th class="font-semibold leading-10 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
<th
class="text-start font-semibold leading-10 px-2 border-b border-neutral-500"
>
Activity
</th>
<th
class="font-semibold leading-10 px-2 border-b border-neutral-500 text-start"
colspan="3"
>
Occurrences
</th>
</tr>
</thead>
<Draggable :list="data" :group="{ name: 'activity', pull: 'clone', put: false }" itemKey="name" tag="tbody" animation="300" @end="onEnd" :fallbackTolerance="5" :forceFallback="true" :ghostClass="'ghostSelected'" :dragClass="'dragSelected'" :sort="false">
<Draggable
:list="data"
:group="{ name: 'activity', pull: 'clone', put: false }"
itemKey="name"
tag="tbody"
animation="300"
@end="onEnd"
:fallbackTolerance="5"
:forceFallback="true"
:ghostClass="'ghostSelected'"
:dragClass="'dragSelected'"
:sort="false"
>
<template #item="{ element, index }">
<tr @dblclick="moveActItem(index, element)" :class="listSequence.includes(element) ? 'text-primary' : ''">
<tr
@dblclick="moveActItem(index, element)"
:class="listSequence.includes(element) ? 'text-primary' : ''"
>
<td class="px-4 py-2" :id="element.label">{{ element.label }}</td>
<td class="px-4 py-2 w-24">
<div class="h-4 min-w-[96px] bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(element.occ_value)"></div>
<div
class="h-4 min-w-[96px] bg-neutral-300 rounded-sm overflow-hidden"
>
<div
class="h-full bg-primary"
:style="progressWidth(element.occ_value)"
></div>
</div>
</td>
<td class="px-4 py-2 text-right">{{ element.occurrences }}</td>
<td class="px-4 py-2 text-right">{{ element.occurrence_ratio }}</td>
<td class="px-4 py-2 text-right">
{{ element.occurrence_ratio }}
</td>
</tr>
</template>
</Draggable>
@@ -32,24 +72,61 @@
</div>
</div>
<!-- Sequence -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm">
<p class="h2 border-b border-500 my-2">Sequence&nbsp({{ listSeq.length }})</p>
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm"
>
<p class="h2 border-b border-500 my-2">
Sequence&nbsp({{ listSeq.length }})
</p>
<!-- No Data -->
<div v-if="listSequence.length === 0" class="p-4 w-[calc(100%_-_32px)] h-5/6 flex justify-center items-center absolute">
<p class="text-neutral-500">Please drag and drop at least two activities here and sort.</p>
<div
v-if="listSequence.length === 0"
class="p-4 w-[calc(100%_-_32px)] h-5/6 flex justify-center items-center absolute"
>
<p class="text-neutral-500">
Please drag and drop at least two activities here and sort.
</p>
</div>
<!-- Have Data -->
<div class="py-4 m-auto w-full h-[calc(100%_-_56px)]">
<div class="w-full h-full overflow-y-auto overflow-x-auto scrollbar px-4 text-center listSequence">
<draggable class="h-full" :list="listSequence" :group="{name: 'activity'}" itemKey="name" animation="300" :forceFallback="true" :fallbackTolerance="5" :dragClass="'!opacity-100'" @start="onStart" @end="onEnd" :component-data="getComponentData()" :ghostClass="'!opacity-0'">
<div class="py-4 m-auto w-full h-[calc(100%_-_56px)]">
<div
class="w-full h-full overflow-y-auto overflow-x-auto scrollbar px-4 text-center listSequence"
>
<draggable
class="h-full"
:list="listSequence"
:group="{ name: 'activity' }"
itemKey="name"
animation="300"
:forceFallback="true"
:fallbackTolerance="5"
:dragClass="'!opacity-100'"
@start="onStart"
@end="onEnd"
:component-data="getComponentData()"
:ghostClass="'!opacity-0'"
>
<template #item="{ element, index }">
<div>
<div class="flex justify-center items-center">
<div class="w-full p-2 border border-primary rounded text-primary bg-neutral-10" @dblclick="moveSeqItem(index, element)"><span>{{ element.label }}</span>
<div
class="w-full p-2 border border-primary rounded text-primary bg-neutral-10"
@dblclick="moveSeqItem(index, element)"
>
<span>{{ element.label }}</span>
</div>
<span class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger" @click.stop.native="moveSeqItem(index, element)">close</span>
<span
class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger"
@click.stop.native="moveSeqItem(index, element)"
>close</span
>
</div>
<span v-show="index !== listSeq.length - 1 && index !== lastItemIndex - 1" class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"></span>
<span
v-show="
index !== listSeq.length - 1 && index !== lastItemIndex - 1
"
class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"
></span>
</div>
</template>
</draggable>
@@ -68,8 +145,8 @@
* rules.
*/
import { ref, computed, watch } from 'vue';
import { sortNumEngZhtwForFilter } from '@/module/sortNumEngZhtw.js';
import { ref, computed, watch } from "vue";
import { sortNumEngZhtwForFilter } from "@/module/sortNumEngZhtw.js";
const props = defineProps({
filterTaskData: {
@@ -83,10 +160,10 @@ const props = defineProps({
listSeq: {
type: Array,
required: true,
}
},
});
const emit = defineEmits(['update:listSeq']);
const emit = defineEmits(["update:listSeq"]);
const listSequence = ref([]);
const filteredData = ref(props.filterTaskData);
@@ -101,13 +178,19 @@ const data = computed(() => {
return filteredData.value;
});
watch(() => props.listSeq, (newval) => {
listSequence.value = newval;
});
watch(
() => props.listSeq,
(newval) => {
listSequence.value = newval;
},
);
watch(() => props.filterTaskData, (newval) => {
filteredData.value = newval;
});
watch(
() => props.filterTaskData,
(newval) => {
filteredData.value = newval;
},
);
/**
* Moves an activity from the list to the sequence on double-click.
@@ -129,7 +212,7 @@ function moveSeqItem(index, element) {
/** Emits the current sequence list to the parent component. */
function getComponentData() {
emit('update:listSeq', listSequence.value);
emit("update:listSeq", listSequence.value);
}
/**
@@ -138,13 +221,13 @@ function getComponentData() {
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
lastChild.style.display = "none";
// Hide the dragged element at its original position
const originalElement = evt.item;
originalElement.style.display = 'none';
originalElement.style.display = "none";
// When dragging the last element, hide the arrow of the second-to-last element
const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
if (evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
}
/**
@@ -154,12 +237,12 @@ function onStart(evt) {
function onEnd(evt) {
// Show the dragged element
const originalElement = evt.item;
originalElement.style.display = '';
originalElement.style.display = "";
// Show the arrow after drag ends, except for the last element
const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1
const listIndex = listSequence.value.length - 1;
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
lastChild.style.display = "";
}
// Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null;
@@ -168,9 +251,9 @@ function onEnd(evt) {
<style scoped>
@reference "../../../../assets/tailwind.css";
.ghostSelected {
@apply shadow-[0px_0px_100px_-10px_inset] shadow-neutral-200
@apply shadow-[0px_0px_100px_-10px_inset] shadow-neutral-200;
}
.dragSelected {
@apply shadow-[0px_0px_4px_2px] bg-neutral-10 shadow-neutral-300 !opacity-100
@apply shadow-[0px_0px_4px_2px] bg-neutral-10 shadow-neutral-300 !opacity-100;
}
</style>

View File

@@ -1,29 +1,69 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full"
>
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">{{ tableTitle }}&nbsp({{ tableData.length }})</p>
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<DataTable v-model:selection="select" :value="tableData" dataKey="label" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect">
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]"
>
<DataTable
v-model:selection="select"
:value="tableData"
dataKey="label"
breakpoint="0"
tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm"
@row-select="onRowSelect"
>
<ColumnGroup type="header">
<Row>
<Column selectionMode="single" headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10"></Column>
<Column field="label" header="Activity" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable />
<Column field="occurrences_base" header="Occurrences" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable :colspan="3" />
<Column
selectionMode="single"
headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10"
></Column>
<Column
field="label"
header="Activity"
headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10"
sortable
/>
<Column
field="occurrences_base"
header="Occurrences"
headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10"
sortable
:colspan="3"
/>
</Row>
</ColumnGroup>
<Column selectionMode="single" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column
field="label"
header="Activity"
bodyClass="break-words !py-2 !border-0"
></Column>
<Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>
<div
class="h-full bg-primary"
:style="progressWidth(slotProps.data.occ_value)"
></div>
</div>
</template>
</Column>
<Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occurrence_ratio" header="Occurrence Ratio" bodyClass="!text-right !py-2 !border-0"></Column>
<Column
field="occurrences"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occurrence_ratio"
header="Occurrence Ratio"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
</div>
@@ -38,7 +78,7 @@
* occurrences filter table with single-row radio selection.
*/
import { ref, watch } from 'vue';
import { ref, watch } from "vue";
const props = defineProps({
tableTitle: {
@@ -51,28 +91,31 @@ const props = defineProps({
},
tableSelect: {
type: [Object, Array],
default: null
default: null,
},
progressWidth: {
type: Function,
required: false,
}
},
});
const emit = defineEmits(['on-row-select']);
const emit = defineEmits(["on-row-select"]);
const select = ref(null);
const metaKey = ref(true);
watch(() => props.tableSelect, (newval) => {
select.value = newval;
});
watch(
() => props.tableSelect,
(newval) => {
select.value = newval;
},
);
/**
* Emits the selected row to the parent component.
* @param {Event} e - The row selection event.
*/
function onRowSelect(e) {
emit('on-row-select', e);
emit("on-row-select", e);
}
</script>

View File

@@ -4,36 +4,93 @@
<p class="h2">{{ tableTitle }}&nbsp({{ data.length }})</p>
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<DataTable v-model:selection="select" :value="data" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect" @row-unselect="onRowUnselect" @row-select-all="onRowSelectAll" @row-unselect-all="onRowUnelectAll">
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]"
>
<DataTable
v-model:selection="select"
:value="data"
breakpoint="0"
tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm"
@row-select="onRowSelect"
@row-unselect="onRowUnselect"
@row-select-all="onRowSelectAll"
@row-unselect-all="onRowUnelectAll"
>
<ColumnGroup type="header">
<Row>
<Column selectionMode="multiple" headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10 allCheckboxAct"></Column>
<Column field="label" header="Activity" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable />
<Column field="occurrences_base" header="Occurrences" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable :colspan="3" />
<Column field="cases_base" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" header="Cases with Activity" sortable :colspan="3" />
<Column
selectionMode="multiple"
headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10 allCheckboxAct"
></Column>
<Column
field="label"
header="Activity"
headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10"
sortable
/>
<Column
field="occurrences_base"
header="Occurrences"
headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10"
sortable
:colspan="3"
/>
<Column
field="cases_base"
headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10"
header="Cases with Activity"
sortable
:colspan="3"
/>
</Row>
</ColumnGroup>
<Column selectionMode="multiple" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column
field="label"
header="Activity"
bodyClass="break-words !py-2 !border-0"
></Column>
<Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>
<div
class="h-full bg-primary"
:style="progressWidth(slotProps.data.occ_value)"
></div>
</div>
</template>
</Column>
<Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occurrence_ratio" header="O2" bodyClass="!text-right !py-2 !border-0"></Column>
<Column
field="occurrences"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occurrence_ratio"
header="O2"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.case_value)"></div>
<div
class="h-full bg-primary"
:style="progressWidth(slotProps.data.case_value)"
></div>
</div>
</template>
</Column>
<Column field="cases" header="Cases with Activity" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="case_ratio" header="C2" bodyClass="!text-right !py-2 !border-0"></Column>
<Column
field="cases"
header="Cases with Activity"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="case_ratio"
header="C2"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
</div>
@@ -50,27 +107,35 @@
* selection.
*/
import { ref, watch } from 'vue';
import { ref, watch } from "vue";
const props = defineProps(['tableTitle', 'tableData', 'tableSelect', 'progressWidth']);
const props = defineProps([
"tableTitle",
"tableData",
"tableSelect",
"progressWidth",
]);
const emit = defineEmits(['on-row-select']);
const emit = defineEmits(["on-row-select"]);
const select = ref(null);
const data = ref(props.tableData);
watch(() => props.tableSelect, (newval) => {
select.value = newval;
});
watch(
() => props.tableSelect,
(newval) => {
select.value = newval;
},
);
/** Emits the current selection when a row is selected. */
function onRowSelect() {
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
/** Emits the current selection when a row is unselected. */
function onRowUnselect() {
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
/**
@@ -79,7 +144,7 @@ function onRowUnselect() {
*/
function onRowSelectAll(e) {
select.value = e.data;
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
/**
@@ -88,6 +153,6 @@ function onRowSelectAll(e) {
*/
function onRowUnelectAll(e) {
select.value = null;
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
</script>

View File

@@ -1,123 +1,310 @@
<template>
<section class="w-full h-full">
<p class="h2 ml-1 mb-2">Activity Select</p>
<div class="flex flex-row justify-between items-start gap-4 w-full h-[calc(100%_-_48px)]">
<!-- Attribute Name -->
<div class="basis-1/3 bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm">
<p class="h2 my-2">Attribute Name ({{ attTotal }})<span class="material-symbols-outlined !text-sm align-middle ml-2 cursor-pointer" v-tooltip.bottom="tooltip.attributeName">info</span></p>
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_56px)]">
<DataTable v-model:selection="selectedAttName" :value="filterAttrs" dataKey="key" breakpoint="0" :tableClass="tableClass" @row-select="switchAttNameRadio">
<Column selectionMode="single" :headerClass="headerModeClass" :bodyClass="bodyModeClass"></Column>
<Column field="key" header="Attribute" :headerClass="headerClass" :bodyClass="bodyClass" sortable>
<template #body="slotProps">
<div :title="slotProps.data.key" class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full">
<section class="w-full h-full">
<p class="h2 ml-1 mb-2">Activity Select</p>
<div
class="flex flex-row justify-between items-start gap-4 w-full h-[calc(100%_-_48px)]"
>
<!-- Attribute Name -->
<div
class="basis-1/3 bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm"
>
<p class="h2 my-2">
Attribute Name ({{ attTotal }})<span
class="material-symbols-outlined !text-sm align-middle ml-2 cursor-pointer"
v-tooltip.bottom="tooltip.attributeName"
>info</span
>
</p>
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_56px)]"
>
<DataTable
v-model:selection="selectedAttName"
:value="filterAttrs"
dataKey="key"
breakpoint="0"
:tableClass="tableClass"
@row-select="switchAttNameRadio"
>
<Column
selectionMode="single"
:headerClass="headerModeClass"
:bodyClass="bodyModeClass"
></Column>
<Column
field="key"
header="Attribute"
:headerClass="headerClass"
:bodyClass="bodyClass"
sortable
>
<template #body="slotProps">
<div
:title="slotProps.data.key"
class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full"
>
{{ slotProps.data.key }}
</div>
</template>
</Column>
</DataTable>
</div>
</div>
<!-- Range Selection -->
<div class="basis-2/3 bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">Range Selection {{ attRangeTotal }}</p>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 w-full h-[calc(100%_-_70px)]">
<!-- type: boolean -->
<div v-if="selectedAttName.type === 'boolean'" class="w-full">
<DataTable v-model:selection="selectedAttRange" :value="attRangeData" dataKey="id" breakpoint="0" :tableClass="tableClass" @row-select="onRowSelect" >
<ColumnGroup type="header">
<Row>
<Column selectionMode="single" :headerClass="headerModeClass" ></Column>
<Column field="value" header="Value" :headerClass="headerClass" sortable />
<Column field="freq" header="Occurrences" :headerClass="headerClass" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="single" :bodyClass="bodyModeClass"></Column>
<Column field="label" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div :title="slotProps.data.label" class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full">
{{ slotProps.data.label }}
</div>
</template>
</Column>
<Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_progress_bar)"></div>
</div>
</template>
</Column>
<Column field="occ_value" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occ_ratio" header="Occurrence Ratio" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
<!-- type: string -->
<div v-else-if="selectedAttName.type === 'string'" class="w-full">
<DataTable v-model:selection="selectedAttRange" :value="attRangeData" dataKey="id" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect" @row-unselect="onRowUnselect" @row-select-all="onRowSelectAll($event)" @row-unselect-all="onRowUnelectAll">
<ColumnGroup type="header">
<Row>
<Column selectionMode="multiple" :headerClass="headerModeClass" ></Column>
<Column field="value" header="Value" :headerClass="headerClass" sortable />
<Column field="freq" header="Occurrences" :headerClass="headerClass" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="multiple" :bodyClass="bodyModeClass"></Column>
<Column field="value" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div :title="slotProps.data.value" class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full">
{{ slotProps.data.value }}
</div>
</template>
</Column>
<Column header="Progress" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_progress_bar)"></div>
</div>
</template>
</Column>
<Column field="occ_value" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occ_ratio" header="Occurrence Ratio" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
<!-- Range Selection -->
<div
class="basis-2/3 bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full"
>
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">Range Selection {{ attRangeTotal }}</p>
</div>
<!-- type: value -->
<div v-else-if="valueTypes.includes(selectedAttName.type)" class="space-y-2 text-sm w-full">
<!-- Chart.js -->
<div class="h-3/5 relative">
<Chart type="line" :data="chartData" :options="chartOptions" class="h-30rem" id="chartCanvasId"/>
<div id="chart-mask-left" class="absolute bg-neutral-10/50"></div>
<div id="chart-mask-right" class="absolute bg-neutral-10/50"></div>
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 w-full h-[calc(100%_-_70px)]"
>
<!-- type: boolean -->
<div v-if="selectedAttName.type === 'boolean'" class="w-full">
<DataTable
v-model:selection="selectedAttRange"
:value="attRangeData"
dataKey="id"
breakpoint="0"
:tableClass="tableClass"
@row-select="onRowSelect"
>
<ColumnGroup type="header">
<Row>
<Column
selectionMode="single"
:headerClass="headerModeClass"
></Column>
<Column
field="value"
header="Value"
:headerClass="headerClass"
sortable
/>
<Column
field="freq"
header="Occurrences"
:headerClass="headerClass"
sortable
:colspan="3"
/>
</Row>
</ColumnGroup>
<Column
selectionMode="single"
:bodyClass="bodyModeClass"
></Column>
<Column field="label" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div
:title="slotProps.data.label"
class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full"
>
{{ slotProps.data.label }}
</div>
</template>
</Column>
<Column
header="Progress"
bodyClass="!py-2 !border-0 min-w-[96px]"
>
<template #body="slotProps">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div
class="h-full bg-primary"
:style="progressWidth(slotProps.data.occ_progress_bar)"
></div>
</div>
</template>
</Column>
<Column
field="occ_value"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occ_ratio"
header="Occurrence Ratio"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
<!-- Slider -->
<div class="px-2 py-3">
<Slider v-model="selectArea" :step="1" :min="0" :max="selectRange" range class="mx-2" @change="changeSelectArea($event)"/>
<!-- type: string -->
<div v-else-if="selectedAttName.type === 'string'" class="w-full">
<DataTable
v-model:selection="selectedAttRange"
:value="attRangeData"
dataKey="id"
breakpoint="0"
tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm"
@row-select="onRowSelect"
@row-unselect="onRowUnselect"
@row-select-all="onRowSelectAll($event)"
@row-unselect-all="onRowUnelectAll"
>
<ColumnGroup type="header">
<Row>
<Column
selectionMode="multiple"
:headerClass="headerModeClass"
></Column>
<Column
field="value"
header="Value"
:headerClass="headerClass"
sortable
/>
<Column
field="freq"
header="Occurrences"
:headerClass="headerClass"
sortable
:colspan="3"
/>
</Row>
</ColumnGroup>
<Column
selectionMode="multiple"
:bodyClass="bodyModeClass"
></Column>
<Column field="value" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div
:title="slotProps.data.value"
class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full"
>
{{ slotProps.data.value }}
</div>
</template>
</Column>
<Column
header="Progress"
bodyClass="!py-2 !border-0 min-w-[96px]"
>
<template #body="slotProps">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div
class="h-full bg-primary"
:style="progressWidth(slotProps.data.occ_progress_bar)"
></div>
</div>
</template>
</Column>
<Column
field="occ_value"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occ_ratio"
header="Occurrence Ratio"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
<!-- Calendar / InputNumber group -->
<div>
<div v-if="selectedAttName.type === 'date'" class="flex justify-center items-center space-x-2 w-full">
<div>
<span class="block mb-2">Start time</span>
<Calendar v-model="startTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="startMinDate" :maxDate="startMaxDate" showTime showIcon hourFormat="24" @date-select="sliderValueRange($event, 'start')" id="startCalendar" />
</div>
<span class="block mt-4">~</span>
<div>
<span class="block mb-2">End time</span>
<Calendar v-model="endTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="endMinDate" :maxDate="endMaxDate" showTime showIcon hourFormat="24" @date-select="sliderValueRange($event, 'end')" id="endCalendar"/>
</div>
<!-- type: value -->
<div
v-else-if="valueTypes.includes(selectedAttName.type)"
class="space-y-2 text-sm w-full"
>
<!-- Chart.js -->
<div class="h-3/5 relative">
<Chart
type="line"
:data="chartData"
:options="chartOptions"
class="h-30rem"
id="chartCanvasId"
/>
<div id="chart-mask-left" class="absolute bg-neutral-10/50"></div>
<div
id="chart-mask-right"
class="absolute bg-neutral-10/50"
></div>
</div>
<div v-else class="flex justify-center items-center space-x-2 w-full">
<InputNumber v-model="valueStart" :min="valueStartMin" :max="valueStartMax" :maxFractionDigits="2" inputClass="w-24 text-sm text-right" @blur="sliderValueRange($event, 'start')"></InputNumber>
<span class="block px-2">~</span>
<InputNumber v-model="valueEnd" :min="valueEndMin" :max="valueEndMax" inputClass="w-24 text-sm text-right" :maxFractionDigits="2" @blur="sliderValueRange($event, 'end')"></InputNumber>
<!-- Slider -->
<div class="px-2 py-3">
<Slider
v-model="selectArea"
:step="1"
:min="0"
:max="selectRange"
range
class="mx-2"
@change="changeSelectArea($event)"
/>
</div>
<!-- Calendar / InputNumber group -->
<div>
<div
v-if="selectedAttName.type === 'date'"
class="flex justify-center items-center space-x-2 w-full"
>
<div>
<span class="block mb-2">Start time</span>
<Calendar
v-model="startTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="startMinDate"
:maxDate="startMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderValueRange($event, 'start')"
id="startCalendar"
/>
</div>
<span class="block mt-4">~</span>
<div>
<span class="block mb-2">End time</span>
<Calendar
v-model="endTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="endMinDate"
:maxDate="endMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderValueRange($event, 'end')"
id="endCalendar"
/>
</div>
</div>
<div
v-else
class="flex justify-center items-center space-x-2 w-full"
>
<InputNumber
v-model="valueStart"
:min="valueStartMin"
:max="valueStartMax"
:maxFractionDigits="2"
inputClass="w-24 text-sm text-right"
@blur="sliderValueRange($event, 'start')"
></InputNumber>
<span class="block px-2">~</span>
<InputNumber
v-model="valueEnd"
:min="valueEndMin"
:max="valueEndMax"
inputClass="w-24 text-sm text-right"
:maxFractionDigits="2"
@blur="sliderValueRange($event, 'end')"
></InputNumber>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</section>
</template>
<script setup>
// The Lucia project.
@@ -132,24 +319,24 @@
* for filtering by attribute values.
*/
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { setLineChartData } from '@/module/setChartData.js';
import getMoment from 'moment';
import InputNumber from 'primevue/inputnumber';
import { Decimal } from 'decimal.js';
import emitter from '@/utils/emitter';
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { setLineChartData } from "@/module/setChartData.js";
import getMoment from "moment";
import InputNumber from "primevue/inputnumber";
import { Decimal } from "decimal.js";
import emitter from "@/utils/emitter";
const emit = defineEmits(['select-attribute']);
const emit = defineEmits(["select-attribute"]);
const allMapDataStore = useAllMapDataStore();
const { filterAttrs } = storeToRefs(allMapDataStore);
const selectedAttName = ref({});
const selectedAttRange = ref(null);
const valueTypes = ['int', 'float', 'date'];
const classTypes = ['boolean', 'string'];
const valueTypes = ["int", "float", "date"];
const classTypes = ["boolean", "string"];
const chartData = ref({});
const chartOptions = ref({});
const chartComplete = ref(null); // Rendered chart.js instance data
@@ -162,16 +349,19 @@ const startMaxDate = ref(null);
const endMinDate = ref(null);
const endMaxDate = ref(null);
const valueStart = ref(null); // PrimeVue InputNumber v-model
const valueEnd = ref(null); // PrimeVue InputNumber v-model
const valueEnd = ref(null); // PrimeVue InputNumber v-model
const valueStartMin = ref(null);
const valueStartMax = ref(null);
const valueEndMin = ref(null);
const valueEndMax = ref(null);
const tableClass = 'w-full h-full !border-separate !border-spacing-x-2 !table-auto text-sm';
const headerModeClass = 'w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10';
const headerClass = '!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10';
const bodyModeClass = '!p-2 !border-0';
const bodyClass = 'break-words !py-2 !border-0';
const tableClass =
"w-full h-full !border-separate !border-spacing-x-2 !table-auto text-sm";
const headerModeClass =
"w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10";
const headerClass =
"!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10";
const bodyModeClass = "!p-2 !border-0";
const bodyClass = "break-words !py-2 !border-0";
const panelProps = {
onClick: (event) => {
event.stopPropagation();
@@ -179,8 +369,9 @@ const panelProps = {
};
const tooltip = {
attributeName: {
value: 'Attributes with too many discrete values are excluded from selection. But users can still view those attributes in the DATA page.',
class: '!max-w-[212px] !text-[10px] !opacity-90',
value:
"Attributes with too many discrete values are excluded from selection. But users can still view those attributes in the DATA page.",
class: "!max-w-[212px] !text-[10px] !opacity-90",
},
};
@@ -194,7 +385,7 @@ const attRangeTotal = computed(() => {
let result = null; // Initialize the result variable with null
if (classTypes.includes(type) && attRangeData.value) {
result = `(${attRangeData.value.length})`; // Assign the length of attRangeData if it exists
result = `(${attRangeData.value.length})`; // Assign the length of attRangeData if it exists
}
return result;
});
@@ -202,7 +393,9 @@ const attRangeTotal = computed(() => {
const attRangeData = computed(() => {
let data = [];
const type = selectedAttName.value.type;
const sum = selectedAttName.value.options.map(item => item.freq).reduce((acc, cur) => acc + cur, 0);
const sum = selectedAttName.value.options
.map((item) => item.freq)
.reduce((acc, cur) => acc + cur, 0);
data = selectedAttName.value.options.map((item, index) => {
const ratio = item.freq / sum;
const result = {
@@ -211,27 +404,31 @@ const attRangeData = computed(() => {
type: type,
value: item.value,
occ_progress_bar: ratio * 100,
occ_value: item.freq.toLocaleString('en-US'),
occ_value: item.freq.toLocaleString("en-US"),
occ_ratio: getPercentLabel(ratio),
freq: item.freq
freq: item.freq,
};
result.label = null;
if (type === 'boolean') {
result.label = item.value ? 'Yes' : 'No';
if (type === "boolean") {
result.label = item.value ? "Yes" : "No";
} else {
result.label = null;
}
return result;
})
});
return data.sort((x, y) => y.freq - x.freq);
});
// Get the selected Attribute radio's numeric-type data
const valueData = computed(() => {
// filter returns an array, find returns the first matched element, so use find here.
if(valueTypes.includes(selectedAttName.value.type)){
const data = filterAttrs.value.find(item => item.type === selectedAttName.value.type && item.key === selectedAttName.value.key);
return data
if (valueTypes.includes(selectedAttName.value.type)) {
const data = filterAttrs.value.find(
(item) =>
item.type === selectedAttName.value.type &&
item.key === selectedAttName.value.key,
);
return data;
}
});
@@ -243,8 +440,8 @@ const sliderDataComputed = computed(() => {
const max = valueData.value.max;
const type = valueData.value.type;
switch (type) {
case 'dummy':
case 'date':
case "dummy":
case "date":
xAxisMin = new Date(min).getTime();
xAxisMax = new Date(max).getTime();
break;
@@ -255,25 +452,25 @@ const sliderDataComputed = computed(() => {
}
const range = xAxisMax - xAxisMin;
const step = range / selectRange.value;
let data = []
let data = [];
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + (step * i));
data.push(xAxisMin + step * i);
}
switch (type) {
case 'int':
data = data.map(value => {
case "int":
data = data.map((value) => {
let result = Math.round(value);
result = result === -0 ? 0 : result;
return result;
});
break;
case 'float':
data = data.map(value => {
case "float":
data = data.map((value) => {
let result = new Decimal(value.toFixed(2)).toNumber();
result = result === -0 ? 0 : result;
return result;
})
});
break;
default:
break;
@@ -288,25 +485,26 @@ const attValueTypeStartEnd = computed(() => {
const type = selectedAttName.value.type;
switch (type) {
case 'dummy': //sonar-qube
case 'date':
start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
case "dummy": //sonar-qube
case "date":
start = getMoment(startTime.value).format("YYYY-MM-DDTHH:mm:00");
end = getMoment(endTime.value).format("YYYY-MM-DDTHH:mm:00");
break;
default:
start = valueStart.value;
end = valueEnd.value;
break;
}
const data = { // Data to send to the backend
const data = {
// Data to send to the backend
type: type,
data: {
key: selectedAttName.value.key,
min: start,
max: end,
}
}
emit('select-attribute', data);
},
};
emit("select-attribute", data);
return [start, end];
});
@@ -317,7 +515,7 @@ const labelsData = computed(() => {
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for(let i = 0; i< numPoints; i++) {
for (let i = 0; i < numPoints; i++) {
const x = min + i * step;
data.push(x);
}
@@ -333,7 +531,7 @@ function onRowSelect() {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data);
emit("select-attribute", data);
}
/**
@@ -345,7 +543,7 @@ function onRowUnselect() {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data);
emit("select-attribute", data);
}
/**
@@ -359,7 +557,7 @@ function onRowSelectAll(e) {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data);
emit("select-attribute", data);
}
/**
@@ -372,7 +570,7 @@ function onRowUnelectAll() {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data)
emit("select-attribute", data);
}
/**
@@ -385,14 +583,15 @@ function switchAttNameRadio(e) {
endTime.value = null;
valueStart.value = null;
valueEnd.value = null;
if(valueData.value) { // Switch Attribute Name
if (valueData.value) {
// Switch Attribute Name
// Initialize two-way bindings
selectArea.value = [0, selectRange.value];
const min = valueData.value.min;
const max = valueData.value.max;
switch (selectedAttName.value.type) {
case 'dummy': //sonar-qube
case 'date':
case "dummy": //sonar-qube
case "date":
// Clear two-way bindings except for date
valueStart.value = null;
valueEnd.value = null;
@@ -431,8 +630,8 @@ function switchAttNameRadio(e) {
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
@@ -440,8 +639,8 @@ function progressWidth(value){
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
@@ -461,7 +660,7 @@ function resizeMask(chart) {
* @param {object} chart - The Chart.js instance data.
*/
function resizeLeftMask(chart, from) {
const canvas = document.querySelector('#chartCanvasId canvas');
const canvas = document.querySelector("#chartCanvasId canvas");
const mask = document.getElementById("chart-mask-left");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`;
mask.style.width = `${chart.chartArea.width * from}px`;
@@ -474,7 +673,7 @@ function resizeLeftMask(chart, from) {
* @param {object} chart - The Chart.js instance data.
*/
function resizeRightMask(chart, to) {
const canvas = document.querySelector('#chartCanvasId canvas');
const canvas = document.querySelector("#chartCanvasId canvas");
const mask = document.getElementById("chart-mask-right");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`;
mask.style.width = `${chart.chartArea.width * (1 - to)}px`;
@@ -488,33 +687,37 @@ function resizeRightMask(chart, to) {
function createChart() {
const vData = valueData.value;
const max = vData.chart.y_axis.max * 1.1;
const data = setLineChartData(vData.chart.data, vData.chart.x_axis.max, vData.chart.x_axis.min);
const isDateType = vData.type === 'date';
const data = setLineChartData(
vData.chart.data,
vData.chart.x_axis.max,
vData.chart.x_axis.min,
);
const isDateType = vData.type === "date";
const minX = vData.chart.x_axis.min;
const maxX = vData.chart.x_axis.max;
let setChartData= {};
let setChartOptions= {};
let setChartData = {};
let setChartOptions = {};
let setLabels = [];
switch (vData.type) {
case 'int':
setLabels = data.map(item => Math.round(item.x));
case "int":
setLabels = data.map((item) => Math.round(item.x));
break;
case 'float':
case "float":
setLabels = data.map((item, index) => {
let x;
if (index === 0) {
x = Math.floor(item.x * 100) / 100;
x = Math.floor(item.x * 100) / 100;
} else if (index === data.length - 1) {
item.x = Math.ceil(item.x * 100) / 100;
x = item.x;
item.x = Math.ceil(item.x * 100) / 100;
x = item.x;
} else {
x = Math.round(item.x * 100) / 100;
x = Math.round(item.x * 100) / 100;
}
return x
return x;
});
break;
case 'date':
case "date":
setLabels = labelsData.value;
break;
default:
@@ -523,14 +726,14 @@ function createChart() {
setChartData = {
datasets: [
{
label: 'Attribute Value',
label: "Attribute Value",
data: data,
fill: 'start',
fill: "start",
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
backgroundColor: "rgba(0,153,255)",
pointRadius: 0,
}
},
],
labels: setLabels,
};
@@ -542,20 +745,20 @@ function createChart() {
top: 16,
left: 8,
right: 8,
}
},
},
plugins: {
legend: false, // Hide legend
filler: {
propagate: false
propagate: false,
},
title: false
title: false,
},
animation: {
onComplete: e => {
onComplete: (e) => {
chartComplete.value = e.chart;
resizeMask(e.chart);
}
},
},
interaction: {
intersect: true,
@@ -564,59 +767,60 @@ function createChart() {
y: {
beginAtZero: true, // Scale includes 0
max: max,
ticks: { // Set tick intervals
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
}
},
},
},
};
if(isDateType) {
if (isDateType) {
setChartOptions.scales.x = {
type: 'time',
type: "time",
ticks: {
min: minX,
max: maxX,
autoSkip: true, // Automatically determine whether to convert time units
maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155',
color: "#334155",
display: true,
source: 'labels', // Flexibly display label count proportionally
source: "labels", // Flexibly display label count proportionally
},
grid: {
display: false, // Hide x-axis grid lines
},
time: {
minUnit: 'day', // Minimum display unit
minUnit: "day", // Minimum display unit
// displayFormats: {
// minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d',
// }
}
}
},
};
} else {
setChartOptions.scales.x = {
bounds: 'data',
type: 'linear',
bounds: "data",
type: "linear",
min: minX,
max: maxX,
ticks: {
autoSkip: true,
maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155',
callback: ((value, index, values) => {
color: "#334155",
callback: (value, index, values) => {
let x;
switch (vData.type) {
case 'int':
case "int":
return Math.round(value);
case 'float':
case "float":
switch (index) {
case 0:
x = Math.floor(value * 100) / 100;
@@ -629,15 +833,17 @@ function createChart() {
}
// Handle scientific notation and other format conversions
// Decimal cannot handle numbers exceeding 16 digits
x = new Intl.NumberFormat(undefined, {useGrouping: false}).format(x);
return x
x = new Intl.NumberFormat(undefined, {
useGrouping: false,
}).format(x);
return x;
}
})
},
},
grid: {
display: false, // Hide x-axis grid lines
},
}
};
}
chartData.value = setChartData;
chartOptions.value = setChartOptions;
@@ -654,8 +860,8 @@ function changeSelectArea(e) {
const end = sliderData[e[1].toFixed()]; // Get the index, which must be an integer.
switch (selectedAttName.value.type) {
case 'dummy':
case 'date':
case "dummy":
case "date":
startTime.value = new Date(start);
endTime.value = new Date(end);
// Reset the start/end calendar selection range
@@ -684,48 +890,56 @@ function changeSelectArea(e) {
function sliderValueRange(e, direction) {
// Find the closest index; time format: millisecond timestamps
const sliderData = sliderDataComputed.value;
const isDateType = selectedAttName.value.type === 'date';
const isDateType = selectedAttName.value.type === "date";
let targetTime = [];
let inputValue;
if(isDateType) targetTime = [new Date(attValueTypeStartEnd.value[0]).getTime(), new Date(attValueTypeStartEnd.value[1]).getTime()];
else targetTime = [attValueTypeStartEnd.value[0], attValueTypeStartEnd.value[1]]
if (isDateType)
targetTime = [
new Date(attValueTypeStartEnd.value[0]).getTime(),
new Date(attValueTypeStartEnd.value[1]).getTime(),
];
else
targetTime = [attValueTypeStartEnd.value[0], attValueTypeStartEnd.value[1]];
const closestIndexes = targetTime.map(target => {
const closestIndexes = targetTime.map((target) => {
let closestIndex = 0;
closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length;
closestIndex =
((target - sliderData[0]) /
(sliderData[sliderData.length - 1] - sliderData[0])) *
sliderData.length;
let result = Math.round(Math.abs(closestIndex));
result = result > selectRange.value ? selectRange.value : result;
return result
return result;
});
// Update the slider
selectArea.value = closestIndexes;
// Reset the start/end calendar selection range
if(!isDateType) inputValue = Number(e.value.replace(/,/g, '')) ;
if(direction === 'start') {
if(isDateType){
if (!isDateType) inputValue = Number(e.value.replace(/,/g, ""));
if (direction === "start") {
if (isDateType) {
endMinDate.value = e;
} else {
valueEndMin.value = inputValue;
}
}
else if(direction === 'end') {
if(isDateType) {
} else if (direction === "end") {
if (isDateType) {
startMaxDate.value = e;
} else {
valueStartMax.value = inputValue;
};
}
}
// Recalculate the chart mask
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chartComplete.value);
if (!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1]))
resizeMask(chartComplete.value);
else return;
}
// created() equivalent
emitter.on('map-filter-reset', value => {
if(value) {
emitter.on("map-filter-reset", (value) => {
if (value) {
selectedAttRange.value = null;
if(valueData.value && valueTypes.includes(selectedAttName.value.type)){
if (valueData.value && valueTypes.includes(selectedAttName.value.type)) {
const min = valueData.value.min;
const max = valueData.value.max;
startTime.value = new Date(min);
@@ -745,12 +959,12 @@ onMounted(() => {
onBeforeUnmount(() => {
selectedAttName.value = {};
emitter.off('map-filter-reset');
emitter.off("map-filter-reset");
});
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
:deep(table tbody td:nth-child(2)) {
@apply whitespace-nowrap break-keep overflow-hidden text-ellipsis max-w-0
@apply whitespace-nowrap break-keep overflow-hidden text-ellipsis max-w-0;
}
</style>

View File

@@ -1,41 +1,75 @@
<template>
<div class=" w-full h-full">
<div class="h-[calc(100%_-_58px)] border-b border-neutral-400 mb-2 scrollbar overflow-x-hidden overflow-y-auto">
<div v-if="this.temporaryData.length === 0" class="h-full flex justify-center items-center">
<span class="text-neutral-500">No Filter.</span>
</div>
<div v-else>
<div class="text-primary h2 flex items-center justify-start my-4">
<span class="material-symbols-outlined m-2">info</span>
<p>Disabled filters will not be saved.</p>
</div>
<Timeline :value="ruleData">
<template #content="rule">
<div class="border-b border-neutral-300 flex justify-between items-center space-x-2">
<!-- content -->
<div class="pl-2 mb-2">
<p class="text-sm font-medium leading-5">{{ rule.item.type }}:&nbsp;<span class="text-neutral-500">{{ rule.item.label }}</span></p>
</div>
<!-- button -->
<div class="min-w-fit">
<InputSwitch v-model="rule.item.toggle" @input="isRule($event, rule.index)"/>
<button type="button" class="m-2 focus:ring focus:ring-danger/20 text-neutral-500 hover:text-danger" @click.stop="deleteRule(rule.index)">
<span class="material-symbols-outlined">delete</span>
</button>
</div>
</div>
</template>
</Timeline>
</div>
<div class="w-full h-full">
<div
class="h-[calc(100%_-_58px)] border-b border-neutral-400 mb-2 scrollbar overflow-x-hidden overflow-y-auto"
>
<div
v-if="this.temporaryData.length === 0"
class="h-full flex justify-center items-center"
>
<span class="text-neutral-500">No Filter.</span>
</div>
<!-- Button -->
<div>
<div class="float-right space-x-4 px-4 py-2">
<button type="button" class="btn btn-sm " :class="[ temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']" :disabled="temporaryData.length === 0" @click="deleteRule('all')">Delete All</button>
<button type="button" class="btn btn-sm" :class="[ temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']" :disabled="temporaryData.length === 0" @click="submitAll">Apply All</button>
<div v-else>
<div class="text-primary h2 flex items-center justify-start my-4">
<span class="material-symbols-outlined m-2">info</span>
<p>Disabled filters will not be saved.</p>
</div>
<Timeline :value="ruleData">
<template #content="rule">
<div
class="border-b border-neutral-300 flex justify-between items-center space-x-2"
>
<!-- content -->
<div class="pl-2 mb-2">
<p class="text-sm font-medium leading-5">
{{ rule.item.type }}:&nbsp;<span class="text-neutral-500">{{
rule.item.label
}}</span>
</p>
</div>
<!-- button -->
<div class="min-w-fit">
<InputSwitch
v-model="rule.item.toggle"
@input="isRule($event, rule.index)"
/>
<button
type="button"
class="m-2 focus:ring focus:ring-danger/20 text-neutral-500 hover:text-danger"
@click.stop="deleteRule(rule.index)"
>
<span class="material-symbols-outlined">delete</span>
</button>
</div>
</div>
</template>
</Timeline>
</div>
</div>
<!-- Button -->
<div>
<div class="float-right space-x-4 px-4 py-2">
<button
type="button"
class="btn btn-sm"
:class="[temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']"
:disabled="temporaryData.length === 0"
@click="deleteRule('all')"
>
Delete All
</button>
<button
type="button"
class="btn btn-sm"
:class="[temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']"
:disabled="temporaryData.length === 0"
@click="submitAll"
>
Apply All
</button>
</div>
</div>
</div>
</template>
<script setup>
@@ -49,30 +83,37 @@
* apply-all actions.
*/
import { storeToRefs } from 'pinia';
import { useToast } from 'vue-toast-notification';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import { delaySecond, } from '@/utils/timeUtil.js';
import { storeToRefs } from "pinia";
import { useToast } from "vue-toast-notification";
import { useLoadingStore } from "@/stores/loading";
import { useAllMapDataStore } from "@/stores/allMapData";
import { delaySecond } from "@/utils/timeUtil.js";
const emit = defineEmits(['submit-all']);
const emit = defineEmits(["submit-all"]);
const $toast = useToast();
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, tempFilterId } = storeToRefs(allMapDataStore);
const {
hasResultRule,
temporaryData,
postRuleData,
ruleData,
isRuleData,
tempFilterId,
} = storeToRefs(allMapDataStore);
/**
* Toggles a filter rule on or off.
* @param {boolean} e - Whether the rule is enabled.
* @param {number} index - The rule index.
*/
function isRule(e, index){
function isRule(e, index) {
const rule = isRuleData.value[index];
// First get the rule object
// To preserve data order, set the value to 0 and remove it during submitAll
if(!e) temporaryData.value[index] = 0;
if (!e) temporaryData.value[index] = 0;
else temporaryData.value[index] = rule;
}
@@ -81,20 +122,20 @@ function isRule(e, index){
* @param {number|string} index - The rule index, or 'all' to delete all.
*/
async function deleteRule(index) {
if(index === 'all') {
if (index === "all") {
temporaryData.value = [];
isRuleData.value = [];
ruleData.value = [];
if(tempFilterId.value) {
if (tempFilterId.value) {
isLoading.value = true;
tempFilterId.value = await null;
await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace needs to update in sync
await emit('submit-all');
await emit("submit-all");
isLoading.value = false;
}
$toast.success('Filter(s) deleted.');
}else{
$toast.success("Filter(s) deleted.");
} else {
$toast.success(`Filter deleted.`);
temporaryData.value.splice(index, 1);
isRuleData.value.splice(index, 1);
@@ -104,23 +145,23 @@ async function deleteRule(index) {
/** Submits all enabled filter rules and refreshes the map data. */
async function submitAll() {
postRuleData.value = temporaryData.value.filter(item => item !== 0); // Get submit data; if toggle buttons are used, find and remove items set to 0
if(!postRuleData.value?.length) return $toast.error('Not selected');
postRuleData.value = temporaryData.value.filter((item) => item !== 0); // Get submit data; if toggle buttons are used, find and remove items set to 0
if (!postRuleData.value?.length) return $toast.error("Not selected");
await allMapDataStore.checkHasResult(); // Quick backend check for results
if(hasResultRule.value === null) {
if (hasResultRule.value === null) {
return;
} else if(hasResultRule.value) {
} else if (hasResultRule.value) {
isLoading.value = true;
await allMapDataStore.addTempFilterId();
await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace needs to update in sync
if(temporaryData.value[0]?.type) {
if (temporaryData.value[0]?.type) {
allMapDataStore.traceId = await allMapDataStore.traces[0]?.id;
}
await emit('submit-all');
await emit("submit-all");
isLoading.value = false;
$toast.success('Filter(s) applied.');
$toast.success("Filter(s) applied.");
return;
}
@@ -128,7 +169,7 @@ async function submitAll() {
isLoading.value = true;
await delaySecond(1);
isLoading.value = false;
$toast.warning('No result.');
$toast.warning("No result.");
}
</script>
@@ -136,21 +177,21 @@ async function submitAll() {
@reference "../../../../assets/tailwind.css";
/* TimeLine */
:deep(.p-timeline) {
@apply leading-none my-4
@apply leading-none my-4;
}
:deep(.p-timeline-event-opposite) {
@apply hidden
@apply hidden;
}
:deep(.p-timeline-event-separator) {
@apply mx-4
@apply mx-4;
}
:deep(.p-timeline-event-marker) {
@apply !bg-primary !border-primary !w-2 !h-2
@apply !bg-primary !border-primary !w-2 !h-2;
}
:deep(.p-timeline-event-connector) {
@apply !bg-primary my-2 !w-[1px]
@apply !bg-primary my-2 !w-[1px];
}
:deep(.p-timeline-event-content) {
@apply !px-0
@apply !px-0;
}
</style>

View File

@@ -1,5 +1,7 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full"
>
<section class="pt-2 pb-20 space-y-2 text-sm min-w-[48%] h-full">
<p class="h2">Range Selection</p>
<div class="text-primary h2 flex items-center justify-start">
@@ -12,18 +14,48 @@
<div id="chart-mask-right" class="absolute bg-neutral-10/50"></div>
</div>
<div class="px-2 py-3">
<Slider v-model="selectArea" :step="1" :min="0" :max="selectRange" range class="mx-2" @change="changeSelectArea($event)"/>
<Slider
v-model="selectArea"
:step="1"
:min="0"
:max="selectRange"
range
class="mx-2"
@change="changeSelectArea($event)"
/>
</div>
<!-- Calendar group -->
<div class="flex justify-center items-center space-x-2 w-full">
<div>
<span class="block mb-2">Start time</span>
<Calendar v-model="startTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="startMinDate" :maxDate="startMaxDate" showTime showIcon hourFormat="24" @date-select="sliderTimeRange($event, 'start')" id="startCalendar"/>
<Calendar
v-model="startTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="startMinDate"
:maxDate="startMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderTimeRange($event, 'start')"
id="startCalendar"
/>
</div>
<span class="block mt-4">~</span>
<div>
<span class="block mb-2">End time</span>
<Calendar v-model="endTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="endMinDate" :maxDate="endMaxDate" showTime showIcon hourFormat="24" @date-select="sliderTimeRange($event, 'end')" id="endCalendar"/>
<Calendar
v-model="endTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="endMinDate"
:maxDate="endMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderTimeRange($event, 'end')"
id="endCalendar"
/>
</div>
</div>
<!-- End calendar group -->
@@ -41,14 +73,14 @@
* duration range selectors.
*/
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { Chart, registerables } from 'chart.js';
import 'chartjs-adapter-moment';
import getMoment from 'moment';
import { ref, computed, watch, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { Chart, registerables } from "chart.js";
import "chartjs-adapter-moment";
import getMoment from "moment";
const props = defineProps(['selectValue']);
const props = defineProps(["selectValue"]);
const allMapDataStore = useAllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
@@ -71,8 +103,8 @@ const panelProps = ref({
// user select time start and end
const timeFrameStartEnd = computed(() => {
const start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
const end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
const start = getMoment(startTime.value).format("YYYY-MM-DDTHH:mm:00");
const end = getMoment(endTime.value).format("YYYY-MM-DDTHH:mm:00");
selectTimeFrame.value = [start, end]; // Data to send to the backend
return [start, end];
@@ -84,10 +116,10 @@ const sliderData = computed(() => {
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
const range = xAxisMax - xAxisMin;
const step = range / selectRange.value;
const data = []
const data = [];
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + (step * i));
data.push(xAxisMin + step * i);
}
return data;
@@ -95,7 +127,7 @@ const sliderData = computed(() => {
// Add the minimum and maximum values
const timeFrameData = computed(() => {
const data = filterTimeframe.value.data.map(i=>({x:i.x,y:i.y}))
const data = filterTimeframe.value.data.map((i) => ({ x: i.x, y: i.y }));
// See ./public/timeFrameSlope for the y-axis slope calculation diagram
// x values are 0 ~ 11,
// Name three coordinates (ax, ay), (bx, by), (cx, cy) as (a, b), (c, d), (e, f)
@@ -109,8 +141,8 @@ const timeFrameData = computed(() => {
const d = filterTimeframe.value.data[0].y;
const e = 2;
const f = filterTimeframe.value.data[1].y;
b = (e*d - a*d - f*a - f*c) / (e - c - a);
if(b < 0) {
b = (e * d - a * d - f * a - f * c) / (e - c - a);
if (b < 0) {
b = 0;
}
// Y-axis maximum value
@@ -119,8 +151,8 @@ const timeFrameData = computed(() => {
const mc = 10;
const md = filterTimeframe.value.data[9].y;
const me = 11;
let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
if(mf < 0) {
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
if (mf < 0) {
mf = 0;
}
@@ -128,12 +160,12 @@ const timeFrameData = computed(() => {
data.unshift({
x: filterTimeframe.value.x_axis.min_base,
y: b,
})
});
// Add the maximum value
data.push({
x: filterTimeframe.value.x_axis.max_base,
y: mf,
})
});
return data;
});
@@ -144,7 +176,7 @@ const labelsData = computed(() => {
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for(let i = 0; i< numPoints; i++) {
for (let i = 0; i < numPoints; i++) {
const x = min + i * step;
data.push(x);
}
@@ -152,7 +184,7 @@ const labelsData = computed(() => {
});
watch(selectTimeFrame, (newValue, oldValue) => {
if(newValue.length === 0) {
if (newValue.length === 0) {
startTime.value = new Date(filterTimeframe.value.x_axis.min);
endTime.value = new Date(filterTimeframe.value.x_axis.max);
selectArea.value = [0, selectRange.value];
@@ -167,7 +199,7 @@ watch(selectTimeFrame, (newValue, oldValue) => {
function resizeMask(chartInstance) {
const from = (selectArea.value[0] * 0.01) / (selectRange.value * 0.01);
const to = (selectArea.value[1] * 0.01) / (selectRange.value * 0.01);
if(props.selectValue[0] === 'Timeframes') {
if (props.selectValue[0] === "Timeframes") {
resizeLeftMask(chartInstance, from);
resizeRightMask(chartInstance, to);
}
@@ -208,20 +240,20 @@ function createChart() {
const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
const data = {
labels:labelsData.value,
labels: labelsData.value,
datasets: [
{
label: 'Case',
label: "Case",
data: timeFrameData.value,
fill: 'start',
fill: "start",
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
backgroundColor: "rgba(0,153,255)",
pointRadius: 0,
x: 'x',
y: 'y',
}
]
x: "x",
y: "y",
},
],
};
const options = {
responsive: true,
@@ -231,66 +263,67 @@ function createChart() {
top: 16,
left: 8,
right: 8,
}
},
},
plugins: {
legend: false, // Hide legend
filler: {
propagate: false
propagate: false,
},
title: false
title: false,
},
// animations: false, // Disable animations
animation: {
onComplete: e => {
onComplete: (e) => {
resizeMask(e.chart);
}
},
},
interaction: {
intersect: true,
},
scales: {
x: {
type: 'time',
type: "time",
min: minX,
max: maxX,
ticks: {
autoSkip: true,
maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155',
color: "#334155",
display: true,
source: 'labels',
source: "labels",
},
grid: {
display: false, // Hide x-axis grid lines
},
time: {
minUnit: 'day', // Minimum display unit
minUnit: "day", // Minimum display unit
// displayFormats: {
// minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d',
// }
}
},
},
y: {
beginAtZero: true, // Scale includes 0
max: max,
ticks: { // Set tick intervals
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
}
},
},
},
};
const config = {
type: 'line',
const config = {
type: "line",
data: data,
options: options,
};
@@ -327,22 +360,29 @@ function changeSelectArea(e) {
function sliderTimeRange(e, direction) {
// Find the closest index; time format: millisecond timestamps
const sliderDataVal = sliderData.value;
const targetTime = [new Date(timeFrameStartEnd.value[0]).getTime(), new Date(timeFrameStartEnd.value[1]).getTime()];
const closestIndexes = targetTime.map(target => {
const targetTime = [
new Date(timeFrameStartEnd.value[0]).getTime(),
new Date(timeFrameStartEnd.value[1]).getTime(),
];
const closestIndexes = targetTime.map((target) => {
let closestIndex = 0;
closestIndex = ((target - sliderDataVal[0])/(sliderDataVal[sliderDataVal.length-1]-sliderDataVal[0])) * sliderDataVal.length;
closestIndex =
((target - sliderDataVal[0]) /
(sliderDataVal[sliderDataVal.length - 1] - sliderDataVal[0])) *
sliderDataVal.length;
let result = Math.round(Math.abs(closestIndex));
result = result > selectRange.value ? selectRange.value : result;
return result
return result;
});
// Update the slider
selectArea.value = closestIndexes;
// Reset the start/end calendar selection range
if(direction === 'start') endMinDate.value = e;
else if(direction === 'end') startMaxDate.value = e;
if (direction === "start") endMinDate.value = e;
else if (direction === "end") startMaxDate.value = e;
// Recalculate the chart mask
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chart.value);
if (!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1]))
resizeMask(chart.value);
else return;
}

View File

@@ -1,5 +1,7 @@
<template>
<div class="flex justify-between items-start bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full space-x-4 overflow-y-auto overflow-x-auto scrollbar">
<div
class="flex justify-between items-start bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full space-x-4 overflow-y-auto overflow-x-auto scrollbar"
>
<!-- Range Selection -->
<section class="py-2 space-y-2 text-sm min-w-[48%] h-full">
<p class="h2">Range Selection</p>
@@ -7,32 +9,64 @@
<span class="material-symbols-outlined mr-2 !text-base">info</span>
<p>Select a percentage range.</p>
</div>
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-2/5" />
<Chart
type="bar"
:data="chartData"
:options="chartOptions"
class="h-2/5"
/>
<div class="px-2">
<p class="py-4">Select percentage of case <span class=" float-right">{{ caseTotalPercent }}%</span></p>
<Slider v-model="selectArea" :step="1" :min="0" :max="traceTotal" range class="mx-2" />
<p class="py-4">
Select percentage of case
<span class="float-right">{{ caseTotalPercent }}%</span>
</p>
<Slider
v-model="selectArea"
:step="1"
:min="0"
:max="traceTotal"
range
class="mx-2"
/>
</div>
</section>
<!-- Trace List -->
<section class="h-full min-w-[48%] py-2 space-y-2">
<p class="h2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 flex items-center justify-start">
<span class="material-symbols-outlined mr-2 !text-base">info</span>Click trace number to see more.
<span class="material-symbols-outlined mr-2 !text-base">info</span>Click
trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<div
class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]"
>
<table class="border-separate border-spacing-x-2 text-sm w-full">
<caption class="hidden">Trace list</caption>
<caption class="hidden">
Trace list
</caption>
<thead class="sticky top-0 z-10 bg-neutral-10">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th class="h2 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
<th
class="h2 px-2 border-b border-neutral-500 text-start"
colspan="3"
>
Occurrences
</th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id, trace.base_count)">
<tr
v-for="(trace, key) in traceList"
:key="key"
class="cursor-pointer hover:text-primary"
@click="switchCaseData(trace.id, trace.base_count)"
>
<td class="p-2 text-center">#{{ trace.id }}</td>
<td class="p-2 min-w-[96px]">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div class="h-full bg-primary" :style="trace.value"></div>
</div>
</td>
@@ -48,16 +82,34 @@
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
<div
id="cyTrace"
ref="cyTraceRef"
class="h-full min-w-full relative"
></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable" @scroll="handleScroll">
<DataTable :value="caseData" showGridlines tableClass="text-sm" breakpoint="0">
<div
class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable"
@scroll="handleScroll"
>
<DataTable
:value="caseData"
showGridlines
tableClass="text-sm"
breakpoint="0"
>
<div v-for="(col, index) in columnData" :key="index">
<Column :field="col.field" :header="col.header">
<template #body="{ data }">
<div :class="data[col.field]?.length > 18 ? 'whitespace-normal' : 'whitespace-nowrap'">
{{ data[col.field] }}
<div
:class="
data[col.field]?.length > 18
? 'whitespace-normal'
: 'whitespace-nowrap'
"
>
{{ data[col.field] }}
</div>
</template>
</Column>
@@ -65,7 +117,7 @@
</DataTable>
</div>
</section>
</div>
</div>
</template>
<script setup>
@@ -79,22 +131,28 @@
* trace detail display.
*/
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useLoadingStore } from '@/stores/loading';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
import { ref, computed, watch, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useLoadingStore } from "@/stores/loading";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
const emit = defineEmits(['filter-trace-selectArea']);
const emit = defineEmits(["filter-trace-selectArea"]);
const allMapDataStore = useAllMapDataStore();
const loadingStore = useLoadingStore();
const { infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases } = storeToRefs(allMapDataStore);
const {
infinit404,
baseInfiniteStart,
baseTraces,
baseTraceTaskSeq,
baseCases,
} = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);
const processMap = ref({
nodes:[],
edges:[],
nodes: [],
edges: [],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
@@ -111,95 +169,113 @@ const traceTotal = computed(() => {
defineExpose({ selectArea, showTraceId, traceTotal });
const traceCountTotal = computed(() => {
return baseTraces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
return baseTraces.value
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
});
const traceList = computed(() => {
return baseTraces.value.map(trace => {
return {
id: trace.id,
value: progressWidth(Number(((trace.count / traceCountTotal.value) * 100).toFixed(1))),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: getPercentLabel(trace.count / traceCountTotal.value),
};
}).slice(selectArea.value[0], selectArea.value[1]);
return baseTraces.value
.map((trace) => {
return {
id: trace.id,
value: progressWidth(
Number(((trace.count / traceCountTotal.value) * 100).toFixed(1)),
),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: getPercentLabel(trace.count / traceCountTotal.value),
};
})
.slice(selectArea.value[0], selectArea.value[1]);
});
const caseTotalPercent = computed(() => {
const ratioSum = traceList.value.map(trace => trace.base_count).reduce((acc, cur) => acc + cur, 0) / traceCountTotal.value;
return getPercentLabel(ratioSum)
const ratioSum =
traceList.value
.map((trace) => trace.base_count)
.reduce((acc, cur) => acc + cur, 0) / traceCountTotal.value;
return getPercentLabel(ratioSum);
});
const chartData = computed(() => {
const start = selectArea.value[0];
const end = selectArea.value[1] - 1;
const labels = baseTraces.value.map(trace => `#${trace.id}`);
const data = baseTraces.value.map(trace => getPercentLabel(trace.count / traceCountTotal.value));
const selectAreaData = baseTraces.value.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
const labels = baseTraces.value.map((trace) => `#${trace.id}`);
const data = baseTraces.value.map((trace) =>
getPercentLabel(trace.count / traceCountTotal.value),
);
const selectAreaData = baseTraces.value.map((trace, index) =>
index >= start && index <= end ? "rgba(0,153,255)" : "rgba(203, 213, 225)",
);
return { // Data to display
return {
// Data to display
labels,
datasets: [
{
label: 'Trace', // Dataset label
label: "Trace", // Dataset label
data,
backgroundColor: selectAreaData,
categoryPercentage: 1.0,
barPercentage: 1.0
barPercentage: 1.0,
},
]
],
};
});
const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => {
data.forEach((item) => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // Create a new key-value pair
});
delete item.attributes; // Remove the original attributes property
})
});
return data;
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(baseCases.value)); // Deep copy the original cases data
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
];
if (data.length !== 0) {
result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
...(data[0]?.attributes ?? []).map((att, index) => ({
field: `att_${index}`,
header: att.key,
})),
];
}
return result
return result;
});
watch(selectArea, (newValue, oldValue) => {
const roundValue = Math.round(newValue[1].toFixed());
if(newValue[1] !== roundValue) selectArea.value[1] = roundValue;
if(newValue != oldValue) emit('filter-trace-selectArea', newValue); // Determine whether Apply should be disabled
if (newValue[1] !== roundValue) selectArea.value[1] = roundValue;
if (newValue != oldValue) emit("filter-trace-selectArea", newValue); // Determine whether Apply should be disabled
});
watch(infinit404, (newValue) => {
if(newValue === 404) infinitMaxItems.value = true;
if (newValue === 404) infinitMaxItems.value = true;
});
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && typeof isScrollTop.scrollTop !== "undefined")
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
/**
* Set bar chart Options
*/
function barOptions(){
function barOptions() {
return {
maintainAspectRatio: false,
aspectRatio: 0.8,
@@ -208,41 +284,43 @@ function barOptions(){
top: 16,
left: 8,
right: 8,
}
},
},
plugins: {
legend: { // Legend
legend: {
// Legend
display: false,
},
tooltip: {
callbacks: {
label: (tooltipItems) =>{
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`
}
}
}
label: (tooltipItems) => {
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`;
},
},
},
},
animations: false,
scales: {
x: {
display:false
display: false,
},
y: {
ticks: { // Set tick intervals
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
min: 0,
max: traceList.value[0]?.ratio,
stepSize: (traceList.value[0]?.ratio)/4,
stepSize: traceList.value[0]?.ratio / 4,
},
grid: {
color: 'rgba(100,116,139)',
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
}
}
}
},
},
},
};
}
@@ -251,8 +329,8 @@ function barOptions(){
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
@@ -261,8 +339,8 @@ function getPercentLabel(val){
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
@@ -272,7 +350,7 @@ function progressWidth(value){
*/
async function switchCaseData(id, count) {
// Do nothing if clicking the same id
if(id == showTraceId.value) return;
if (id == showTraceId.value) return;
isLoading.value = true; // Always show loading screen
infinit404.value = null;
infinitMaxItems.value = false;
@@ -287,7 +365,7 @@ async function switchCaseData(id, count) {
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData(){
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
@@ -296,20 +374,20 @@ function setNodesData(){
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100
}
width: 100,
},
});
})
});
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData(){
function setEdgesData() {
processMap.value.edges = [];
baseTraceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
@@ -317,8 +395,8 @@ function setEdgesData(){
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
style: "solid",
},
});
});
// The number of edges is one less than the number of nodes
@@ -328,7 +406,7 @@ function setEdgesData(){
/**
* create trace cytoscape's map
*/
function createCy(){
function createCy() {
const graphId = cyTraceRef.value;
setNodesData();
@@ -341,12 +419,18 @@ function createCy(){
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if(infinitMaxItems.value || baseCases.value.length < 20 || infiniteFinish.value === false) return;
if (
infinitMaxItems.value ||
baseCases.value.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
const overScrollHeight =
container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) fetchData();
if (overScrollHeight) fetchData();
}
/**
@@ -361,8 +445,8 @@ async function fetchData() {
infiniteData.value = [...infiniteData.value, ...baseCases.value];
infiniteFinish.value = true;
isLoading.value = false;
} catch(error) {
console.error('Failed to load data:', error);
} catch (error) {
console.error("Failed to load data:", error);
}
}
@@ -372,7 +456,7 @@ onMounted(() => {
setEdgesData();
createCy();
chartOptions.value = barOptions();
selectArea.value = [0, traceTotal.value]
selectArea.value = [0, traceTotal.value];
isLoading.value = false;
});
</script>
@@ -381,14 +465,14 @@ onMounted(() => {
@reference "../../../../assets/tailwind.css";
/* Table set */
:deep(.p-datatable-thead) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
@apply sticky top-0 left-0 z-10 bg-neutral-10;
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
@apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500;
white-space: nowrap;
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0 text-center
@apply border-neutral-500 !border-t-0 text-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;
@@ -398,6 +482,6 @@ onMounted(() => {
}
/* Center datatable header */
:deep(.p-column-header-content) {
@apply justify-center
@apply justify-center;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,13 @@
<template>
<Sidebar :visible="sidebarTraces" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="false" class="!w-11/12" @show="show()">
<Sidebar
:visible="sidebarTraces"
:closeIcon="'pi pi-chevron-left'"
:modal="false"
position="left"
:dismissable="false"
class="!w-11/12"
@show="show()"
>
<template #header>
<p class="h1">Traces</p>
</template>
@@ -7,23 +15,37 @@
<!-- Trace List -->
<section class="w-80 h-full pr-4 border-r border-neutral-300">
<p class="h2 px-2 mb-2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 px-2 mb-2">
Click trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<p class="text-primary h2 px-2 mb-2">Click trace number to see more.</p>
<div
class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]"
>
<table class="border-separate border-spacing-x-2 text-sm">
<caption class="hidden">Trace List</caption>
<caption class="hidden">
Trace List
</caption>
<thead class="sticky top-0 z-10 bg-neutral-10">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th class="h2 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
<th
class="h2 px-2 border-b border-neutral-500 text-start"
colspan="3"
>
Occurrences
</th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id, trace.base_count)">
<tr
v-for="(trace, key) in traceList"
:key="key"
class="cursor-pointer hover:text-primary"
@click="switchCaseData(trace.id, trace.base_count)"
>
<td class="p-2">#{{ trace.id }}</td>
<td class="p-2 w-24">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div class="h-full bg-primary" :style="trace.value"></div>
</div>
</td>
@@ -39,16 +61,34 @@
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
<div
id="cyTrace"
ref="cyTraceRef"
class="h-full min-w-full relative"
></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar w-full h-[calc(100%_-_200px)] infiniteTable " @scroll="handleScroll">
<DataTable :value="caseData" showGridlines tableClass="text-sm" breakpoint="0">
<div
class="overflow-y-auto overflow-x-auto scrollbar w-full h-[calc(100%_-_200px)] infiniteTable"
@scroll="handleScroll"
>
<DataTable
:value="caseData"
showGridlines
tableClass="text-sm"
breakpoint="0"
>
<div v-for="(col, index) in columnData" :key="index">
<Column :field="col.field" :header="col.header">
<template #body="{ data }">
<div :class="data[col.field]?.length > 18 ? 'whitespace-normal' : 'whitespace-nowrap'">
{{ data[col.field] }}
<div
:class="
data[col.field]?.length > 18
? 'whitespace-normal'
: 'whitespace-nowrap'
"
>
{{ data[col.field] }}
</div>
</template>
</Column>
@@ -70,23 +110,30 @@
* clickable trace lists for highlighting on the map.
*/
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
import { ref, computed, watch } from "vue";
import { storeToRefs } from "pinia";
import { useLoadingStore } from "@/stores/loading";
import { useAllMapDataStore } from "@/stores/allMapData";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
const props = defineProps(['sidebarTraces', 'cases']);
const emit = defineEmits(['switch-Trace-Id']);
const props = defineProps(["sidebarTraces", "cases"]);
const emit = defineEmits(["switch-Trace-Id"]);
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases } = storeToRefs(allMapDataStore);
const {
infinit404,
infiniteStart,
traceId,
traces,
traceTaskSeq,
infiniteFirstCases,
} = storeToRefs(allMapDataStore);
const processMap = ref({
nodes:[],
edges:[],
nodes: [],
edges: [],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
@@ -99,8 +146,10 @@ const traceTotal = computed(() => {
});
const traceList = computed(() => {
const sum = traces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
const result = traces.value.map(trace => {
const sum = traces.value
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
const result = traces.value.map((trace) => {
return {
id: trace.id,
value: progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
@@ -108,54 +157,63 @@ const traceList = computed(() => {
base_count: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
})
});
return result;
});
const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => {
data.forEach((item) => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // Create a new key-value pair
});
delete item.attributes; // Remove the original attributes property
})
});
return data;
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // Deep copy the original cases data
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
];
if (data.length !== 0) {
result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...(data[0]?.attributes ?? []).map((att, index) => ({ field: `att_${index}`, header: att.key })),
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
...(data[0]?.attributes ?? []).map((att, index) => ({
field: `att_${index}`,
header: att.key,
})),
];
}
return result
return result;
});
watch(infinit404, (newValue) => {
if(newValue === 404) infinitMaxItems.value = true;
if (newValue === 404) infinitMaxItems.value = true;
});
watch(traceId, (newValue) => {
showTraceId.value = newValue;
}, { immediate: true });
watch(
traceId,
(newValue) => {
showTraceId.value = newValue;
},
{ immediate: true },
);
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && typeof isScrollTop.scrollTop !== "undefined")
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(infiniteFirstCases, (newValue) => {
if(infiniteFirstCases.value) infiniteData.value = JSON.parse(JSON.stringify(newValue));
if (infiniteFirstCases.value)
infiniteData.value = JSON.parse(JSON.stringify(newValue));
});
/**
@@ -163,8 +221,8 @@ watch(infiniteFirstCases, (newValue) => {
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
@@ -173,8 +231,8 @@ function getPercentLabel(val){
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
@@ -184,19 +242,19 @@ function progressWidth(value){
*/
async function switchCaseData(id, count) {
// Do nothing if clicking the same id
if(id == showTraceId.value) return;
if (id == showTraceId.value) return;
isLoading.value = true; // Always show loading screen
infinit404.value = null;
infinitMaxItems.value = false;
showTraceId.value = id;
infiniteStart.value = 0;
emit('switch-Trace-Id', {id: showTraceId.value, count: count}); // Pass to Map index, which will close loading
emit("switch-Trace-Id", { id: showTraceId.value, count: count }); // Pass to Map index, which will close loading
}
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData(){
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
@@ -205,20 +263,20 @@ function setNodesData(){
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100
}
width: 100,
},
});
})
});
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData(){
function setEdgesData() {
processMap.value.edges = [];
traceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
@@ -226,8 +284,8 @@ function setEdgesData(){
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
style: "solid",
},
});
});
// The number of edges is one less than the number of nodes
@@ -237,7 +295,7 @@ function setEdgesData(){
/**
* create trace cytoscape's map
*/
function createCy(){
function createCy() {
const graphId = cyTraceRef.value;
setNodesData();
@@ -264,12 +322,18 @@ async function show() {
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if(infinitMaxItems.value || props.cases.length < 20 || infiniteFinish.value === false) return;
if (
infinitMaxItems.value ||
props.cases.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
const overScrollHeight =
container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) fetchData();
if (overScrollHeight) fetchData();
}
/**
@@ -284,8 +348,8 @@ async function fetchData() {
infiniteData.value = [...infiniteData.value, ...props.cases];
infiniteFinish.value = true;
isLoading.value = false;
} catch(error) {
console.error('Failed to load data:', error);
} catch (error) {
console.error("Failed to load data:", error);
}
}
</script>
@@ -294,18 +358,18 @@ async function fetchData() {
@reference "../../../assets/tailwind.css";
/* Progress bar color */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
@apply bg-primary;
}
/* Table set */
:deep(.p-datatable-thead) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
@apply sticky top-0 left-0 z-10 bg-neutral-10;
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
@apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500;
white-space: nowrap;
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0 text-center
@apply border-neutral-500 !border-t-0 text-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;
@@ -315,6 +379,6 @@ async function fetchData() {
}
/* Center datatable header */
:deep(.p-column-header-content) {
@apply justify-center
@apply justify-center;
}
</style>

View File

@@ -1,5 +1,11 @@
<template>
<Sidebar :visible="sidebarView" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="false" >
<Sidebar
:visible="sidebarView"
:closeIcon="'pi pi-chevron-left'"
:modal="false"
position="left"
:dismissable="false"
>
<template #header>
<p class="h1">Visualization Setting</p>
</template>
@@ -10,28 +16,54 @@
<ul class="space-y-3 mb-4">
<!-- Select bpmn / processmap button -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="mapType === 'processMap'?'btn-toggle-show ':''" @click="onProcessMapClick()">
<span
class="btn-toggle-item"
:class="mapType === 'processMap' ? 'btn-toggle-show ' : ''"
@click="onProcessMapClick()"
>
Process Map
</span>
<span class="btn-toggle-item" :class="mapType === 'bpmn'?'btn-toggle-show':''" @click="onBPMNClick()">
<span
class="btn-toggle-item"
:class="mapType === 'bpmn' ? 'btn-toggle-show' : ''"
@click="onBPMNClick()"
>
BPMN Model
</span>
</li>
<!-- Select drawing style: bezier / unbundled-bezier button -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="curveStyle === 'unbundled-bezier'?'btn-toggle-show ':''" @click="switchCurveStyles('unbundled-bezier')">
<span
class="btn-toggle-item"
:class="
curveStyle === 'unbundled-bezier' ? 'btn-toggle-show ' : ''
"
@click="switchCurveStyles('unbundled-bezier')"
>
Curved
</span>
<span class="btn-toggle-item" :class="curveStyle === 'taxi'?'btn-toggle-show':''" @click="switchCurveStyles('taxi')">
<span
class="btn-toggle-item"
:class="curveStyle === 'taxi' ? 'btn-toggle-show' : ''"
@click="switchCurveStyles('taxi')"
>
Elbow
</span>
</li>
<!-- Vertical TB | Horizontal LR -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="rank === 'LR'?'btn-toggle-show ':''" @click="switchRank('LR')">
<span
class="btn-toggle-item"
:class="rank === 'LR' ? 'btn-toggle-show ' : ''"
@click="switchRank('LR')"
>
Horizontal
</span>
<span class="btn-toggle-item" :class="rank === 'TB'?'btn-toggle-show':''" @click="switchRank('TB')">
<span
class="btn-toggle-item"
:class="rank === 'TB' ? 'btn-toggle-show' : ''"
@click="switchRank('TB')"
>
Vertical
</span>
</li>
@@ -41,25 +73,67 @@
<div>
<p class="h2">Data Layer</p>
<ul class="space-y-2">
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'freq')">
<li
class="flex justify-between mb-3"
@change="switchDataLayerType($event, 'freq')"
>
<div class="flex items-center w-1/2">
<RadioButton v-model="dataLayerType" inputId="freq" name="dataLayer" value="freq" class="mr-2" @click.prevent="switchDataLayerType($event, 'freq')"/>
<RadioButton
v-model="dataLayerType"
inputId="freq"
name="dataLayer"
value="freq"
class="mr-2"
@click.prevent="switchDataLayerType($event, 'freq')"
/>
<label for="freq">Frequency</label>
</div>
<div class="w-1/2">
<select class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary" :disabled="dataLayerType === 'duration'">
<option v-for="(freq, index) in selectFrequency" :key="index" :value="freq.value" :disabled="freq.disabled" :selected="freq.value === selectedFreq">{{ freq.label }}</option>
<select
class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary"
:disabled="dataLayerType === 'duration'"
>
<option
v-for="(freq, index) in selectFrequency"
:key="index"
:value="freq.value"
:disabled="freq.disabled"
:selected="freq.value === selectedFreq"
>
{{ freq.label }}
</option>
</select>
</div>
</li>
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'duration')">
<li
class="flex justify-between mb-3"
@change="switchDataLayerType($event, 'duration')"
>
<div class="flex items-center w-1/2">
<RadioButton v-model="dataLayerType" inputId="duration" name="dataLayer" value="duration" class="mr-2" @click.prevent="switchDataLayerType($event, 'duration')"/>
<RadioButton
v-model="dataLayerType"
inputId="duration"
name="dataLayer"
value="duration"
class="mr-2"
@click.prevent="switchDataLayerType($event, 'duration')"
/>
<label for="duration">Duration</label>
</div>
<div class="w-1/2">
<select class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary" :disabled="dataLayerType === 'freq'">
<option v-for="(duration, index) in selectDuration" :key="index" :value="duration.value" :disabled="duration.disabled" :selected="duration.value === selectedDuration">{{ duration.label }}</option>
<select
class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary"
:disabled="dataLayerType === 'freq'"
>
<option
v-for="(duration, index) in selectDuration"
:key="index"
:value="duration.value"
:disabled="duration.disabled"
:selected="duration.value === selectedDuration"
>
{{ duration.label }}
</option>
</select>
</div>
</li>
@@ -80,9 +154,9 @@
* style, direction, and data layer selection.
*/
import { ref, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useMapPathStore } from '@/stores/mapPathStore';
import { ref, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useMapPathStore } from "@/stores/mapPathStore";
defineProps({
sidebarView: {
@@ -92,39 +166,39 @@ defineProps({
});
const emit = defineEmits([
'switch-map-type',
'switch-curve-styles',
'switch-rank',
'switch-data-layer-type',
"switch-map-type",
"switch-curve-styles",
"switch-rank",
"switch-data-layer-type",
]);
const mapPathStore = useMapPathStore();
const { isBPMNOn } = storeToRefs(mapPathStore);
const selectFrequency = ref([
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_freq", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
{ value:"cases", label:"Number of cases", disabled:false, },
{ value: "total", label: "Total", disabled: false },
{ value: "rel_freq", label: "Relative", disabled: false },
{ value: "average", label: "Average", disabled: false },
{ value: "median", label: "Median", disabled: false },
{ value: "max", label: "Max", disabled: false },
{ value: "min", label: "Min", disabled: false },
{ value: "cases", label: "Number of cases", disabled: false },
]);
const selectDuration = ref([
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_duration", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
{ value: "total", label: "Total", disabled: false },
{ value: "rel_duration", label: "Relative", disabled: false },
{ value: "average", label: "Average", disabled: false },
{ value: "median", label: "Median", disabled: false },
{ value: "max", label: "Max", disabled: false },
{ value: "min", label: "Min", disabled: false },
]);
const curveStyle = ref('unbundled-bezier'); // unbundled-bezier | taxi
const mapType = ref('processMap'); // processMap | bpmn
const curveStyle = ref("unbundled-bezier"); // unbundled-bezier | taxi
const mapType = ref("processMap"); // processMap | bpmn
const dataLayerType = ref(null); // freq | duration
const dataLayerOption = ref(null);
const selectedFreq = ref('');
const selectedDuration = ref('');
const rank = ref('LR'); // Vertical TB | Horizontal LR
const selectedFreq = ref("");
const selectedDuration = ref("");
const rank = ref("LR"); // Vertical TB | Horizontal LR
/**
* Switches the map type and emits the change event.
@@ -132,7 +206,7 @@ const rank = ref('LR'); // Vertical TB | Horizontal LR
*/
function switchMapType(type) {
mapType.value = type;
emit('switch-map-type', mapType.value);
emit("switch-map-type", mapType.value);
}
/**
@@ -141,7 +215,7 @@ function switchMapType(type) {
*/
function switchCurveStyles(style) {
curveStyle.value = style;
emit('switch-curve-styles', curveStyle.value);
emit("switch-curve-styles", curveStyle.value);
}
/**
@@ -150,7 +224,7 @@ function switchCurveStyles(style) {
*/
function switchRank(rankValue) {
rank.value = rankValue;
emit('switch-rank', rank.value);
emit("switch-rank", rank.value);
}
/**
@@ -159,40 +233,41 @@ function switchRank(rankValue) {
* @param {string} type - 'freq' or 'duration'.
*/
function switchDataLayerType(e, type) {
let value = '';
let value = "";
if(e.target.value !== 'freq' && e.target.value !== 'duration') value = e.target.value;
if (e.target.value !== "freq" && e.target.value !== "duration")
value = e.target.value;
switch (type) {
case 'freq':
value = value || selectedFreq.value || 'total';
case "freq":
value = value || selectedFreq.value || "total";
dataLayerType.value = type;
dataLayerOption.value = value;
selectedFreq.value = value;
break;
case 'duration':
value = value || selectedDuration.value || 'total';
case "duration":
value = value || selectedDuration.value || "total";
dataLayerType.value = type;
dataLayerOption.value = value;
selectedDuration.value = value;
break;
}
emit('switch-data-layer-type', dataLayerType.value, dataLayerOption.value);
emit("switch-data-layer-type", dataLayerType.value, dataLayerOption.value);
}
/** Switches to Process Map view. */
function onProcessMapClick() {
mapPathStore.setIsBPMNOn(false);
switchMapType('processMap');
switchMapType("processMap");
}
/** Switches to BPMN Model view. */
function onBPMNClick() {
mapPathStore.setIsBPMNOn(true);
switchMapType('bpmn');
switchMapType("bpmn");
}
onMounted(() => {
dataLayerType.value = 'freq';
dataLayerOption.value = 'total';
dataLayerType.value = "freq";
dataLayerOption.value = "total";
});
</script>

View File

@@ -1,81 +1,149 @@
<template>
<section class="w-full top-0 absolute shadow-[0px_6px_6px_inset_rgba(0,0,0,0.1)] z-20">
<!-- status content -->
<ul class="bg-neutral-100 flex justify-start shadow-[0px_1px_4px_rgba(0,0,0,0.2)] gap-3 p-3 text-sm overflow-x-auto scrollbar duration-700" v-show="isPanel" v-if="statData">
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Cases</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.cases.count }} / {{ statData.cases.total }}</span>
<ProgressBar :value="statData.cases.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
<section
class="w-full top-0 absolute shadow-[0px_6px_6px_inset_rgba(0,0,0,0.1)] z-20"
>
<!-- status content -->
<ul
class="bg-neutral-100 flex justify-start shadow-[0px_1px_4px_rgba(0,0,0,0.2)] gap-3 p-3 text-sm overflow-x-auto scrollbar duration-700"
v-show="isPanel"
v-if="statData"
>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Cases</p>
</div>
<span class="block text-2xl font-medium">{{ statData.cases.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Traces</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.traces.count }} / {{ statData.traces.total }}</span>
<ProgressBar :value="statData.traces.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.cases.count }} / {{ statData.cases.total }}</span
>
<ProgressBar
:value="statData.cases.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.cases.ratio }}%</span
>
</div>
<span class="block text-2xl font-medium">{{ statData.traces.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activity Instances</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.task_instances.count }} / {{ statData.task_instances.total }}</span>
<ProgressBar :value="statData.task_instances.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Traces</p>
</div>
<span class="block text-2xl font-medium">{{ statData.task_instances.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activities</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.tasks.count }} / {{ statData.tasks.total }}</span>
<ProgressBar :value="statData.tasks.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.traces.count }} / {{ statData.traces.total }}</span
>
<ProgressBar
:value="statData.traces.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.traces.ratio }}%</span
>
</div>
<span class="block text-2xl font-medium">{{ statData.tasks.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8 mb-2.5">Log Timeframe</p>
<div class="px-2 space-y-2 min-w-[140px] h-[40px]">
<span class="inline-block">{{ statData.started_at }}&nbsp</span>
<span class="inline-block">~&nbsp{{ statData.completed_at }}</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8">Case Duration</p>
<div class="flex justify-between items-center space-x-2 min-w-[272px]">
<div class="space-y-2">
<p><Tag value="MAX" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.max }}</p>
<p><Tag value="MIN" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.min }}</p>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activity Instances</p>
</div>
<div class="space-y-2">
<p><Tag value="MED" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.median }}</p>
<p><Tag value="AVG" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.average }}</p>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.task_instances.count }} /
{{ statData.task_instances.total }}</span
>
<ProgressBar
:value="statData.task_instances.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.task_instances.ratio }}%</span
>
</div>
</div>
</li>
</ul>
<!-- control button -->
<div class="bg-neutral-300 rounded-b-full w-20 text-center mx-auto cursor-pointer hover:bg-neutral-500 hover:text-neutral-10 active:ring focus:outline-none focus:border-neutral-500 focus:ring" @click="isPanel = !isPanel">
<span class="material-symbols-outlined block px-8 !text-xs ">{{ isPanel ? 'keyboard_double_arrow_up' : 'keyboard_double_arrow_down' }}</span>
</div>
</section>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activities</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.tasks.count }} / {{ statData.tasks.total }}</span
>
<ProgressBar
:value="statData.tasks.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.tasks.ratio }}%</span
>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8 mb-2.5">Log Timeframe</p>
<div class="px-2 space-y-2 min-w-[140px] h-[40px]">
<span class="inline-block">{{ statData.started_at }}&nbsp</span>
<span class="inline-block">~&nbsp{{ statData.completed_at }}</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8">Case Duration</p>
<div class="flex justify-between items-center space-x-2 min-w-[272px]">
<div class="space-y-2">
<p>
<Tag
value="MAX"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.max }}
</p>
<p>
<Tag
value="MIN"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.min }}
</p>
</div>
<div class="space-y-2">
<p>
<Tag
value="MED"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.median }}
</p>
<p>
<Tag
value="AVG"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.average }}
</p>
</div>
</div>
</li>
</ul>
<!-- control button -->
<div
class="bg-neutral-300 rounded-b-full w-20 text-center mx-auto cursor-pointer hover:bg-neutral-500 hover:text-neutral-10 active:ring focus:outline-none focus:border-neutral-500 focus:ring"
@click="isPanel = !isPanel"
>
<span class="material-symbols-outlined block px-8 !text-xs">{{
isPanel ? "keyboard_double_arrow_up" : "keyboard_double_arrow_down"
}}</span>
</div>
</section>
</template>
<script setup>
@@ -89,12 +157,12 @@
* timeframe, case duration) for the Discover page.
*/
import { ref, onMounted, } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { getTimeLabel } from '@/module/timeLabel.js';
import getMoment from 'moment';
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { getTimeLabel } from "@/module/timeLabel.js";
import getMoment from "moment";
const route = useRoute();
@@ -109,8 +177,8 @@ const statData = ref(null);
* @param {number} val - The ratio value to convert.
* @returns {number} The percentage value.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
@@ -118,46 +186,48 @@ function getPercentLabel(val){
function getStatData() {
statData.value = {
cases: {
count: stats.value.cases.count.toLocaleString('en-US'),
total: stats.value.cases.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.cases.ratio)
count: stats.value.cases.count.toLocaleString("en-US"),
total: stats.value.cases.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.cases.ratio),
},
traces: {
count: stats.value.traces.count.toLocaleString('en-US'),
total: stats.value.traces.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.traces.ratio)
count: stats.value.traces.count.toLocaleString("en-US"),
total: stats.value.traces.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.traces.ratio),
},
task_instances: {
count: stats.value.task_instances.count.toLocaleString('en-US'),
total: stats.value.task_instances.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.task_instances.ratio)
count: stats.value.task_instances.count.toLocaleString("en-US"),
total: stats.value.task_instances.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.task_instances.ratio),
},
tasks: {
count: stats.value.tasks.count.toLocaleString('en-US'),
total: stats.value.tasks.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.tasks.ratio)
count: stats.value.tasks.count.toLocaleString("en-US"),
total: stats.value.tasks.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.tasks.ratio),
},
started_at: getMoment(stats.value.started_at).format('YYYY-MM-DD HH:mm'),
completed_at: getMoment(stats.value.completed_at).format('YYYY-MM-DD HH:mm'),
started_at: getMoment(stats.value.started_at).format("YYYY-MM-DD HH:mm"),
completed_at: getMoment(stats.value.completed_at).format(
"YYYY-MM-DD HH:mm",
),
case_duration: {
min: getTimeLabel(stats.value.case_duration.min),
max: getTimeLabel(stats.value.case_duration.max),
average: getTimeLabel(stats.value.case_duration.average),
median: getTimeLabel(stats.value.case_duration.median),
}
}
},
};
}
onMounted(async () => {
const params = route.params;
const file = route.meta.file;
const isCheckPage = route.name.includes('Check');
const isCheckPage = route.name.includes("Check");
switch (params.type) {
case 'log':
case "log":
logId.value = isCheckPage ? file.parent.id : params.fileId;
break;
case 'filter':
case "filter":
createFilterId.value = isCheckPage ? file.parent.id : params.fileId;
break;
}
@@ -169,6 +239,6 @@ onMounted(async () => {
<style scoped>
@reference "../../assets/tailwind.css";
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-neutral-900
@apply bg-neutral-900;
}
</style>

View File

@@ -1,15 +1,30 @@
<template>
<Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="emit('closeModal', $event)">
<Dialog
:visible="uploadModal"
modal
:style="{ width: '90vw', height: '90vh' }"
:contentClass="contentClass"
@update:visible="emit('closeModal', $event)"
>
<template #header>
<div class="py-5">
</div>
<div class="py-5"></div>
</template>
<label for="uploadFiles">
<div class=" h-full flex flex-col justify-center items-center p-4 space-y-4 relative">
<input id="uploadFiles" class=" absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" type="file" accept="text/csv" @change="upload($event)">
<div
class="h-full flex flex-col justify-center items-center p-4 space-y-4 relative"
>
<input
id="uploadFiles"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
type="file"
accept="text/csv"
@change="upload($event)"
/>
<IconUploarding class="loader-arrow-upward"></IconUploarding>
<p class="text-neutral-900">Click or drag a file here.</p>
<p class="text-neutral-500">(Only <span class="text-primary">.csv</span> is supported)</p>
<p class="text-neutral-500">
(Only <span class="text-primary">.csv</span> is supported)
</p>
</div>
</label>
</Dialog>
@@ -26,59 +41,59 @@
* with drag-and-drop support for CSV files (max 90 MB).
*/
import { onBeforeUnmount, } from 'vue';
import { storeToRefs } from 'pinia';
import IconUploarding from '../icons/IconUploarding.vue';
import { uploadFailedFirst } from '@/module/alertModal.js';
import { useFilesStore } from '@/stores/files';
import { onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import IconUploarding from "../icons/IconUploarding.vue";
import { uploadFailedFirst } from "@/module/alertModal.js";
import { useFilesStore } from "@/stores/files";
defineProps(['uploadModal']);
const emit = defineEmits(['closeModal']);
defineProps(["uploadModal"]);
const emit = defineEmits(["closeModal"]);
const filesStore = useFilesStore();
const { uploadFileName } = storeToRefs(filesStore);
const contentClass = 'h-full';
const contentClass = "h-full";
/**
* Handles CSV file upload: validates size, sends to API, extracts filename.
* @param {Event} event - The file input change event.
*/
async function upload(event) {
const fileInput = document.getElementById('uploadFiles');
const fileInput = document.getElementById("uploadFiles");
const target = event.target;
const formData = new FormData();
let uploadFile;
// Check if a file exists
if(target && target.files) {
if (target && target.files) {
uploadFile = target.files[0];
}
// File size must not exceed 90 MB (90*1024*1024 = 94,371,840 bytes)
if(uploadFile.size >= 94371840) {
fileInput.value = '';
return uploadFailedFirst('size');
if (uploadFile.size >= 94371840) {
fileInput.value = "";
return uploadFailedFirst("size");
}
// Append the file to formData; the field name must be "csv"
formData.append('csv', uploadFile);
formData.append("csv", uploadFile);
// Call the first-stage upload API
if(uploadFile) {
if (uploadFile) {
await filesStore.upload(formData);
}
if (uploadFile.name.endsWith('.csv')) {
if (uploadFile.name.endsWith(".csv")) {
uploadFileName.value = uploadFile.name.slice(0, -4);
} else {
// Handle error or invalid file format
uploadFileName.value = ''; // Or other appropriate error handling
uploadFileName.value = ""; // Or other appropriate error handling
}
// Clear the selected file
if(fileInput) {
fileInput.value = '';
if (fileInput) {
fileInput.value = "";
}
}
onBeforeUnmount(() => {
emit('closeModal', false);
emit("closeModal", false);
});
</script>
<style scoped>

View File

@@ -1,19 +1,35 @@
<template>
<div id='header.vue' class="mx-auto px-4 h-14 z-50">
<div id="header.vue" class="mx-auto px-4 h-14 z-50">
<div class="flex justify-between items-center h-full">
<figure>
<DspLogo />
<DspLogo />
</figure>
<div class="flex justify-between items-center relative"
v-show="showMember">
<img id="acct_mgmt_button" v-if="!isHeadHovered" src="@/assets/icon-head-black.svg" @mouseenter='isHeadHovered = true'
width="32" height="32" @click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50" alt="user-head"
/>
<img id="acct_mgmt_button" v-else src="@/assets/icon-head-blue.svg" @mouseleave='isHeadHovered = false'
width="32" height="32" @click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50" alt="user-head"
/>
<div
class="flex justify-between items-center relative"
v-show="showMember"
>
<img
id="acct_mgmt_button"
v-if="!isHeadHovered"
src="@/assets/icon-head-black.svg"
@mouseenter="isHeadHovered = true"
width="32"
height="32"
@click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50"
alt="user-head"
/>
<img
id="acct_mgmt_button"
v-else
src="@/assets/icon-head-blue.svg"
@mouseleave="isHeadHovered = false"
width="32"
height="32"
@click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50"
alt="user-head"
/>
</div>
</div>
</div>
@@ -31,16 +47,16 @@
* user account menu toggle button.
*/
import { ref, onMounted, } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs, } from 'pinia';
import emitter from '@/utils/emitter';
import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import DspLogo from '@/components/icons/DspLogo.vue';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import emitter from "@/utils/emitter";
import { useLoginStore } from "@/stores/login";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import DspLogo from "@/components/icons/DspLogo.vue";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { leaveFilter, leaveConformance } from "@/module/alertModal.js";
const route = useRoute();
@@ -49,8 +65,13 @@ const { logOut } = store;
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
const { tempFilterId, temporaryData, postRuleData, ruleData } =
storeToRefs(allMapDataStore);
const {
conformanceLogTempCheckId,
conformanceFilterTempCheckId,
conformanceFileName,
} = storeToRefs(conformanceStore);
const isHeadHovered = ref(false);
const showMember = ref(false);
@@ -64,21 +85,31 @@ const toggleIsAcctMenuOpen = () => {
* and Conformance pages.
*/
function logOutButton() {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
if (
(route.name === "Map" || route.name === "CheckMap") &&
tempFilterId.value
) {
// Notify Map to close the Sidebar.
emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
emitter.emit("leaveFilter", false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut);
} else if (
(route.name === "Conformance" || route.name === "CheckConformance") &&
(conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)
) {
leaveConformance(
false,
conformanceStore.addConformanceCreateCheckId,
false,
logOut,
);
} else {
logOut();
}
}
onMounted(() => {
if (route.name === 'Login' || route.name === 'NotFound404') {
showMember.value = false
if (route.name === "Login" || route.name === "NotFound404") {
showMember.value = false;
} else {
showMember.value = true;
}

View File

@@ -3,7 +3,9 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<div class="w-full h-full fixed inset-0 m-auto flex justify-center items-center bg-gradient-to-tr from-neutral-500/50 to-neutral-900/50 z-[9999]">
<div
class="w-full h-full fixed inset-0 m-auto flex justify-center items-center bg-gradient-to-tr from-neutral-500/50 to-neutral-900/50 z-[9999]"
>
<span class="loader block"></span>
</div>
</template>

View File

@@ -1,32 +1,62 @@
<template>
<nav id='nav_bar' class="bg-neutral-700">
<div class="mx-auto px-4" :class="[showNavbarBreadcrumb? 'min-h-12': 'h-12']">
<div class="flex justify-between items-center flex-wrap relative" v-show="showNavbarBreadcrumb">
<nav id="nav_bar" class="bg-neutral-700">
<div
class="mx-auto px-4"
:class="[showNavbarBreadcrumb ? 'min-h-12' : 'h-12']"
>
<div
class="flex justify-between items-center flex-wrap relative"
v-show="showNavbarBreadcrumb"
>
<div id="nav_bar_logged_in" class="flex flex-1 items-center">
<!-- Back to Files page -->
<router-link to="/files" class="mr-4" v-if="showIcon" id="backPage">
<span class="material-symbols-outlined text-neutral-10 !leading-loose">
<span
class="material-symbols-outlined text-neutral-10 !leading-loose"
>
arrow_back
</span>
</router-link>
<div>
<h2 v-if="navViewName !== 'UPLOAD'" class="mr-14 py-3 text-2xl font-black text-neutral-10">{{ navViewName }}</h2>
<h2 v-else class="mr-14 py-3 text-2xl font-black text-neutral-10">FILES</h2>
<h2
v-if="navViewName !== 'UPLOAD'"
class="mr-14 py-3 text-2xl font-black text-neutral-10"
>
{{ navViewName }}
</h2>
<h2 v-else class="mr-14 py-3 text-2xl font-black text-neutral-10">
FILES
</h2>
</div>
<ul class="flex justify-center items-center space-x-4 text-xl font-semibold text-neutral-300 cursor-pointer">
<li @click="onNavItemBtnClick($event, item)"
<ul
class="flex justify-center items-center space-x-4 text-xl font-semibold text-neutral-300 cursor-pointer"
>
<li
@click="onNavItemBtnClick($event, item)"
v-for="(item, index) in navViewData[navViewName]"
:key="index" class="nav-item"
:class="{'active': activePage === item}">
:key="index"
class="nav-item"
:class="{ active: activePage === item }"
>
{{ item }}
</li>
</ul>
</div>
<!-- Files Page: Search and Upload -->
<div class="flex justify-end items-center" v-if="navViewName === 'FILES'">
<div id="import_btn" class="btn btn-sm btn-neutral cursor-pointer" @click="uploadModal = true">
<div
class="flex justify-end items-center"
v-if="navViewName === 'FILES'"
>
<div
id="import_btn"
class="btn btn-sm btn-neutral cursor-pointer"
@click="uploadModal = true"
>
Import
<UploadModal :visible="uploadModal" @closeModal="uploadModal = $event"></UploadModal>
<UploadModal
:visible="uploadModal"
@closeModal="uploadModal = $event"
></UploadModal>
</div>
</div>
<!-- Upload, Performance, Compare have no button actions -->
@@ -34,12 +64,16 @@
<!-- Other Page: Save and Download -->
<!-- Save: if data exists, prompt rename; if no data, prompt save; if unchanged, do nothing -->
<div v-else class="space-x-4">
<button class="btn btn-sm" :class="[ disabledSave ? 'btn-disable' : 'btn-neutral']"
:disabled="disabledSave" @click="saveModal">
<button
class="btn btn-sm"
:class="[disabledSave ? 'btn-disable' : 'btn-neutral']"
:disabled="disabledSave"
@click="saveModal"
>
Save
</button>
</div>
<AcctMenu v-if="showNavbarBreadcrumb"/>
<AcctMenu v-if="showNavbarBreadcrumb" />
</div>
</div>
</nav>
@@ -57,20 +91,24 @@
* Map/Conformance pages.
*/
import { ref, computed, watch, onMounted, } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { storeToRefs, } from 'pinia';
import emitter from '@/utils/emitter';
import { useFilesStore } from '@/stores/files';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useMapCompareStore } from '@/stores/mapCompareStore';
import IconSearch from '@/components/icons/IconSearch.vue';
import IconSetting from '@/components/icons/IconSetting.vue';
import { saveFilter, savedSuccessfully, saveConformance } from '@/module/alertModal.js';
import UploadModal from './File/UploadModal.vue';
import AcctMenu from './AccountMenu/AcctMenu.vue';
import { ref, computed, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { storeToRefs } from "pinia";
import emitter from "@/utils/emitter";
import { useFilesStore } from "@/stores/files";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { usePageAdminStore } from "@/stores/pageAdmin";
import { useMapCompareStore } from "@/stores/mapCompareStore";
import IconSearch from "@/components/icons/IconSearch.vue";
import IconSetting from "@/components/icons/IconSetting.vue";
import {
saveFilter,
savedSuccessfully,
saveConformance,
} from "@/module/alertModal.js";
import UploadModal from "./File/UploadModal.vue";
import AcctMenu from "./AccountMenu/AcctMenu.vue";
const route = useRoute();
const router = useRouter();
@@ -81,54 +119,87 @@ const conformanceStore = useConformanceStore();
const mapCompareStore = useMapCompareStore();
const pageAdminStore = usePageAdminStore();
const { logId, tempFilterId, createFilterId, filterName, postRuleData, isUpdateFilter } = storeToRefs(allMapDataStore);
const { conformanceRuleData, conformanceLogId, conformanceFilterId,
conformanceLogTempCheckId, conformanceFilterTempCheckId,
conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
isUpdateConformance, conformanceFileName
const {
logId,
tempFilterId,
createFilterId,
filterName,
postRuleData,
isUpdateFilter,
} = storeToRefs(allMapDataStore);
const {
conformanceRuleData,
conformanceLogId,
conformanceFilterId,
conformanceLogTempCheckId,
conformanceFilterTempCheckId,
conformanceLogCreateCheckId,
conformanceFilterCreateCheckId,
isUpdateConformance,
conformanceFileName,
} = storeToRefs(conformanceStore);
const { activePage, pendingActivePage, activePageComputedByRoute, shouldKeepPreviousPage } = storeToRefs(pageAdminStore);
const { setPendingActivePage, setPreviousPage, setActivePage, setActivePageComputedByRoute, setIsPagePendingBoolean } = pageAdminStore;
const {
activePage,
pendingActivePage,
activePageComputedByRoute,
shouldKeepPreviousPage,
} = storeToRefs(pageAdminStore);
const {
setPendingActivePage,
setPreviousPage,
setActivePage,
setActivePageComputedByRoute,
setIsPagePendingBoolean,
} = pageAdminStore;
const showNavbarBreadcrumb = ref(false);
const navViewData = {
// e.g. FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'],
FILES: ['ALL', 'DISCOVER', 'COMPARE'],
FILES: ["ALL", "DISCOVER", "COMPARE"],
// e.g. DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'],
DISCOVER: ["MAP", "CONFORMANCE", "PERFORMANCE"],
// e.g. COMPARE: ['PROCESS MAP', 'DASHBOARD']
COMPARE: ['MAP', 'PERFORMANCE'],
'ACCOUNT MANAGEMENT': [],
'MY ACCOUNT': [],
COMPARE: ["MAP", "PERFORMANCE"],
"ACCOUNT MANAGEMENT": [],
"MY ACCOUNT": [],
};
const navViewName = ref('FILES');
const navViewName = ref("FILES");
const uploadModal = ref(false);
const disabledSave = computed(() => {
switch (route.name) {
case 'Map':
case 'CheckMap':
case "Map":
case "CheckMap":
// Cannot save without a filter ID or a temporary tempFilterId
return !tempFilterId.value;
case 'Conformance':
case 'CheckConformance':
return !(conformanceFilterTempCheckId.value || conformanceLogTempCheckId.value);
case "Conformance":
case "CheckConformance":
return !(
conformanceFilterTempCheckId.value || conformanceLogTempCheckId.value
);
}
});
const showIcon = computed(() => {
return !['FILES', 'UPLOAD'].includes(navViewName.value);
return !["FILES", "UPLOAD"].includes(navViewName.value);
});
const noShowSaveButton = computed(() => {
return navViewName.value === 'UPLOAD' || navViewName.value === 'COMPARE' ||
navViewName.value === 'ACCOUNT MANAGEMENT' ||
activePage.value === 'PERFORMANCE';
return (
navViewName.value === "UPLOAD" ||
navViewName.value === "COMPARE" ||
navViewName.value === "ACCOUNT MANAGEMENT" ||
activePage.value === "PERFORMANCE"
);
});
watch(() => route, () => {
getNavViewName();
}, { deep: true });
watch(
() => route,
() => {
getNavViewName();
},
{ deep: true },
);
watch(filterName, (newVal) => {
filterName.value = newVal;
@@ -147,53 +218,76 @@ function onNavItemBtnClick(event) {
setPendingActivePage(navItemCandidate);
switch (navViewName.value) {
case 'FILES':
case "FILES":
store.filesTag = navItemCandidate;
break;
case 'DISCOVER':
case "DISCOVER":
type = route.params.type;
fileId = route.params.fileId;
isCheckPage = route.name.includes('Check');
isCheckPage = route.name.includes("Check");
switch (navItemCandidate) {
case 'MAP':
if(isCheckPage) {
router.push({name: 'CheckMap', params: { type: type, fileId: fileId }});
}
else {
router.push({name: 'Map', params: { type: type, fileId: fileId }});
case "MAP":
if (isCheckPage) {
router.push({
name: "CheckMap",
params: { type: type, fileId: fileId },
});
} else {
router.push({
name: "Map",
params: { type: type, fileId: fileId },
});
}
break;
case 'CONFORMANCE':
if(isCheckPage) { // Beware of Swal popup, it might disturb which is the current active page
router.push({name: 'CheckConformance', params: { type: type, fileId: fileId }});
case "CONFORMANCE":
if (isCheckPage) {
// Beware of Swal popup, it might disturb which is the current active page
router.push({
name: "CheckConformance",
params: { type: type, fileId: fileId },
});
} else {
// Beware of Swal popup, it might disturb which is the current active page
router.push({
name: "Conformance",
params: { type: type, fileId: fileId },
});
}
else { // Beware of Swal popup, it might disturb which is the current active page
router.push({name: 'Conformance', params: { type: type, fileId: fileId }});
}
break
case 'PERFORMANCE':
if(isCheckPage) {
router.push({name: 'CheckPerformance', params: { type: type, fileId: fileId }});
}
else {
router.push({name: 'Performance', params: { type: type, fileId: fileId }});
break;
case "PERFORMANCE":
if (isCheckPage) {
router.push({
name: "CheckPerformance",
params: { type: type, fileId: fileId },
});
} else {
router.push({
name: "Performance",
params: { type: type, fileId: fileId },
});
}
break;
}
break;
case 'COMPARE':
case "COMPARE":
switch (navItemCandidate) {
case 'MAP':
router.push({name: 'MapCompare', params: mapCompareStore.routeParam});
case "MAP":
router.push({
name: "MapCompare",
params: mapCompareStore.routeParam,
});
break;
case 'PERFORMANCE':
router.push({name: 'CompareDashboard', params: mapCompareStore.routeParam});
case "PERFORMANCE":
router.push({
name: "CompareDashboard",
params: mapCompareStore.routeParam,
});
break;
default:
break;
}
};
}
}
/**
@@ -201,42 +295,41 @@ function onNavItemBtnClick(event) {
* @returns {string} The navigation item name to highlight.
*/
function getNavViewName() {
const name = route.name;
let valueToSet;
if(route.name === 'NotFound404' || !route.matched[1]) {
if (route.name === "NotFound404" || !route.matched[1]) {
return;
}
// route.matched[1] is the second matched route record for the current route
navViewName.value = route.matched[1].name.toUpperCase();
store.filesTag = 'ALL';
store.filesTag = "ALL";
switch (navViewName.value) {
case 'FILES':
case "FILES":
valueToSet = activePage.value;
break;
case 'DISCOVER':
case "DISCOVER":
switch (name) {
case 'Map':
case 'CheckMap':
valueToSet = 'MAP';
case "Map":
case "CheckMap":
valueToSet = "MAP";
break;
case 'Conformance':
case 'CheckConformance':
valueToSet = 'CONFORMANCE';
case "Conformance":
case "CheckConformance":
valueToSet = "CONFORMANCE";
break;
case 'Performance':
case 'CheckPerformance':
valueToSet = 'PERFORMANCE';
case "Performance":
case "CheckPerformance":
valueToSet = "PERFORMANCE";
break;
}
break;
case 'COMPARE':
switch(name) {
case 'dummy':
case 'CompareDashboard':
valueToSet = 'DASHBOARD';
case "COMPARE":
switch (name) {
case "dummy":
case "CompareDashboard":
valueToSet = "DASHBOARD";
break;
default:
break;
@@ -248,11 +341,11 @@ function getNavViewName() {
// so here we need to save to a pending state
// The frontend cannot determine which modal button the user will press
// (cancel or confirm/save), so we save it to a pending state.
if(!shouldKeepPreviousPage.value) { // If the user did not press cancel
if (!shouldKeepPreviousPage.value) {
// If the user did not press cancel
setPendingActivePage(valueToSet);
}
return valueToSet;
}
@@ -260,27 +353,27 @@ function getNavViewName() {
async function saveModal() {
// Help determine MAP/CONFORMANCE save with "submit" or "cancel".
// Notify Map to close the Sidebar.
emitter.emit('saveModal', false);
emitter.emit("saveModal", false);
switch (route.name) {
case 'Map':
await handleMapSave();
break;
case 'CheckMap':
await handleCheckMapSave();
break;
case 'Conformance':
case 'CheckConformance':
await handleConformanceSave();
break;
default:
break;
}
case "Map":
await handleMapSave();
break;
case "CheckMap":
await handleCheckMapSave();
break;
case "Conformance":
case "CheckConformance":
await handleConformanceSave();
break;
default:
break;
}
}
/** Sets nav item button background color when the active page is empty. */
function handleNavItemBtn() {
if(activePageComputedByRoute.value === "") {
if (activePageComputedByRoute.value === "") {
setActivePageComputedByRoute(route.matched[route.matched.length - 1].name);
}
}
@@ -290,13 +383,13 @@ async function handleMapSave() {
if (createFilterId.value) {
await allMapDataStore.updateFilter();
if (isUpdateFilter.value) {
await savedSuccessfully(filterName.value);
}
await savedSuccessfully(filterName.value);
}
} else if (logId.value) {
const isSaved = await saveFilter(allMapDataStore.addFilterId);
if (isSaved) {
setActivePage('MAP');
await router.push(`/discover/filter/${createFilterId.value}/map`);
setActivePage("MAP");
await router.push(`/discover/filter/${createFilterId.value}/map`);
}
}
}
@@ -305,43 +398,52 @@ async function handleMapSave() {
async function handleCheckMapSave() {
const isSaved = await saveFilter(allMapDataStore.addFilterId);
if (isSaved) {
setActivePage('MAP');
await router.push(`/discover/filter/${createFilterId.value}/map`);
setActivePage("MAP");
await router.push(`/discover/filter/${createFilterId.value}/map`);
}
}
/** Saves or updates conformance check data. */
async function handleConformanceSave() {
if (conformanceFilterCreateCheckId.value || conformanceLogCreateCheckId.value) {
await conformanceStore.updateConformance();
if (isUpdateConformance.value) {
await savedSuccessfully(conformanceFileName.value);
}
if (
conformanceFilterCreateCheckId.value ||
conformanceLogCreateCheckId.value
) {
await conformanceStore.updateConformance();
if (isUpdateConformance.value) {
await savedSuccessfully(conformanceFileName.value);
}
} else {
const isSaved = await saveConformance(conformanceStore.addConformanceCreateCheckId);
if (isSaved) {
if (conformanceLogId.value) {
setActivePage('CONFORMANCE');
await router.push(`/discover/conformance/log/${conformanceLogCreateCheckId.value}/conformance`);
} else if (conformanceFilterId.value) {
setActivePage('CONFORMANCE');
await router.push(`/discover/conformance/filter/${conformanceFilterCreateCheckId.value}/conformance`);
}
const isSaved = await saveConformance(
conformanceStore.addConformanceCreateCheckId,
);
if (isSaved) {
if (conformanceLogId.value) {
setActivePage("CONFORMANCE");
await router.push(
`/discover/conformance/log/${conformanceLogCreateCheckId.value}/conformance`,
);
} else if (conformanceFilterId.value) {
setActivePage("CONFORMANCE");
await router.push(
`/discover/conformance/filter/${conformanceFilterCreateCheckId.value}/conformance`,
);
}
}
}
}
onMounted(() => {
handleNavItemBtn();
if(route.params.type === 'filter') {
if (route.params.type === "filter") {
createFilterId.value = route.params.fileId;
}
showNavbarBreadcrumb.value = route.matched[0].name !== ('AuthContainer');
showNavbarBreadcrumb.value = route.matched[0].name !== "AuthContainer";
getNavViewName();
});
</script>
<style scoped>
#searchFiles::-webkit-search-cancel-button{
#searchFiles::-webkit-search-cancel-button {
appearance: none;
}
</style>

View File

@@ -2,12 +2,19 @@
<form role="search">
<label for="searchFiles" class="mr-4 relative" htmlFor="searchFiles">
Search
<input type="search" id="searchFiles" placeholder="Search Activity" class="px-5 py-2 w-52 rounded-full text-sm align-middle
duration-300 border bg-neutral-100 border-neutral-300 hover:border-neutral-500 focus:outline-none focus:ring
focus:border-neutral-500"/>
<span class="absolute top-2 bottom-0.5 right-0.5 flex justify-center items-center gap-2">
<input
type="search"
id="searchFiles"
placeholder="Search Activity"
class="px-5 py-2 w-52 rounded-full text-sm align-middle duration-300 border bg-neutral-100 border-neutral-300 hover:border-neutral-500 focus:outline-none focus:ring focus:border-neutral-500"
/>
<span
class="absolute top-2 bottom-0.5 right-0.5 flex justify-center items-center gap-2"
>
<IconSetting class="w-6 h-6 cursor-pointer"></IconSetting>
<span class="w-px h-6 block after:border after:border-neutral-300 after:content-['']"></span>
<span
class="w-px h-6 block after:border after:border-neutral-300 after:content-['']"
></span>
<button class="pr-2">
<IconSearch class="w-6 h-6"></IconSearch>
</button>
@@ -28,6 +35,6 @@
* search input, settings icon, and search icon button.
*/
import IconSearch from '@/components/icons/IconSearch.vue';
import IconSetting from '@/components/icons/IconSetting.vue';
import IconSearch from "@/components/icons/IconSearch.vue";
import IconSetting from "@/components/icons/IconSetting.vue";
</script>

View File

@@ -1,42 +1,52 @@
<template>
<div class="relative">
<div class="w-32 px-1 border border-neutral-500 cursor-pointer" @click="openTimeSelect = !openTimeSelect" :id="size">
<div v-if="totalSeconds === 0" class="text-center">
<p>0</p>
<div class="relative">
<div
class="w-32 px-1 border border-neutral-500 cursor-pointer"
@click="openTimeSelect = !openTimeSelect"
:id="size"
>
<div v-if="totalSeconds === 0" class="text-center">
<p>0</p>
</div>
<!-- This section shows the fixed time display, not the popup -->
<div
id="cyp-timerange-show"
v-else
class="flex justify-center items-center gap-1"
>
<p v-show="days != 0">{{ days }}d</p>
<p v-show="hours != 0">{{ hours }}h</p>
<p v-show="minutes != 0">{{ minutes }}m</p>
<p v-show="seconds != 0">{{ seconds }}s</p>
</div>
</div>
<!-- This section shows the fixed time display, not the popup -->
<div id="cyp-timerange-show" v-else class="flex justify-center items-center gap-1">
<p v-show="days != 0">{{ days }}d</p>
<p v-show="hours != 0">{{ hours }}h</p>
<p v-show="minutes != 0">{{ minutes }}m</p>
<p v-show="seconds != 0">{{ seconds }}s</p>
<!-- The following section is the popup that appears when the user clicks to open -->
<div
class="duration-container absolute left-0 top-full translate-y-2 dhms-input-popup-container"
v-show="openTimeSelect"
v-closable="{ id: size, handler: onClose }"
>
<div class="duration-box" v-for="(unit, index) in inputTypes" :key="unit">
<input
id="input_duration_dhms"
type="text"
class="duration duration-val input-dhms-field"
:data-index="index"
:data-tunit="unit"
:data-max="tUnits[unit].max"
:data-min="tUnits[unit].min"
:maxlength="tUnits[unit].dsp === 'd' ? 3 : 2"
@focus="onFocus"
@change="onChange"
@keyup="onKeyUp"
v-model="inputTimeFields[index]"
/>
<label class="duration" for="input_duration_dhms">{{
tUnits[unit].dsp
}}</label>
</div>
</div>
</div>
<!-- The following section is the popup that appears when the user clicks to open -->
<div class="duration-container absolute left-0 top-full translate-y-2
dhms-input-popup-container"
v-show="openTimeSelect"
v-closable="{id: size, handler: onClose}">
<div class="duration-box" v-for="(unit, index) in inputTypes" :key="unit">
<input
id="input_duration_dhms"
type="text"
class="duration duration-val input-dhms-field"
:data-index="index"
:data-tunit="unit"
:data-max="tUnits[unit].max"
:data-min="tUnits[unit].min"
:maxlength="tUnits[unit].dsp === 'd' ? 3 : 2"
@focus="onFocus"
@change="onChange"
@keyup="onKeyUp"
v-model="inputTimeFields[index]"
/>
<label class="duration" for="input_duration_dhms">{{ tUnits[unit].dsp }}</label>
</div>
</div>
</div>
</template>
<script setup>
@@ -52,8 +62,8 @@
* fields and bounded min/max validation.
*/
import { ref, computed, watch, onMounted } from 'vue';
import emitter from '@/utils/emitter';
import { ref, computed, watch, onMounted } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps({
max: {
@@ -97,12 +107,12 @@ const props = defineProps({
validator(value) {
return value >= 0;
},
}
},
});
const emit = defineEmits(['total-seconds']);
const emit = defineEmits(["total-seconds"]);
const display = ref('dhms');
const display = ref("dhms");
const seconds = ref(0);
const minutes = ref(0);
const hours = ref(0);
@@ -119,19 +129,34 @@ const openTimeSelect = ref(false);
const tUnits = computed({
get() {
return {
s: { dsp: 's', inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
m: { dsp: 'm', inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
h: { dsp: 'h', inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
d: { dsp: 'd', inc: 1, val: days.value, max: maxDays.value, rate: 86400, min: minDays.value }
s: { dsp: "s", inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
m: { dsp: "m", inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
h: { dsp: "h", inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
d: {
dsp: "d",
inc: 1,
val: days.value,
max: maxDays.value,
rate: 86400,
min: minDays.value,
},
};
},
set(newValues) {
for (const unit in newValues) {
switch (unit) {
case 's': seconds.value = newValues[unit].val; break;
case 'm': minutes.value = newValues[unit].val; break;
case 'h': hours.value = newValues[unit].val; break;
case 'd': days.value = newValues[unit].val; break;
case "s":
seconds.value = newValues[unit].val;
break;
case "m":
minutes.value = newValues[unit].val;
break;
case "h":
hours.value = newValues[unit].val;
break;
case "d":
days.value = newValues[unit].val;
break;
}
const input = document.querySelector(`[data-tunit="${unit}"]`);
if (input) {
@@ -143,8 +168,10 @@ const tUnits = computed({
const inputTimeFields = computed(() => {
const paddedTimeFields = [];
inputTypes.value.forEach(inputTypeUnit => {
paddedTimeFields.push(tUnits.value[inputTypeUnit].val.toString().padStart(2, '0'));
inputTypes.value.forEach((inputTypeUnit) => {
paddedTimeFields.push(
tUnits.value[inputTypeUnit].val.toString().padStart(2, "0"),
);
});
return paddedTimeFields;
});
@@ -170,8 +197,8 @@ function onFocus(event) {
function onChange(event) {
const baseInputValue = event.target.value;
let decoratedInputValue;
if(isNaN(event.target.value)){
event.target.value = '00';
if (isNaN(event.target.value)) {
event.target.value = "00";
} else {
event.target.value = event.target.value.toString();
}
@@ -180,27 +207,27 @@ function onChange(event) {
const inputValue = parseInt(event.target.value, 10);
const max = parseInt(event.target.dataset.max, 10);
const min = parseInt(event.target.dataset.min, 10);
if(inputValue > max) {
decoratedInputValue = max.toString().padStart(2, '0');
}else if(inputValue < min) {
decoratedInputValue= min.toString();
if (inputValue > max) {
decoratedInputValue = max.toString().padStart(2, "0");
} else if (inputValue < min) {
decoratedInputValue = min.toString();
}
const dsp = event.target.dataset.tunit;
tUnits.value[dsp].val = decoratedInputValue;
switch (dsp) {
case 'd':
case "d":
days.value = baseInputValue;
break;
case 'h':
case "h":
hours.value = decoratedInputValue;
break;
case 'm':
case "m":
minutes.value = decoratedInputValue;
break;
case 's':
case "s":
seconds.value = decoratedInputValue;
break;
};
}
calculateTotalSeconds();
}
@@ -210,10 +237,10 @@ function onChange(event) {
* @param {KeyboardEvent} event - The keyup event.
*/
function onKeyUp(event) {
event.target.value = event.target.value.replace(/\D/g, '');
event.target.value = event.target.value.replace(/\D/g, "");
if (event.keyCode === 38 || event.keyCode === 40) {
actionUpDown(event.target, event.keyCode === 38, true);
};
}
}
/**
@@ -247,7 +274,7 @@ function getNewValue(input) {
function handleArrowUp(newVal, tUnit, input) {
newVal += tUnits.value[tUnit].inc;
if (newVal > tUnits.value[tUnit].max) {
if (tUnits.value[tUnit].dsp === 'd') {
if (tUnits.value[tUnit].dsp === "d") {
totalSeconds.value = maxTotal.value;
} else {
newVal = newVal % (tUnits.value[tUnit].max + 1);
@@ -260,14 +287,16 @@ function handleArrowUp(newVal, tUnit, input) {
function handleArrowDown(newVal, tUnit) {
newVal -= tUnits.value[tUnit].inc;
if (newVal < 0) {
newVal = (tUnits.value[tUnit].max + 1) - tUnits.value[tUnit].inc;
newVal = tUnits.value[tUnit].max + 1 - tUnits.value[tUnit].inc;
}
return newVal;
}
function incrementPreviousUnit(input) {
if (input.dataset.index > 0) {
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
const prevUnit = document.querySelector(
`input[data-index="${parseInt(input.dataset.index) - 1}"]`,
);
actionUpDown(prevUnit, true);
}
}
@@ -275,16 +304,16 @@ function incrementPreviousUnit(input) {
function updateInputValue(input, newVal, tUnit) {
input.value = newVal.toString();
switch (tUnit) {
case 'd':
case "d":
days.value = input.value;
break;
case 'h':
case "h":
hours.value = input.value;
break;
case 'm':
case "m":
minutes.value = input.value;
break;
case 's':
case "s":
seconds.value = input.value;
break;
}
@@ -297,19 +326,18 @@ function updateInputValue(input, newVal, tUnit) {
*/
function secondToDate(totalSec, size) {
totalSec = parseInt(totalSec);
if(!isNaN(totalSec)) {
if (!isNaN(totalSec)) {
seconds.value = totalSec % 60;
minutes.value = (Math.floor(totalSec - seconds.value) / 60) % 60;
hours.value = (Math.floor(totalSec / 3600)) % 24;
hours.value = Math.floor(totalSec / 3600) % 24;
days.value = Math.floor(totalSec / (3600 * 24));
if(size === 'max') {
if (size === "max") {
maxDays.value = Math.floor(totalSec / (3600 * 24));
}
else if(size === 'min') {
} else if (size === "min") {
minDays.value = Math.floor(totalSec / (3600 * 24));
}
};
}
}
/** Calculates total seconds from all duration units and emits the result. */
@@ -323,39 +351,39 @@ function calculateTotalSeconds() {
}
}
if(total >= maxTotal.value){
if (total >= maxTotal.value) {
total = maxTotal.value;
secondToDate(maxTotal.value, 'max');
secondToDate(maxTotal.value, "max");
} else if (total <= minTotal.value) {
total = minTotal.value;
secondToDate(minTotal.value, 'min');
} else if((props.size === 'min' && total <= maxTotal.value)) {
secondToDate(minTotal.value, "min");
} else if (props.size === "min" && total <= maxTotal.value) {
maxDays.value = Math.floor(maxTotal.value / (3600 * 24));
}
totalSeconds.value = total;
emit('total-seconds', total);
emit("total-seconds", total);
}
/** Initializes the duration display based on min/max boundaries and preset value. */
async function createData() {
const size = props.size;
if (maxTotal.value !== await null && minTotal.value !== await null) {
if (maxTotal.value !== (await null) && minTotal.value !== (await null)) {
switch (size) {
case 'max':
secondToDate(minTotal.value, 'min');
secondToDate(maxTotal.value, 'max');
case "max":
secondToDate(minTotal.value, "min");
secondToDate(maxTotal.value, "max");
totalSeconds.value = maxTotal.value;
if(props.value !== null) {
if (props.value !== null) {
totalSeconds.value = props.value;
secondToDate(props.value);
}
break;
case 'min':
secondToDate(maxTotal.value, 'max');
secondToDate(minTotal.value, 'min');
case "min":
secondToDate(maxTotal.value, "max");
secondToDate(minTotal.value, "min");
totalSeconds.value = minTotal.value;
if(props.value !== null) {
if (props.value !== null) {
totalSeconds.value = props.value;
secondToDate(props.value);
}
@@ -365,33 +393,33 @@ async function createData() {
}
// created
emitter.on('reset', () => {
emitter.on("reset", () => {
createData();
});
// mounted
onMounted(() => {
inputTypes.value = display.value.split('');
inputTypes.value = display.value.split("");
});
const vClosable = {
mounted(el, {value}) {
const handleOutsideClick = function(e) {
mounted(el, { value }) {
const handleOutsideClick = function (e) {
let target = e.target;
while (target && target.id !== value.id) {
target = target.parentElement;
};
const isClickOutside = target?.id !== value.id && !el.contains(e.target)
}
const isClickOutside = target?.id !== value.id && !el.contains(e.target);
if (isClickOutside) {
value.handler();
}
e.stopPropagation();
}
document.addEventListener('click', handleOutsideClick);
};
document.addEventListener("click", handleOutsideClick);
el._handleOutsideClick = handleOutsideClick;
},
unmounted(el) {
document.removeEventListener('click', el._handleOutsideClick);
document.removeEventListener("click", el._handleOutsideClick);
},
};
</script>
@@ -399,55 +427,55 @@ const vClosable = {
.duration-container {
margin: 4px;
border-radius: 4px;
background: var(--10, #FFF);
background: var(--10, #fff);
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.25);
height: 36px;
width: 221px;
}
.duration-box {
float:left;
overflow: auto;
height: var(--main-input-height);
padding: 4px;
float: left;
overflow: auto;
height: var(--main-input-height);
padding: 4px;
}
.duration-box > .duration {
border: 1px solid var(--main-bg-light);
border-right: 0;
border-left: 0;
background-color:transparent;
color: var(--main-bg-light);
border: 1px solid var(--main-bg-light);
border-right: 0;
border-left: 0;
background-color: transparent;
color: var(--main-bg-light);
}
.duration-box > .duration:nth-child(1) {
border-left: 1px solid var(--main-bg-light);
border-top-left-radius: var(--main-input-br);
border-bottom-left-radius: var(--main-input-br);
border-left: 1px solid var(--main-bg-light);
border-top-left-radius: var(--main-input-br);
border-bottom-left-radius: var(--main-input-br);
}
.duration-box > .duration:nth-last-child(1) {
border-right: 1px solid var(--main-bg-light);
border-top-right-radius: var(--main-input-br);
border-bottom-right-radius: var(--main-input-br);
border-right: 1px solid var(--main-bg-light);
border-top-right-radius: var(--main-input-br);
border-bottom-right-radius: var(--main-input-br);
}
.duration {
float:left;
display: block;
overflow: auto;
height: var(--main-input-height);
outline: none;
font-size: 14px;
margin: 0px 2px;
float: left;
display: block;
overflow: auto;
height: var(--main-input-height);
outline: none;
font-size: 14px;
margin: 0px 2px;
}
.duration-box > label.duration {
line-height: 28px;
width: 12px;
overflow: hidden;
line-height: 28px;
width: 12px;
overflow: hidden;
}
.duration-box > input[type="text"].duration {
text-align: right;
width: 26px;
padding: 3px 2px 0px 0px;
text-align: right;
width: 26px;
padding: 3px 2px 0px 0px;
}
.duration-box > input[type="button"].duration {
width: 60px;
cursor: pointer;
width: 60px;
cursor: pointer;
}
</style>

View File

@@ -1,11 +1,27 @@
<template>
<div
class="relative two-imgs-container w-[18px] h-[24px] cursor-pointer mt-1 mr-2"> <!-- A relative div containing two absolutely positioned img elements -->
<img v-if="!isChecked" :src="ImgCheckboxGrayFrame" class="absolute" alt="checkbox"/>
<img v-if="isChecked" :src="ImgCheckboxBlueFrame" class="absolute" alt="checkbox"/>
<img v-if="isChecked" :src="ImgCheckboxCheckedMark" class="absolute top-[11x] left-[2px] h-[16px] w-[14px]" alt="checkbox"/>
</div>
<div
class="relative two-imgs-container w-[18px] h-[24px] cursor-pointer mt-1 mr-2"
>
<!-- A relative div containing two absolutely positioned img elements -->
<img
v-if="!isChecked"
:src="ImgCheckboxGrayFrame"
class="absolute"
alt="checkbox"
/>
<img
v-if="isChecked"
:src="ImgCheckboxBlueFrame"
class="absolute"
alt="checkbox"
/>
<img
v-if="isChecked"
:src="ImgCheckboxCheckedMark"
class="absolute top-[11x] left-[2px] h-[16px] w-[14px]"
alt="checkbox"
/>
</div>
</template>
<script setup>
@@ -24,9 +40,9 @@ import ImgCheckboxCheckedMark from "@/assets/icon-checkbox-checked.svg";
import ImgCheckboxGrayFrame from "@/assets/icon-checkbox-empty.svg";
defineProps({
isChecked: {
type: Boolean,
required: true,
},
isChecked: {
type: Boolean,
required: true,
},
});
</script>

View File

@@ -3,10 +3,24 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M28.8571 31H3.14286C2.57471 30.9994 2.03 30.7735 1.62825 30.3717C1.22651 29.97 1.00057 29.4253 1 28.8571V3.14286C1.00057 2.57471 1.22651 2.03 1.62825 1.62825C2.03 1.22651 2.57471 1.00057 3.14286 1H28.8571C29.4253 1.00057 29.97 1.22651 30.3717 1.62825C30.7735 2.03 30.9994 2.57471 31 3.14286V28.8571C30.9994 29.4253 30.7735 29.97 30.3717 30.3717C29.97 30.7735 29.4253 30.9994 28.8571 31ZM3.14286 3.14286V28.8571H28.8571V3.14286H3.14286Z"/>
<path d="M11.1092 19.1541V20.2689H7.01401V19.1541H11.1092ZM7.40616 12.112V20.2689H6V12.112H7.40616Z"/>
<path d="M18.3305 15.9664V16.4146C18.3305 17.0308 18.2502 17.5836 18.0896 18.0728C17.929 18.5621 17.6993 18.9785 17.4006 19.3221C17.1055 19.6657 16.7507 19.929 16.3361 20.112C15.9216 20.2913 15.4622 20.381 14.958 20.381C14.4575 20.381 14 20.2913 13.5854 20.112C13.1746 19.929 12.8179 19.6657 12.5154 19.3221C12.2129 18.9785 11.9776 18.5621 11.8095 18.0728C11.6452 17.5836 11.563 17.0308 11.563 16.4146V15.9664C11.563 15.3501 11.6452 14.7993 11.8095 14.3137C11.9739 13.8245 12.2054 13.408 12.5042 13.0644C12.8067 12.7171 13.1634 12.4538 13.5742 12.2745C13.9888 12.0915 14.4463 12 14.9468 12C15.451 12 15.9104 12.0915 16.3249 12.2745C16.7395 12.4538 17.0962 12.7171 17.395 13.0644C17.6937 13.408 17.9234 13.8245 18.084 14.3137C18.2484 14.7993 18.3305 15.3501 18.3305 15.9664ZM16.9244 16.4146V15.9552C16.9244 15.4995 16.8796 15.098 16.7899 14.7507C16.704 14.3996 16.5752 14.1064 16.4034 13.8711C16.2353 13.6321 16.028 13.4528 15.7815 13.3333C15.535 13.2101 15.2568 13.1485 14.9468 13.1485C14.6368 13.1485 14.3604 13.2101 14.1176 13.3333C13.8749 13.4528 13.6676 13.6321 13.4958 13.8711C13.3277 14.1064 13.1989 14.3996 13.1092 14.7507C13.0196 15.098 12.9748 15.4995 12.9748 15.9552V16.4146C12.9748 16.8702 13.0196 17.2736 13.1092 17.6247C13.1989 17.9757 13.3296 18.2726 13.5014 18.5154C13.6769 18.7544 13.8861 18.9356 14.1289 19.0588C14.3716 19.1783 14.648 19.2381 14.958 19.2381C15.2717 19.2381 15.55 19.1783 15.7927 19.0588C16.0355 18.9356 16.2409 18.7544 16.409 18.5154C16.577 18.2726 16.704 17.9757 16.7899 17.6247C16.8796 17.2736 16.9244 16.8702 16.9244 16.4146Z"/>
<path d="M26 16.1008V19.2157C25.8842 19.3688 25.7031 19.5369 25.4566 19.7199C25.2138 19.8992 24.8908 20.0542 24.4874 20.1849C24.084 20.3156 23.5817 20.381 22.9804 20.381C22.4687 20.381 22 20.2951 21.5742 20.1233C21.1485 19.9477 20.7806 19.6919 20.4706 19.3557C20.1643 19.0196 19.9272 18.6106 19.7591 18.1289C19.591 17.6433 19.507 17.0906 19.507 16.4706V15.9048C19.507 15.2885 19.5836 14.7395 19.7367 14.2577C19.8936 13.7722 20.1176 13.3613 20.409 13.0252C20.7003 12.6891 21.0514 12.4351 21.4622 12.2633C21.8768 12.0878 22.3455 12 22.8683 12C23.5369 12 24.0896 12.112 24.5266 12.3361C24.9673 12.5565 25.3072 12.8627 25.5462 13.2549C25.7852 13.6471 25.9365 14.0952 26 14.5994H24.6218C24.577 14.3156 24.4893 14.0616 24.3585 13.8375C24.2316 13.6134 24.0486 13.4379 23.8095 13.3109C23.5742 13.1802 23.268 13.1148 22.8908 13.1148C22.5658 13.1148 22.2801 13.1765 22.0336 13.2997C21.7871 13.423 21.5817 13.6041 21.4174 13.8431C21.2568 14.0822 21.1354 14.3735 21.0532 14.7171C20.9711 15.0607 20.93 15.4528 20.93 15.8936V16.4706C20.93 16.9188 20.9767 17.3165 21.07 17.6639C21.1671 18.0112 21.3053 18.3044 21.4846 18.5434C21.6676 18.7824 21.8898 18.9636 22.1513 19.0868C22.4127 19.2063 22.7078 19.2661 23.0364 19.2661C23.3576 19.2661 23.6209 19.24 23.8263 19.1877C24.0317 19.1317 24.1942 19.0663 24.3137 18.9916C24.437 18.9132 24.5322 18.8385 24.5994 18.7675V17.1485H22.902V16.1008H26Z"/>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="black"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M28.8571 31H3.14286C2.57471 30.9994 2.03 30.7735 1.62825 30.3717C1.22651 29.97 1.00057 29.4253 1 28.8571V3.14286C1.00057 2.57471 1.22651 2.03 1.62825 1.62825C2.03 1.22651 2.57471 1.00057 3.14286 1H28.8571C29.4253 1.00057 29.97 1.22651 30.3717 1.62825C30.7735 2.03 30.9994 2.57471 31 3.14286V28.8571C30.9994 29.4253 30.7735 29.97 30.3717 30.3717C29.97 30.7735 29.4253 30.9994 28.8571 31ZM3.14286 3.14286V28.8571H28.8571V3.14286H3.14286Z"
/>
<path
d="M11.1092 19.1541V20.2689H7.01401V19.1541H11.1092ZM7.40616 12.112V20.2689H6V12.112H7.40616Z"
/>
<path
d="M18.3305 15.9664V16.4146C18.3305 17.0308 18.2502 17.5836 18.0896 18.0728C17.929 18.5621 17.6993 18.9785 17.4006 19.3221C17.1055 19.6657 16.7507 19.929 16.3361 20.112C15.9216 20.2913 15.4622 20.381 14.958 20.381C14.4575 20.381 14 20.2913 13.5854 20.112C13.1746 19.929 12.8179 19.6657 12.5154 19.3221C12.2129 18.9785 11.9776 18.5621 11.8095 18.0728C11.6452 17.5836 11.563 17.0308 11.563 16.4146V15.9664C11.563 15.3501 11.6452 14.7993 11.8095 14.3137C11.9739 13.8245 12.2054 13.408 12.5042 13.0644C12.8067 12.7171 13.1634 12.4538 13.5742 12.2745C13.9888 12.0915 14.4463 12 14.9468 12C15.451 12 15.9104 12.0915 16.3249 12.2745C16.7395 12.4538 17.0962 12.7171 17.395 13.0644C17.6937 13.408 17.9234 13.8245 18.084 14.3137C18.2484 14.7993 18.3305 15.3501 18.3305 15.9664ZM16.9244 16.4146V15.9552C16.9244 15.4995 16.8796 15.098 16.7899 14.7507C16.704 14.3996 16.5752 14.1064 16.4034 13.8711C16.2353 13.6321 16.028 13.4528 15.7815 13.3333C15.535 13.2101 15.2568 13.1485 14.9468 13.1485C14.6368 13.1485 14.3604 13.2101 14.1176 13.3333C13.8749 13.4528 13.6676 13.6321 13.4958 13.8711C13.3277 14.1064 13.1989 14.3996 13.1092 14.7507C13.0196 15.098 12.9748 15.4995 12.9748 15.9552V16.4146C12.9748 16.8702 13.0196 17.2736 13.1092 17.6247C13.1989 17.9757 13.3296 18.2726 13.5014 18.5154C13.6769 18.7544 13.8861 18.9356 14.1289 19.0588C14.3716 19.1783 14.648 19.2381 14.958 19.2381C15.2717 19.2381 15.55 19.1783 15.7927 19.0588C16.0355 18.9356 16.2409 18.7544 16.409 18.5154C16.577 18.2726 16.704 17.9757 16.7899 17.6247C16.8796 17.2736 16.9244 16.8702 16.9244 16.4146Z"
/>
<path
d="M26 16.1008V19.2157C25.8842 19.3688 25.7031 19.5369 25.4566 19.7199C25.2138 19.8992 24.8908 20.0542 24.4874 20.1849C24.084 20.3156 23.5817 20.381 22.9804 20.381C22.4687 20.381 22 20.2951 21.5742 20.1233C21.1485 19.9477 20.7806 19.6919 20.4706 19.3557C20.1643 19.0196 19.9272 18.6106 19.7591 18.1289C19.591 17.6433 19.507 17.0906 19.507 16.4706V15.9048C19.507 15.2885 19.5836 14.7395 19.7367 14.2577C19.8936 13.7722 20.1176 13.3613 20.409 13.0252C20.7003 12.6891 21.0514 12.4351 21.4622 12.2633C21.8768 12.0878 22.3455 12 22.8683 12C23.5369 12 24.0896 12.112 24.5266 12.3361C24.9673 12.5565 25.3072 12.8627 25.5462 13.2549C25.7852 13.6471 25.9365 14.0952 26 14.5994H24.6218C24.577 14.3156 24.4893 14.0616 24.3585 13.8375C24.2316 13.6134 24.0486 13.4379 23.8095 13.3109C23.5742 13.1802 23.268 13.1148 22.8908 13.1148C22.5658 13.1148 22.2801 13.1765 22.0336 13.2997C21.7871 13.423 21.5817 13.6041 21.4174 13.8431C21.2568 14.0822 21.1354 14.3735 21.0532 14.7171C20.9711 15.0607 20.93 15.4528 20.93 15.8936V16.4706C20.93 16.9188 20.9767 17.3165 21.07 17.6639C21.1671 18.0112 21.3053 18.3044 21.4846 18.5434C21.6676 18.7824 21.8898 18.9636 22.1513 19.0868C22.4127 19.2063 22.7078 19.2661 23.0364 19.2661C23.3576 19.2661 23.6209 19.24 23.8263 19.1877C24.0317 19.1317 24.1942 19.0663 24.3137 18.9916C24.437 18.9132 24.5322 18.8385 24.5994 18.7675V17.1485H22.902V16.1008H26Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.0529 3.24373C4.9174 3.10198 4.73177 3.01893 4.53578 3.01238C4.33979 3.00583 4.14903 3.07631 4.00437 3.20871C3.85971 3.34111 3.77266 3.5249 3.76187 3.7207C3.75108 3.9165 3.81741 4.10874 3.94665 4.25624L5.74665 6.23436C2.34352 8.32498 0.881025 11.55 0.8154 11.7C0.773866 11.7962 0.752441 11.8999 0.752441 12.0047C0.752441 12.1095 0.773866 12.2132 0.8154 12.3094C0.843525 12.375 1.6404 14.1375 3.4029 15.9094C5.75602 18.2531 8.7279 19.5 11.9998 19.5C13.6807 19.5067 15.3444 19.1618 16.8842 18.4875L18.9467 20.7562C19.0169 20.833 19.1023 20.8943 19.1976 20.9363C19.2928 20.9783 19.3957 21 19.4998 21C19.6869 20.9982 19.8669 20.9282 20.006 20.8031C20.0797 20.7373 20.1396 20.6574 20.1819 20.5681C20.2243 20.4789 20.2484 20.382 20.2528 20.2833C20.2571 20.1846 20.2417 20.086 20.2074 19.9933C20.173 19.9007 20.1205 19.8158 20.0529 19.7437L5.0529 3.24373ZM9.48727 10.3594L13.3966 14.6531C12.9666 14.8818 12.4868 15.0009 11.9998 15C11.4591 15.0001 10.9284 14.8542 10.4639 14.5775C9.99933 14.3009 9.61819 13.9038 9.36076 13.4283C9.10334 12.9528 8.97919 12.4166 9.00146 11.8764C9.02373 11.3362 9.19159 10.812 9.48727 10.3594V10.3594ZM11.9998 18C9.11228 18 6.5904 16.95 4.50915 14.8781C3.64691 14.0329 2.91685 13.0626 2.34352 12C2.78415 11.175 4.19978 8.85936 6.7779 7.36873L8.4654 9.22499C7.81494 10.0615 7.48054 11.1007 7.52112 12.1596C7.5617 13.2185 7.97465 14.2291 8.68723 15.0134C9.39981 15.7976 10.3663 16.3053 11.4164 16.4469C12.4666 16.5885 13.533 16.355 14.4279 15.7875L15.806 17.3062C14.5911 17.771 13.3005 18.0063 11.9998 18V18ZM23.1842 12.3094C23.1467 12.3937 22.1998 14.4937 20.0529 16.4156C19.9152 16.5359 19.7388 16.6025 19.556 16.6031C19.4509 16.6044 19.3467 16.5823 19.2511 16.5385C19.1555 16.4948 19.0707 16.4304 19.0029 16.35C18.937 16.2767 18.8862 16.1912 18.8534 16.0983C18.8205 16.0055 18.8063 15.907 18.8116 15.8086C18.8168 15.7103 18.8413 15.6139 18.8838 15.525C18.9263 15.4361 18.9859 15.3565 19.0592 15.2906C20.107 14.3507 20.9854 13.2376 21.656 12C21.0811 10.9356 20.3513 9.96244 19.4904 9.11248C17.4091 7.04998 14.8873 5.99999 11.9998 5.99999C11.3903 5.99753 10.7818 6.04772 10.181 6.14998C9.98493 6.17994 9.78489 6.13197 9.62371 6.01634C9.46252 5.90071 9.35299 5.72659 9.31852 5.53124C9.30244 5.43397 9.30569 5.33448 9.32809 5.23847C9.35049 5.14246 9.3916 5.0518 9.44907 4.9717C9.50654 4.89159 9.57924 4.8236 9.66301 4.77161C9.74678 4.71963 9.83998 4.68467 9.93727 4.66873C10.6189 4.55517 11.3088 4.49873 11.9998 4.49998C15.2717 4.49998 18.2435 5.74686 20.5966 8.09061C22.3591 9.86249 23.156 11.625 23.1842 11.7C23.2257 11.7962 23.2471 11.8999 23.2471 12.0047C23.2471 12.1095 23.2257 12.2132 23.1842 12.3094V12.3094ZM12.5623 9.05624C12.465 9.03777 12.3723 9.00032 12.2896 8.94604C12.2068 8.89176 12.1355 8.82171 12.0798 8.73987C11.9672 8.57461 11.925 8.37141 11.9623 8.17498C11.9996 7.97856 12.1134 7.80499 12.2786 7.69247C12.4439 7.57995 12.6471 7.53769 12.8435 7.57499C13.7999 7.76202 14.6703 8.25257 15.3257 8.97375C15.981 9.69494 16.3862 10.6083 16.481 11.5781C16.4993 11.7757 16.4385 11.9725 16.312 12.1254C16.1855 12.2782 16.0035 12.3747 15.806 12.3937H15.731C15.5455 12.3945 15.3664 12.3255 15.2292 12.2005C15.0921 12.0755 15.0068 11.9036 14.9904 11.7187C14.9257 11.0729 14.6546 10.4651 14.2172 9.98546C13.7798 9.50585 13.1995 9.18 12.5623 9.05624V9.05624Z" fill="#C4C4C4"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.0529 3.24373C4.9174 3.10198 4.73177 3.01893 4.53578 3.01238C4.33979 3.00583 4.14903 3.07631 4.00437 3.20871C3.85971 3.34111 3.77266 3.5249 3.76187 3.7207C3.75108 3.9165 3.81741 4.10874 3.94665 4.25624L5.74665 6.23436C2.34352 8.32498 0.881025 11.55 0.8154 11.7C0.773866 11.7962 0.752441 11.8999 0.752441 12.0047C0.752441 12.1095 0.773866 12.2132 0.8154 12.3094C0.843525 12.375 1.6404 14.1375 3.4029 15.9094C5.75602 18.2531 8.7279 19.5 11.9998 19.5C13.6807 19.5067 15.3444 19.1618 16.8842 18.4875L18.9467 20.7562C19.0169 20.833 19.1023 20.8943 19.1976 20.9363C19.2928 20.9783 19.3957 21 19.4998 21C19.6869 20.9982 19.8669 20.9282 20.006 20.8031C20.0797 20.7373 20.1396 20.6574 20.1819 20.5681C20.2243 20.4789 20.2484 20.382 20.2528 20.2833C20.2571 20.1846 20.2417 20.086 20.2074 19.9933C20.173 19.9007 20.1205 19.8158 20.0529 19.7437L5.0529 3.24373ZM9.48727 10.3594L13.3966 14.6531C12.9666 14.8818 12.4868 15.0009 11.9998 15C11.4591 15.0001 10.9284 14.8542 10.4639 14.5775C9.99933 14.3009 9.61819 13.9038 9.36076 13.4283C9.10334 12.9528 8.97919 12.4166 9.00146 11.8764C9.02373 11.3362 9.19159 10.812 9.48727 10.3594V10.3594ZM11.9998 18C9.11228 18 6.5904 16.95 4.50915 14.8781C3.64691 14.0329 2.91685 13.0626 2.34352 12C2.78415 11.175 4.19978 8.85936 6.7779 7.36873L8.4654 9.22499C7.81494 10.0615 7.48054 11.1007 7.52112 12.1596C7.5617 13.2185 7.97465 14.2291 8.68723 15.0134C9.39981 15.7976 10.3663 16.3053 11.4164 16.4469C12.4666 16.5885 13.533 16.355 14.4279 15.7875L15.806 17.3062C14.5911 17.771 13.3005 18.0063 11.9998 18V18ZM23.1842 12.3094C23.1467 12.3937 22.1998 14.4937 20.0529 16.4156C19.9152 16.5359 19.7388 16.6025 19.556 16.6031C19.4509 16.6044 19.3467 16.5823 19.2511 16.5385C19.1555 16.4948 19.0707 16.4304 19.0029 16.35C18.937 16.2767 18.8862 16.1912 18.8534 16.0983C18.8205 16.0055 18.8063 15.907 18.8116 15.8086C18.8168 15.7103 18.8413 15.6139 18.8838 15.525C18.9263 15.4361 18.9859 15.3565 19.0592 15.2906C20.107 14.3507 20.9854 13.2376 21.656 12C21.0811 10.9356 20.3513 9.96244 19.4904 9.11248C17.4091 7.04998 14.8873 5.99999 11.9998 5.99999C11.3903 5.99753 10.7818 6.04772 10.181 6.14998C9.98493 6.17994 9.78489 6.13197 9.62371 6.01634C9.46252 5.90071 9.35299 5.72659 9.31852 5.53124C9.30244 5.43397 9.30569 5.33448 9.32809 5.23847C9.35049 5.14246 9.3916 5.0518 9.44907 4.9717C9.50654 4.89159 9.57924 4.8236 9.66301 4.77161C9.74678 4.71963 9.83998 4.68467 9.93727 4.66873C10.6189 4.55517 11.3088 4.49873 11.9998 4.49998C15.2717 4.49998 18.2435 5.74686 20.5966 8.09061C22.3591 9.86249 23.156 11.625 23.1842 11.7C23.2257 11.7962 23.2471 11.8999 23.2471 12.0047C23.2471 12.1095 23.2257 12.2132 23.1842 12.3094V12.3094ZM12.5623 9.05624C12.465 9.03777 12.3723 9.00032 12.2896 8.94604C12.2068 8.89176 12.1355 8.82171 12.0798 8.73987C11.9672 8.57461 11.925 8.37141 11.9623 8.17498C11.9996 7.97856 12.1134 7.80499 12.2786 7.69247C12.4439 7.57995 12.6471 7.53769 12.8435 7.57499C13.7999 7.76202 14.6703 8.25257 15.3257 8.97375C15.981 9.69494 16.3862 10.6083 16.481 11.5781C16.4993 11.7757 16.4385 11.9725 16.312 12.1254C16.1855 12.2782 16.0035 12.3747 15.806 12.3937H15.731C15.5455 12.3945 15.3664 12.3255 15.2292 12.2005C15.0921 12.0755 15.0068 11.9036 14.9904 11.7187C14.9257 11.0729 14.6546 10.4651 14.2172 9.98546C13.7798 9.50585 13.1995 9.18 12.5623 9.05624V9.05624Z"
fill="#C4C4C4"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.1842 11.7C23.1561 11.625 22.3592 9.8625 20.5967 8.09063C18.2436 5.74688 15.2717 4.5 11.9998 4.5C8.72793 4.5 5.75606 5.74688 3.40293 8.09063C1.64043 9.8625 0.843558 11.625 0.815433 11.7C0.773576 11.7945 0.751953 11.8967 0.751953 12C0.751953 12.1033 0.773576 12.2055 0.815433 12.3C0.843558 12.375 1.64043 14.1375 3.40293 15.9094C5.75606 18.2531 8.72793 19.5 11.9998 19.5C15.2717 19.5 18.2436 18.2531 20.5967 15.9094C22.3592 14.1375 23.1561 12.375 23.1842 12.3C23.226 12.2055 23.2477 12.1033 23.2477 12C23.2477 11.8967 23.226 11.7945 23.1842 11.7ZM11.9998 18C9.11231 18 6.59043 16.95 4.50918 14.8781C3.64906 14.031 2.91925 13.0611 2.34356 12C2.91847 10.9356 3.64831 9.96245 4.50918 9.1125C6.59043 7.05 9.11231 6 11.9998 6C14.8873 6 17.4092 7.05 19.4904 9.1125C20.3513 9.96245 21.0811 10.9356 21.6561 12C20.9811 13.2656 18.0373 18 11.9998 18ZM11.9998 7.5C11.1098 7.5 10.2398 7.76392 9.49974 8.25839C8.75972 8.75285 8.18295 9.45566 7.84235 10.2779C7.50176 11.1002 7.41264 12.005 7.58628 12.8779C7.75991 13.7508 8.18849 14.5526 8.81783 15.182C9.44716 15.8113 10.249 16.2399 11.1219 16.4135C11.9948 16.5872 12.8996 16.4981 13.7219 16.1575C14.5442 15.8169 15.247 15.2401 15.7414 14.5001C16.2359 13.76 16.4998 12.89 16.4998 12C16.4998 10.8065 16.0257 9.66193 15.1818 8.81802C14.3379 7.97411 13.1933 7.5 11.9998 7.5ZM11.9998 15C11.4065 15 10.8264 14.8241 10.3331 14.4944C9.83975 14.1648 9.45523 13.6962 9.22817 13.1481C9.00111 12.5999 8.9417 11.9967 9.05745 11.4147C9.17321 10.8328 9.45893 10.2982 9.87849 9.87868C10.298 9.45912 10.8326 9.1734 11.4145 9.05764C11.9965 8.94189 12.5997 9.0013 13.1479 9.22836C13.696 9.45542 14.1646 9.83994 14.4942 10.3333C14.8239 10.8266 14.9998 11.4067 14.9998 12C14.9973 12.7949 14.6805 13.5565 14.1184 14.1186C13.5563 14.6807 12.7947 14.9975 11.9998 15Z" fill="#0099FF"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.1842 11.7C23.1561 11.625 22.3592 9.8625 20.5967 8.09063C18.2436 5.74688 15.2717 4.5 11.9998 4.5C8.72793 4.5 5.75606 5.74688 3.40293 8.09063C1.64043 9.8625 0.843558 11.625 0.815433 11.7C0.773576 11.7945 0.751953 11.8967 0.751953 12C0.751953 12.1033 0.773576 12.2055 0.815433 12.3C0.843558 12.375 1.64043 14.1375 3.40293 15.9094C5.75606 18.2531 8.72793 19.5 11.9998 19.5C15.2717 19.5 18.2436 18.2531 20.5967 15.9094C22.3592 14.1375 23.1561 12.375 23.1842 12.3C23.226 12.2055 23.2477 12.1033 23.2477 12C23.2477 11.8967 23.226 11.7945 23.1842 11.7ZM11.9998 18C9.11231 18 6.59043 16.95 4.50918 14.8781C3.64906 14.031 2.91925 13.0611 2.34356 12C2.91847 10.9356 3.64831 9.96245 4.50918 9.1125C6.59043 7.05 9.11231 6 11.9998 6C14.8873 6 17.4092 7.05 19.4904 9.1125C20.3513 9.96245 21.0811 10.9356 21.6561 12C20.9811 13.2656 18.0373 18 11.9998 18ZM11.9998 7.5C11.1098 7.5 10.2398 7.76392 9.49974 8.25839C8.75972 8.75285 8.18295 9.45566 7.84235 10.2779C7.50176 11.1002 7.41264 12.005 7.58628 12.8779C7.75991 13.7508 8.18849 14.5526 8.81783 15.182C9.44716 15.8113 10.249 16.2399 11.1219 16.4135C11.9948 16.5872 12.8996 16.4981 13.7219 16.1575C14.5442 15.8169 15.247 15.2401 15.7414 14.5001C16.2359 13.76 16.4998 12.89 16.4998 12C16.4998 10.8065 16.0257 9.66193 15.1818 8.81802C14.3379 7.97411 13.1933 7.5 11.9998 7.5ZM11.9998 15C11.4065 15 10.8264 14.8241 10.3331 14.4944C9.83975 14.1648 9.45523 13.6962 9.22817 13.1481C9.00111 12.5999 8.9417 11.9967 9.05745 11.4147C9.17321 10.8328 9.45893 10.2982 9.87849 9.87868C10.298 9.45912 10.8326 9.1734 11.4145 9.05764C11.9965 8.94189 12.5997 9.0013 13.1479 9.22836C13.696 9.45542 14.1646 9.83994 14.4942 10.3333C14.8239 10.8266 14.9998 11.4067 14.9998 12C14.9973 12.7949 14.6805 13.5565 14.1184 14.1186C13.5563 14.6807 12.7947 14.9975 11.9998 15Z"
fill="#0099FF"
/>
</svg>
</template>

View File

@@ -3,14 +3,24 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2381_1348)">
<path d="M5.67116 32C4.61918 31.9997 3.58801 31.7069 2.69286 31.1544C1.79771 30.6018 1.07385 29.8111 0.60217 28.8708C0.130493 27.9305 -0.0704207 26.8776 0.0218796 25.8297C0.11418 24.7818 0.496058 23.7802 1.12484 22.9368C1.75362 22.0934 2.60454 21.4415 3.5825 21.0539C4.56046 20.6663 5.62695 20.5582 6.66277 20.7419C7.69859 20.9255 8.66295 21.3935 9.44809 22.0937C10.2332 22.7938 10.8082 23.6985 11.1088 24.7066H20.2579V21.4652H23.4994V12.1329L19.864 8.49919H10.5334V11.7407H0.80892V2.01621H10.5334V5.2577H19.864L25.1201 0L31.9969 6.87844L26.7408 12.1313V21.4652H29.9823V31.1896H20.2579V27.9481H11.1088C10.7594 29.1189 10.0415 30.1457 9.06182 30.8757C8.08212 31.6057 6.89294 32.0001 5.67116 32ZM5.67116 23.8963C5.35179 23.8964 5.03557 23.9594 4.74056 24.0817C4.44555 24.204 4.17751 24.3832 3.95176 24.6091C3.72601 24.835 3.54697 25.1032 3.42485 25.3983C3.30273 25.6934 3.23993 26.0096 3.24004 26.329C3.24014 26.6484 3.30315 26.9646 3.42547 27.2596C3.54778 27.5546 3.72701 27.8227 3.95291 28.0484C4.17881 28.2742 4.44696 28.4532 4.74206 28.5753C5.03715 28.6974 5.35341 28.7602 5.67278 28.7601C6.31776 28.7599 6.93625 28.5035 7.39217 28.0473C7.8481 27.591 8.10411 26.9724 8.1039 26.3274C8.10368 25.6824 7.84725 25.0639 7.39103 24.608C6.9348 24.1521 6.31614 23.8961 5.67116 23.8963ZM26.7408 24.7066H23.4994V27.9481H26.7408V24.7066ZM25.1201 4.58671L22.8284 6.87844L25.1201 9.17018L27.4118 6.87844L25.1201 4.58671ZM7.2919 5.2577H4.05041V8.49919H7.2919V5.2577Z" fill="#191C21" stroke="white"/>
</g>
<defs>
<clipPath id="clip0_2381_1348">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2381_1348)">
<path
d="M5.67116 32C4.61918 31.9997 3.58801 31.7069 2.69286 31.1544C1.79771 30.6018 1.07385 29.8111 0.60217 28.8708C0.130493 27.9305 -0.0704207 26.8776 0.0218796 25.8297C0.11418 24.7818 0.496058 23.7802 1.12484 22.9368C1.75362 22.0934 2.60454 21.4415 3.5825 21.0539C4.56046 20.6663 5.62695 20.5582 6.66277 20.7419C7.69859 20.9255 8.66295 21.3935 9.44809 22.0937C10.2332 22.7938 10.8082 23.6985 11.1088 24.7066H20.2579V21.4652H23.4994V12.1329L19.864 8.49919H10.5334V11.7407H0.80892V2.01621H10.5334V5.2577H19.864L25.1201 0L31.9969 6.87844L26.7408 12.1313V21.4652H29.9823V31.1896H20.2579V27.9481H11.1088C10.7594 29.1189 10.0415 30.1457 9.06182 30.8757C8.08212 31.6057 6.89294 32.0001 5.67116 32ZM5.67116 23.8963C5.35179 23.8964 5.03557 23.9594 4.74056 24.0817C4.44555 24.204 4.17751 24.3832 3.95176 24.6091C3.72601 24.835 3.54697 25.1032 3.42485 25.3983C3.30273 25.6934 3.23993 26.0096 3.24004 26.329C3.24014 26.6484 3.30315 26.9646 3.42547 27.2596C3.54778 27.5546 3.72701 27.8227 3.95291 28.0484C4.17881 28.2742 4.44696 28.4532 4.74206 28.5753C5.03715 28.6974 5.35341 28.7602 5.67278 28.7601C6.31776 28.7599 6.93625 28.5035 7.39217 28.0473C7.8481 27.591 8.10411 26.9724 8.1039 26.3274C8.10368 25.6824 7.84725 25.0639 7.39103 24.608C6.9348 24.1521 6.31614 23.8961 5.67116 23.8963ZM26.7408 24.7066H23.4994V27.9481H26.7408V24.7066ZM25.1201 4.58671L22.8284 6.87844L25.1201 9.17018L27.4118 6.87844L25.1201 4.58671ZM7.2919 5.2577H4.05041V8.49919H7.2919V5.2577Z"
fill="#191C21"
stroke="white"
/>
</g>
<defs>
<clipPath id="clip0_2381_1348">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
</template>

View File

@@ -3,7 +3,15 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#86909C" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 3.58929C1.5 3.03517 1.72012 2.50375 2.11194 2.11194C2.50375 1.72012 3.03517 1.5 3.58929 1.5H7.76786C8.32197 1.5 8.85339 1.72012 9.24521 2.11194C9.63702 2.50375 9.85714 3.03517 9.85714 3.58929V7.76786C9.85714 8.32197 9.63702 8.85339 9.24521 9.24521C8.85339 9.63702 8.32197 9.85714 7.76786 9.85714H3.58929C3.03517 9.85714 2.50375 9.63702 2.11194 9.24521C1.72012 8.85339 1.5 8.32197 1.5 7.76786V3.58929ZM3.58929 2.89286C3.40458 2.89286 3.22744 2.96623 3.09684 3.09684C2.96623 3.22744 2.89286 3.40458 2.89286 3.58929V7.76786C2.89286 7.95256 2.96623 8.1297 3.09684 8.26031C3.22744 8.39091 3.40458 8.46429 3.58929 8.46429H7.76786C7.95256 8.46429 8.1297 8.39091 8.26031 8.26031C8.39091 8.1297 8.46429 7.95256 8.46429 7.76786V3.58929C8.46429 3.40458 8.39091 3.22744 8.26031 3.09684C8.1297 2.96623 7.95256 2.89286 7.76786 2.89286H3.58929ZM12.6429 3.58929C12.6429 3.03517 12.863 2.50375 13.2548 2.11194C13.6466 1.72012 14.178 1.5 14.7321 1.5H18.9107C19.4648 1.5 19.9962 1.72012 20.3881 2.11194C20.7799 2.50375 21 3.03517 21 3.58929V7.76786C21 8.32197 20.7799 8.85339 20.3881 9.24521C19.9962 9.63702 19.4648 9.85714 18.9107 9.85714H14.7321C14.178 9.85714 13.6466 9.63702 13.2548 9.24521C12.863 8.85339 12.6429 8.32197 12.6429 7.76786V3.58929ZM14.7321 2.89286C14.5474 2.89286 14.3703 2.96623 14.2397 3.09684C14.1091 3.22744 14.0357 3.40458 14.0357 3.58929V7.76786C14.0357 7.95256 14.1091 8.1297 14.2397 8.26031C14.3703 8.39091 14.5474 8.46429 14.7321 8.46429H18.9107C19.0954 8.46429 19.2726 8.39091 19.4032 8.26031C19.5338 8.1297 19.6071 7.95256 19.6071 7.76786V3.58929C19.6071 3.40458 19.5338 3.22744 19.4032 3.09684C19.2726 2.96623 19.0954 2.89286 18.9107 2.89286H14.7321ZM1.5 14.7321C1.5 14.178 1.72012 13.6466 2.11194 13.2548C2.50375 12.863 3.03517 12.6429 3.58929 12.6429H7.76786C8.32197 12.6429 8.85339 12.863 9.24521 13.2548C9.63702 13.6466 9.85714 14.178 9.85714 14.7321V18.9107C9.85714 19.4648 9.63702 19.9962 9.24521 20.3881C8.85339 20.7799 8.32197 21 7.76786 21H3.58929C3.03517 21 2.50375 20.7799 2.11194 20.3881C1.72012 19.9962 1.5 19.4648 1.5 18.9107V14.7321ZM3.58929 14.0357C3.40458 14.0357 3.22744 14.1091 3.09684 14.2397C2.96623 14.3703 2.89286 14.5474 2.89286 14.7321V18.9107C2.89286 19.0954 2.96623 19.2726 3.09684 19.4032C3.22744 19.5338 3.40458 19.6071 3.58929 19.6071H7.76786C7.95256 19.6071 8.1297 19.5338 8.26031 19.4032C8.39091 19.2726 8.46429 19.0954 8.46429 18.9107V14.7321C8.46429 14.5474 8.39091 14.3703 8.26031 14.2397C8.1297 14.1091 7.95256 14.0357 7.76786 14.0357H3.58929ZM12.6429 14.7321C12.6429 14.178 12.863 13.6466 13.2548 13.2548C13.6466 12.863 14.178 12.6429 14.7321 12.6429H18.9107C19.4648 12.6429 19.9962 12.863 20.3881 13.2548C20.7799 13.6466 21 14.178 21 14.7321V18.9107C21 19.4648 20.7799 19.9962 20.3881 20.3881C19.9962 20.7799 19.4648 21 18.9107 21H14.7321C14.178 21 13.6466 20.7799 13.2548 20.3881C12.863 19.9962 12.6429 19.4648 12.6429 18.9107V14.7321ZM14.7321 14.0357C14.5474 14.0357 14.3703 14.1091 14.2397 14.2397C14.1091 14.3703 14.0357 14.5474 14.0357 14.7321V18.9107C14.0357 19.0954 14.1091 19.2726 14.2397 19.4032C14.3703 19.5338 14.5474 19.6071 14.7321 19.6071H18.9107C19.0954 19.6071 19.2726 19.5338 19.4032 19.4032C19.5338 19.2726 19.6071 19.0954 19.6071 18.9107V14.7321C19.6071 14.5474 19.5338 14.3703 19.4032 14.2397C19.2726 14.1091 19.0954 14.0357 18.9107 14.0357H14.7321Z" />
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="#86909C"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.5 3.58929C1.5 3.03517 1.72012 2.50375 2.11194 2.11194C2.50375 1.72012 3.03517 1.5 3.58929 1.5H7.76786C8.32197 1.5 8.85339 1.72012 9.24521 2.11194C9.63702 2.50375 9.85714 3.03517 9.85714 3.58929V7.76786C9.85714 8.32197 9.63702 8.85339 9.24521 9.24521C8.85339 9.63702 8.32197 9.85714 7.76786 9.85714H3.58929C3.03517 9.85714 2.50375 9.63702 2.11194 9.24521C1.72012 8.85339 1.5 8.32197 1.5 7.76786V3.58929ZM3.58929 2.89286C3.40458 2.89286 3.22744 2.96623 3.09684 3.09684C2.96623 3.22744 2.89286 3.40458 2.89286 3.58929V7.76786C2.89286 7.95256 2.96623 8.1297 3.09684 8.26031C3.22744 8.39091 3.40458 8.46429 3.58929 8.46429H7.76786C7.95256 8.46429 8.1297 8.39091 8.26031 8.26031C8.39091 8.1297 8.46429 7.95256 8.46429 7.76786V3.58929C8.46429 3.40458 8.39091 3.22744 8.26031 3.09684C8.1297 2.96623 7.95256 2.89286 7.76786 2.89286H3.58929ZM12.6429 3.58929C12.6429 3.03517 12.863 2.50375 13.2548 2.11194C13.6466 1.72012 14.178 1.5 14.7321 1.5H18.9107C19.4648 1.5 19.9962 1.72012 20.3881 2.11194C20.7799 2.50375 21 3.03517 21 3.58929V7.76786C21 8.32197 20.7799 8.85339 20.3881 9.24521C19.9962 9.63702 19.4648 9.85714 18.9107 9.85714H14.7321C14.178 9.85714 13.6466 9.63702 13.2548 9.24521C12.863 8.85339 12.6429 8.32197 12.6429 7.76786V3.58929ZM14.7321 2.89286C14.5474 2.89286 14.3703 2.96623 14.2397 3.09684C14.1091 3.22744 14.0357 3.40458 14.0357 3.58929V7.76786C14.0357 7.95256 14.1091 8.1297 14.2397 8.26031C14.3703 8.39091 14.5474 8.46429 14.7321 8.46429H18.9107C19.0954 8.46429 19.2726 8.39091 19.4032 8.26031C19.5338 8.1297 19.6071 7.95256 19.6071 7.76786V3.58929C19.6071 3.40458 19.5338 3.22744 19.4032 3.09684C19.2726 2.96623 19.0954 2.89286 18.9107 2.89286H14.7321ZM1.5 14.7321C1.5 14.178 1.72012 13.6466 2.11194 13.2548C2.50375 12.863 3.03517 12.6429 3.58929 12.6429H7.76786C8.32197 12.6429 8.85339 12.863 9.24521 13.2548C9.63702 13.6466 9.85714 14.178 9.85714 14.7321V18.9107C9.85714 19.4648 9.63702 19.9962 9.24521 20.3881C8.85339 20.7799 8.32197 21 7.76786 21H3.58929C3.03517 21 2.50375 20.7799 2.11194 20.3881C1.72012 19.9962 1.5 19.4648 1.5 18.9107V14.7321ZM3.58929 14.0357C3.40458 14.0357 3.22744 14.1091 3.09684 14.2397C2.96623 14.3703 2.89286 14.5474 2.89286 14.7321V18.9107C2.89286 19.0954 2.96623 19.2726 3.09684 19.4032C3.22744 19.5338 3.40458 19.6071 3.58929 19.6071H7.76786C7.95256 19.6071 8.1297 19.5338 8.26031 19.4032C8.39091 19.2726 8.46429 19.0954 8.46429 18.9107V14.7321C8.46429 14.5474 8.39091 14.3703 8.26031 14.2397C8.1297 14.1091 7.95256 14.0357 7.76786 14.0357H3.58929ZM12.6429 14.7321C12.6429 14.178 12.863 13.6466 13.2548 13.2548C13.6466 12.863 14.178 12.6429 14.7321 12.6429H18.9107C19.4648 12.6429 19.9962 12.863 20.3881 13.2548C20.7799 13.6466 21 14.178 21 14.7321V18.9107C21 19.4648 20.7799 19.9962 20.3881 20.3881C19.9962 20.7799 19.4648 21 18.9107 21H14.7321C14.178 21 13.6466 20.7799 13.2548 20.3881C12.863 19.9962 12.6429 19.4648 12.6429 18.9107V14.7321ZM14.7321 14.0357C14.5474 14.0357 14.3703 14.1091 14.2397 14.2397C14.1091 14.3703 14.0357 14.5474 14.0357 14.7321V18.9107C14.0357 19.0954 14.1091 19.2726 14.2397 19.4032C14.3703 19.5338 14.5474 19.6071 14.7321 19.6071H18.9107C19.0954 19.6071 19.2726 19.5338 19.4032 19.4032C19.5338 19.2726 19.6071 19.0954 19.6071 18.9107V14.7321C19.6071 14.5474 19.5338 14.3703 19.4032 14.2397C19.2726 14.1091 19.0954 14.0357 18.9107 14.0357H14.7321Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,17 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#86909C" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 18C3.75 17.8011 3.82902 17.6103 3.96967 17.4697C4.11032 17.329 4.30109 17.25 4.5 17.25H19.5C19.6989 17.25 19.8897 17.329 20.0303 17.4697C20.171 17.6103 20.25 17.8011 20.25 18C20.25 18.1989 20.171 18.3897 20.0303 18.5303C19.8897 18.671 19.6989 18.75 19.5 18.75H4.5C4.30109 18.75 4.11032 18.671 3.96967 18.5303C3.82902 18.3897 3.75 18.1989 3.75 18ZM3.75 12C3.75 11.8011 3.82902 11.6103 3.96967 11.4697C4.11032 11.329 4.30109 11.25 4.5 11.25H19.5C19.6989 11.25 19.8897 11.329 20.0303 11.4697C20.171 11.6103 20.25 11.8011 20.25 12C20.25 12.1989 20.171 12.3897 20.0303 12.5303C19.8897 12.671 19.6989 12.75 19.5 12.75H4.5C4.30109 12.75 4.11032 12.671 3.96967 12.5303C3.82902 12.3897 3.75 12.1989 3.75 12ZM3.75 6C3.75 5.80109 3.82902 5.61032 3.96967 5.46967C4.11032 5.32902 4.30109 5.25 4.5 5.25H19.5C19.6989 5.25 19.8897 5.32902 20.0303 5.46967C20.171 5.61032 20.25 5.80109 20.25 6C20.25 6.19891 20.171 6.38968 20.0303 6.53033C19.8897 6.67098 19.6989 6.75 19.5 6.75H4.5C4.30109 6.75 4.11032 6.67098 3.96967 6.53033C3.82902 6.38968 3.75 6.19891 3.75 6Z" />
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="#86909C"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.75 18C3.75 17.8011 3.82902 17.6103 3.96967 17.4697C4.11032 17.329 4.30109 17.25 4.5 17.25H19.5C19.6989 17.25 19.8897 17.329 20.0303 17.4697C20.171 17.6103 20.25 17.8011 20.25 18C20.25 18.1989 20.171 18.3897 20.0303 18.5303C19.8897 18.671 19.6989 18.75 19.5 18.75H4.5C4.30109 18.75 4.11032 18.671 3.96967 18.5303C3.82902 18.3897 3.75 18.1989 3.75 18ZM3.75 12C3.75 11.8011 3.82902 11.6103 3.96967 11.4697C4.11032 11.329 4.30109 11.25 4.5 11.25H19.5C19.6989 11.25 19.8897 11.329 20.0303 11.4697C20.171 11.6103 20.25 11.8011 20.25 12C20.25 12.1989 20.171 12.3897 20.0303 12.5303C19.8897 12.671 19.6989 12.75 19.5 12.75H4.5C4.30109 12.75 4.11032 12.671 3.96967 12.5303C3.82902 12.3897 3.75 12.1989 3.75 12ZM3.75 6C3.75 5.80109 3.82902 5.61032 3.96967 5.46967C4.11032 5.32902 4.30109 5.25 4.5 5.25H19.5C19.6989 5.25 19.8897 5.32902 20.0303 5.46967C20.171 5.61032 20.25 5.80109 20.25 6C20.25 6.19891 20.171 6.38968 20.0303 6.53033C19.8897 6.67098 19.6989 6.75 19.5 6.75H4.5C4.30109 6.75 4.11032 6.67098 3.96967 6.53033C3.82902 6.38968 3.75 6.19891 3.75 6Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 11.75C11.3713 11.7524 10.7643 11.9805 10.2895 12.3926C9.81465 12.8048 9.50353 13.3736 9.41271 13.9958C9.32189 14.6179 9.45738 15.252 9.79456 15.7827C10.1317 16.3134 10.6482 16.7054 11.25 16.8875V18.5C11.25 18.6989 11.329 18.8897 11.4697 19.0303C11.6103 19.171 11.8011 19.25 12 19.25C12.1989 19.25 12.3897 19.171 12.5303 19.0303C12.671 18.8897 12.75 18.6989 12.75 18.5V16.8875C13.3518 16.7054 13.8683 16.3134 14.2054 15.7827C14.5426 15.252 14.6781 14.6179 14.5873 13.9958C14.4965 13.3736 14.1854 12.8048 13.7105 12.3926C13.2357 11.9805 12.6287 11.7524 12 11.75ZM12 15.5C11.7775 15.5 11.56 15.434 11.375 15.3104C11.19 15.1868 11.0458 15.0111 10.9606 14.8055C10.8755 14.6 10.8532 14.3738 10.8966 14.1555C10.94 13.9373 11.0472 13.7368 11.2045 13.5795C11.3618 13.4222 11.5623 13.315 11.7805 13.2716C11.9988 13.2282 12.225 13.2505 12.4305 13.3356C12.6361 13.4208 12.8118 13.565 12.9354 13.75C13.059 13.935 13.125 14.1525 13.125 14.375C13.125 14.6734 13.0065 14.9595 12.7955 15.1705C12.5845 15.3815 12.2984 15.5 12 15.5ZM19.5 8.75H16.125V6.125C16.125 5.03098 15.6904 3.98177 14.9168 3.20818C14.1432 2.4346 13.094 2 12 2C10.906 2 9.85677 2.4346 9.08318 3.20818C8.3096 3.98177 7.875 5.03098 7.875 6.125V8.75H4.5C4.10218 8.75 3.72064 8.90804 3.43934 9.18934C3.15804 9.47064 3 9.85218 3 10.25V20.75C3 21.1478 3.15804 21.5294 3.43934 21.8107C3.72064 22.092 4.10218 22.25 4.5 22.25H19.5C19.8978 22.25 20.2794 22.092 20.5607 21.8107C20.842 21.5294 21 21.1478 21 20.75V10.25C21 9.85218 20.842 9.47064 20.5607 9.18934C20.2794 8.90804 19.8978 8.75 19.5 8.75ZM9.375 6.125C9.375 5.42881 9.65156 4.76113 10.1438 4.26884C10.6361 3.77656 11.3038 3.5 12 3.5C12.6962 3.5 13.3639 3.77656 13.8562 4.26884C14.3484 4.76113 14.625 5.42881 14.625 6.125V8.75H9.375V6.125ZM19.5 20.75H4.5V10.25H19.5V20.75Z" fill="#1A1A1A"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 11.75C11.3713 11.7524 10.7643 11.9805 10.2895 12.3926C9.81465 12.8048 9.50353 13.3736 9.41271 13.9958C9.32189 14.6179 9.45738 15.252 9.79456 15.7827C10.1317 16.3134 10.6482 16.7054 11.25 16.8875V18.5C11.25 18.6989 11.329 18.8897 11.4697 19.0303C11.6103 19.171 11.8011 19.25 12 19.25C12.1989 19.25 12.3897 19.171 12.5303 19.0303C12.671 18.8897 12.75 18.6989 12.75 18.5V16.8875C13.3518 16.7054 13.8683 16.3134 14.2054 15.7827C14.5426 15.252 14.6781 14.6179 14.5873 13.9958C14.4965 13.3736 14.1854 12.8048 13.7105 12.3926C13.2357 11.9805 12.6287 11.7524 12 11.75ZM12 15.5C11.7775 15.5 11.56 15.434 11.375 15.3104C11.19 15.1868 11.0458 15.0111 10.9606 14.8055C10.8755 14.6 10.8532 14.3738 10.8966 14.1555C10.94 13.9373 11.0472 13.7368 11.2045 13.5795C11.3618 13.4222 11.5623 13.315 11.7805 13.2716C11.9988 13.2282 12.225 13.2505 12.4305 13.3356C12.6361 13.4208 12.8118 13.565 12.9354 13.75C13.059 13.935 13.125 14.1525 13.125 14.375C13.125 14.6734 13.0065 14.9595 12.7955 15.1705C12.5845 15.3815 12.2984 15.5 12 15.5ZM19.5 8.75H16.125V6.125C16.125 5.03098 15.6904 3.98177 14.9168 3.20818C14.1432 2.4346 13.094 2 12 2C10.906 2 9.85677 2.4346 9.08318 3.20818C8.3096 3.98177 7.875 5.03098 7.875 6.125V8.75H4.5C4.10218 8.75 3.72064 8.90804 3.43934 9.18934C3.15804 9.47064 3 9.85218 3 10.25V20.75C3 21.1478 3.15804 21.5294 3.43934 21.8107C3.72064 22.092 4.10218 22.25 4.5 22.25H19.5C19.8978 22.25 20.2794 22.092 20.5607 21.8107C20.842 21.5294 21 21.1478 21 20.75V10.25C21 9.85218 20.842 9.47064 20.5607 9.18934C20.2794 8.90804 19.8978 8.75 19.5 8.75ZM9.375 6.125C9.375 5.42881 9.65156 4.76113 10.1438 4.26884C10.6361 3.77656 11.3038 3.5 12 3.5C12.6962 3.5 13.3639 3.77656 13.8562 4.26884C14.3484 4.76113 14.625 5.42881 14.625 6.125V8.75H9.375V6.125ZM19.5 20.75H4.5V10.25H19.5V20.75Z"
fill="#1A1A1A"
/>
</svg>
</template>

View File

@@ -3,8 +3,17 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="40" height="40" viewBox="0 0 40 40" fill="black" xmlns="http://www.w3.org/2000/svg" id="iconMember">
<circle cx="20" cy="20" r="20" fill="white"/>
<path d="M40 20.0018C40.0001 16.6744 39.1701 13.3995 37.5852 10.4739C36.0004 7.5482 33.7107 5.06419 30.9236 3.24683C28.1365 1.42948 24.9401 0.336201 21.6239 0.0660405C18.3076 -0.20412 14.9764 0.357371 11.9319 1.69965C8.88748 3.04193 6.22597 5.12259 4.1885 7.75315C2.15104 10.3837 0.802001 13.481 0.263595 16.7646C-0.274811 20.0481 0.0144265 23.4141 1.10511 26.5577C2.19579 29.7012 4.05344 32.523 6.50981 34.7673L6.7451 34.9634C10.3981 38.208 15.1142 40 20 40C24.8858 40 29.6019 38.208 33.2549 34.9634L33.4902 34.7673C35.5431 32.8948 37.1826 30.6143 38.3035 28.0717C39.4245 25.5291 40.0023 22.7805 40 20.0018ZM2.35294 20.0018C2.349 17.1207 3.05046 14.2824 4.39604 11.7349C5.74163 9.18731 7.69042 7.00795 10.0722 5.38712C12.454 3.76629 15.1964 2.75327 18.0599 2.43653C20.9234 2.11979 23.8209 2.50896 26.4993 3.57007C29.1778 4.63117 31.5557 6.33193 33.4255 8.52381C35.2952 10.7157 36.6 13.332 37.2257 16.1444C37.8515 18.9567 37.7792 21.8795 37.0153 24.6574C36.2513 27.4354 34.8189 29.9841 32.8431 32.0809C30.9901 29.2141 28.2281 27.0527 25 25.9433C26.6114 24.8699 27.8347 23.3065 28.489 21.4841C29.1433 19.6617 29.1938 17.6772 28.6331 15.8239C28.0725 13.9706 26.9303 12.3469 25.3756 11.1929C23.8209 10.0389 21.9362 9.41584 20 9.41584C18.0638 9.41584 16.1791 10.0389 14.6244 11.1929C13.0697 12.3469 11.9275 13.9706 11.3669 15.8239C10.8062 17.6772 10.8567 19.6617 11.511 21.4841C12.1653 23.3065 13.3886 24.8699 15 25.9433C11.7719 27.0527 9.00992 29.2141 7.15686 32.0809C4.06852 28.8179 2.34915 24.4947 2.35294 20.0018ZM13.3333 18.433C13.3333 17.1144 13.7243 15.8254 14.4569 14.729C15.1894 13.6326 16.2306 12.7781 17.4488 12.2735C18.667 11.7689 20.0074 11.6369 21.3006 11.8941C22.5938 12.1514 23.7817 12.7863 24.714 13.7187C25.6464 14.6511 26.2813 15.8391 26.5386 17.1324C26.7958 18.4256 26.6638 19.7662 26.1592 20.9844C25.6546 22.2027 24.8001 23.2439 23.7038 23.9765C22.6075 24.7091 21.3185 25.1001 20 25.1001C18.2335 25.0949 16.5408 24.3908 15.2917 23.1416C14.0425 21.8925 13.3385 20.1997 13.3333 18.433ZM8.90196 33.728C10.0553 31.8159 11.6831 30.2342 13.6275 29.1362C15.5719 28.0383 17.767 27.4613 20 27.4613C22.233 27.4613 24.428 28.0383 26.3725 29.1362C28.3169 30.2342 29.9447 31.8159 31.098 33.728C27.9556 36.2653 24.0388 37.6492 20 37.6492C15.9612 37.6492 12.0444 36.2653 8.90196 33.728Z"/>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="black"
xmlns="http://www.w3.org/2000/svg"
id="iconMember"
>
<circle cx="20" cy="20" r="20" fill="white" />
<path
d="M40 20.0018C40.0001 16.6744 39.1701 13.3995 37.5852 10.4739C36.0004 7.5482 33.7107 5.06419 30.9236 3.24683C28.1365 1.42948 24.9401 0.336201 21.6239 0.0660405C18.3076 -0.20412 14.9764 0.357371 11.9319 1.69965C8.88748 3.04193 6.22597 5.12259 4.1885 7.75315C2.15104 10.3837 0.802001 13.481 0.263595 16.7646C-0.274811 20.0481 0.0144265 23.4141 1.10511 26.5577C2.19579 29.7012 4.05344 32.523 6.50981 34.7673L6.7451 34.9634C10.3981 38.208 15.1142 40 20 40C24.8858 40 29.6019 38.208 33.2549 34.9634L33.4902 34.7673C35.5431 32.8948 37.1826 30.6143 38.3035 28.0717C39.4245 25.5291 40.0023 22.7805 40 20.0018ZM2.35294 20.0018C2.349 17.1207 3.05046 14.2824 4.39604 11.7349C5.74163 9.18731 7.69042 7.00795 10.0722 5.38712C12.454 3.76629 15.1964 2.75327 18.0599 2.43653C20.9234 2.11979 23.8209 2.50896 26.4993 3.57007C29.1778 4.63117 31.5557 6.33193 33.4255 8.52381C35.2952 10.7157 36.6 13.332 37.2257 16.1444C37.8515 18.9567 37.7792 21.8795 37.0153 24.6574C36.2513 27.4354 34.8189 29.9841 32.8431 32.0809C30.9901 29.2141 28.2281 27.0527 25 25.9433C26.6114 24.8699 27.8347 23.3065 28.489 21.4841C29.1433 19.6617 29.1938 17.6772 28.6331 15.8239C28.0725 13.9706 26.9303 12.3469 25.3756 11.1929C23.8209 10.0389 21.9362 9.41584 20 9.41584C18.0638 9.41584 16.1791 10.0389 14.6244 11.1929C13.0697 12.3469 11.9275 13.9706 11.3669 15.8239C10.8062 17.6772 10.8567 19.6617 11.511 21.4841C12.1653 23.3065 13.3886 24.8699 15 25.9433C11.7719 27.0527 9.00992 29.2141 7.15686 32.0809C4.06852 28.8179 2.34915 24.4947 2.35294 20.0018ZM13.3333 18.433C13.3333 17.1144 13.7243 15.8254 14.4569 14.729C15.1894 13.6326 16.2306 12.7781 17.4488 12.2735C18.667 11.7689 20.0074 11.6369 21.3006 11.8941C22.5938 12.1514 23.7817 12.7863 24.714 13.7187C25.6464 14.6511 26.2813 15.8391 26.5386 17.1324C26.7958 18.4256 26.6638 19.7662 26.1592 20.9844C25.6546 22.2027 24.8001 23.2439 23.7038 23.9765C22.6075 24.7091 21.3185 25.1001 20 25.1001C18.2335 25.0949 16.5408 24.3908 15.2917 23.1416C14.0425 21.8925 13.3385 20.1997 13.3333 18.433ZM8.90196 33.728C10.0553 31.8159 11.6831 30.2342 13.6275 29.1362C15.5719 28.0383 17.767 27.4613 20 27.4613C22.233 27.4613 24.428 28.0383 26.3725 29.1362C28.3169 30.2342 29.9447 31.8159 31.098 33.728C27.9556 36.2653 24.0388 37.6492 20 37.6492C15.9612 37.6492 12.0444 36.2653 8.90196 33.728Z"
/>
</svg>
</template>

View File

@@ -3,24 +3,72 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="16" height="16" viewBox="3 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_4086_5522)">
<path d="M10.0359 5.48491V10.4086H9.0885L7.18616 7.30519L6.70871 6.43235H6.70125L6.73109 7.23058V10.4086H6V5.48491H6.93998L8.83486 8.58087L9.31977 9.46863H9.32723L9.29739 8.66293V5.48491H10.0359Z" fill="white"/>
<path d="M13.7426 5L11.5195 10.9308H10.7735L12.9966 5H13.7426Z" fill="white"/>
<path d="M18 10.4086H17.157L16.687 9.118H14.6206L14.1506 10.4086H13.3299L15.1875 5.48491H16.1424L18 10.4086ZM14.8444 8.46897H16.4632L15.6575 6.17124L14.8444 8.46897Z" fill="white"/>
<rect x="4" y="0.2" width="16" height="16" rx="8" stroke="white" shape-rendering="geometricPrecision"/>
</g>
<defs>
<filter id="filter0_d_4086_5522" x="0" y="0" width="24" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4086_5522"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4086_5522" result="shape"/>
</filter>
</defs>
</svg>
<svg
width="16"
height="16"
viewBox="3 0 20 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter0_d_4086_5522)">
<path
d="M10.0359 5.48491V10.4086H9.0885L7.18616 7.30519L6.70871 6.43235H6.70125L6.73109 7.23058V10.4086H6V5.48491H6.93998L8.83486 8.58087L9.31977 9.46863H9.32723L9.29739 8.66293V5.48491H10.0359Z"
fill="white"
/>
<path
d="M13.7426 5L11.5195 10.9308H10.7735L12.9966 5H13.7426Z"
fill="white"
/>
<path
d="M18 10.4086H17.157L16.687 9.118H14.6206L14.1506 10.4086H13.3299L15.1875 5.48491H16.1424L18 10.4086ZM14.8444 8.46897H16.4632L15.6575 6.17124L14.8444 8.46897Z"
fill="white"
/>
<rect
x="4"
y="0.2"
width="16"
height="16"
rx="8"
stroke="white"
shape-rendering="geometricPrecision"
/>
</g>
<defs>
<filter
id="filter0_d_4086_5522"
x="0"
y="0"
width="24"
height="24"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_4086_5522"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4086_5522"
result="shape"
/>
</filter>
</defs>
</svg>
</template>

View File

@@ -3,8 +3,17 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 16H22V18H10V16ZM10 10H22V12H10V10Z" fill="#191C21"/>
<path d="M16 30L9.82401 26.707C8.06334 25.7703 6.59097 24.3719 5.56493 22.6617C4.53888 20.9516 3.99789 18.9943 4.00001 17V4C4.00054 3.46973 4.21142 2.96133 4.58638 2.58637C4.96134 2.21141 5.46974 2.00053 6.00001 2H26C26.5303 2.00053 27.0387 2.21141 27.4136 2.58637C27.7886 2.96133 27.9995 3.46973 28 4V17C28.0021 18.9943 27.4611 20.9516 26.4351 22.6617C25.409 24.3719 23.9367 25.7703 22.176 26.707L16 30ZM6.00001 4V17C5.99835 18.6318 6.44111 20.2333 7.28077 21.6325C8.12043 23.0317 9.32528 24.1758 10.766 24.942L16 27.733L21.234 24.943C22.6749 24.1767 23.8798 23.0324 24.7195 21.633C25.5592 20.2336 26.0018 18.632 26 17V4H6.00001Z" fill="#191C21"/>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 16H22V18H10V16ZM10 10H22V12H10V10Z" fill="#191C21" />
<path
d="M16 30L9.82401 26.707C8.06334 25.7703 6.59097 24.3719 5.56493 22.6617C4.53888 20.9516 3.99789 18.9943 4.00001 17V4C4.00054 3.46973 4.21142 2.96133 4.58638 2.58637C4.96134 2.21141 5.46974 2.00053 6.00001 2H26C26.5303 2.00053 27.0387 2.21141 27.4136 2.58637C27.7886 2.96133 27.9995 3.46973 28 4V17C28.0021 18.9943 27.4611 20.9516 26.4351 22.6617C25.409 24.3719 23.9367 25.7703 22.176 26.707L16 30ZM6.00001 4V17C5.99835 18.6318 6.44111 20.2333 7.28077 21.6325C8.12043 23.0317 9.32528 24.1758 10.766 24.942L16 27.733L21.234 24.943C22.6749 24.1767 23.8798 23.0324 24.7195 21.633C25.5592 20.2336 26.0018 18.632 26 17V4H6.00001Z"
fill="#191C21"
/>
</svg>
</template>

View File

@@ -3,8 +3,26 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 19C15.1944 19 19 15.1944 19 10.5C19 5.80558 15.1944 2 10.5 2C5.80558 2 2 5.80558 2 10.5C2 15.1944 5.80558 19 10.5 19Z" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 21L17 17" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 19C15.1944 19 19 15.1944 19 10.5C19 5.80558 15.1944 2 10.5 2C5.80558 2 2 5.80558 2 10.5C2 15.1944 5.80558 19 10.5 19Z"
stroke="#4E5969"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 21L17 17"
stroke="#4E5969"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.5 6H19.425C19.05 4.275 17.55 3 15.75 3C13.95 3 12.45 4.275 12.075 6H1.5V7.5H12.075C12.45 9.225 13.95 10.5 15.75 10.5C17.55 10.5 19.05 9.225 19.425 7.5H22.5V6ZM15.75 9C14.475 9 13.5 8.025 13.5 6.75C13.5 5.475 14.475 4.5 15.75 4.5C17.025 4.5 18 5.475 18 6.75C18 8.025 17.025 9 15.75 9ZM1.5 18H4.575C4.95 19.725 6.45 21 8.25 21C10.05 21 11.55 19.725 11.925 18H22.5V16.5H11.925C11.55 14.775 10.05 13.5 8.25 13.5C6.45 13.5 4.95 14.775 4.575 16.5H1.5V18ZM8.25 15C9.525 15 10.5 15.975 10.5 17.25C10.5 18.525 9.525 19.5 8.25 19.5C6.975 19.5 6 18.525 6 17.25C6 15.975 6.975 15 8.25 15Z" fill="#64748B"/>
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.5 6H19.425C19.05 4.275 17.55 3 15.75 3C13.95 3 12.45 4.275 12.075 6H1.5V7.5H12.075C12.45 9.225 13.95 10.5 15.75 10.5C17.55 10.5 19.05 9.225 19.425 7.5H22.5V6ZM15.75 9C14.475 9 13.5 8.025 13.5 6.75C13.5 5.475 14.475 4.5 15.75 4.5C17.025 4.5 18 5.475 18 6.75C18 8.025 17.025 9 15.75 9ZM1.5 18H4.575C4.95 19.725 6.45 21 8.25 21C10.05 21 11.55 19.725 11.925 18H22.5V16.5H11.925C11.55 14.775 10.05 13.5 8.25 13.5C6.45 13.5 4.95 14.775 4.575 16.5H1.5V18ZM8.25 15C9.525 15 10.5 15.975 10.5 17.25C10.5 18.525 9.525 19.5 8.25 19.5C6.975 19.5 6 18.525 6 17.25C6 15.975 6.975 15 8.25 15Z"
fill="#64748B"
/>
</svg>
</template>

View File

@@ -3,14 +3,23 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
>
<g clip-path="url(#clip0_2395_2657)">
<circle cx="32" cy="32" r="30" fill="#0099FF"/>
<path d="M33.7678 22.2322C32.7915 21.2559 31.2085 21.2559 30.2322 22.2322L14.3223 38.1421C13.346 39.1185 13.346 40.7014 14.3223 41.6777C15.2986 42.654 16.8816 42.654 17.8579 41.6777L32 27.5355L46.1421 41.6777C47.1185 42.654 48.7014 42.654 49.6777 41.6777C50.654 40.7014 50.654 39.1184 49.6777 38.1421L33.7678 22.2322ZM29.5 64C29.5 65.3807 30.6193 66.5 32 66.5C33.3807 66.5 34.5 65.3807 34.5 64H29.5ZM29.5 24L29.5 64H34.5L34.5 24L29.5 24Z" fill="white"/>
<circle cx="32" cy="32" r="30" fill="#0099FF" />
<path
d="M33.7678 22.2322C32.7915 21.2559 31.2085 21.2559 30.2322 22.2322L14.3223 38.1421C13.346 39.1185 13.346 40.7014 14.3223 41.6777C15.2986 42.654 16.8816 42.654 17.8579 41.6777L32 27.5355L46.1421 41.6777C47.1185 42.654 48.7014 42.654 49.6777 41.6777C50.654 40.7014 50.654 39.1184 49.6777 38.1421L33.7678 22.2322ZM29.5 64C29.5 65.3807 30.6193 66.5 32 66.5C33.3807 66.5 34.5 65.3807 34.5 64H29.5ZM29.5 24L29.5 64H34.5L34.5 24L29.5 24Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_2395_2657">
<rect width="64" height="64" fill="white"/>
<rect width="64" height="64" fill="white" />
</clipPath>
</defs>
</svg>

View File

@@ -3,7 +3,15 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="16" height="16" viewBox="0 0 16 16" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M7.9999 16C7.71657 16 7.47924 15.904 7.2879 15.712C7.0959 15.5207 6.9999 15.2833 6.9999 15V3.82499L2.1249 8.69999C1.9249 8.89999 1.68724 8.99999 1.4119 8.99999C1.13724 8.99999 0.899902 8.89999 0.699902 8.69999C0.499902 8.49999 0.399902 8.26665 0.399902 7.99999C0.399902 7.73332 0.499902 7.49999 0.699902 7.29999L7.2999 0.699987C7.3999 0.599987 7.50824 0.528988 7.6249 0.486988C7.74157 0.445654 7.86657 0.424988 7.9999 0.424988C8.13324 0.424988 8.26257 0.445654 8.3879 0.486988C8.51257 0.528988 8.61657 0.599987 8.6999 0.699987L15.2999 7.29999C15.4999 7.49999 15.5999 7.73332 15.5999 7.99999C15.5999 8.26665 15.4999 8.49999 15.2999 8.69999C15.0999 8.89999 14.8622 8.99999 14.5869 8.99999C14.3122 8.99999 14.0749 8.89999 13.8749 8.69999L8.9999 3.82499V15C8.9999 15.2833 8.90424 15.5207 8.7129 15.712C8.5209 15.904 8.28324 16 7.9999 16Z"/>
</svg>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="black"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.9999 16C7.71657 16 7.47924 15.904 7.2879 15.712C7.0959 15.5207 6.9999 15.2833 6.9999 15V3.82499L2.1249 8.69999C1.9249 8.89999 1.68724 8.99999 1.4119 8.99999C1.13724 8.99999 0.899902 8.89999 0.699902 8.69999C0.499902 8.49999 0.399902 8.26665 0.399902 7.99999C0.399902 7.73332 0.499902 7.49999 0.699902 7.29999L7.2999 0.699987C7.3999 0.599987 7.50824 0.528988 7.6249 0.486988C7.74157 0.445654 7.86657 0.424988 7.9999 0.424988C8.13324 0.424988 8.26257 0.445654 8.3879 0.486988C8.51257 0.528988 8.61657 0.599987 8.6999 0.699987L15.2999 7.29999C15.4999 7.49999 15.5999 7.73332 15.5999 7.99999C15.5999 8.26665 15.4999 8.49999 15.2999 8.69999C15.0999 8.89999 14.8622 8.99999 14.5869 8.99999C14.3122 8.99999 14.0749 8.89999 13.8749 8.69999L8.9999 3.82499V15C8.9999 15.2833 8.90424 15.5207 8.7129 15.712C8.5209 15.904 8.28324 16 7.9999 16Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,18 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.78488 2.03878C9.62343 1.72766 9.37026 1.46508 9.05455 1.28129C8.73885 1.09751 8.37345 1 8.00047 1C7.62748 1 7.26209 1.09751 6.94638 1.28129C6.63068 1.46508 6.37751 1.72766 6.21605 2.03878L0.521086 12.4048C-0.160262 13.6423 0.70072 15.2857 2.3048 15.2857H13.6954C15.3002 15.2857 16.1598 13.643 15.4799 12.4048L9.78488 2.03878ZM8.00047 5.54628C8.18657 5.54628 8.36505 5.61468 8.49664 5.73645C8.62824 5.85822 8.70216 6.02337 8.70216 6.19557V9.44205C8.70216 9.61425 8.62824 9.77941 8.49664 9.90117C8.36505 10.0229 8.18657 10.0913 8.00047 10.0913C7.81437 10.0913 7.63589 10.0229 7.50429 9.90117C7.3727 9.77941 7.29877 9.61425 7.29877 9.44205V6.19557C7.29877 6.02337 7.3727 5.85822 7.50429 5.73645C7.63589 5.61468 7.81437 5.54628 8.00047 5.54628ZM8.00047 11.0653C8.18657 11.0653 8.36505 11.1337 8.49664 11.2555C8.62824 11.3772 8.70216 11.5424 8.70216 11.7146V12.0392C8.70216 12.2114 8.62824 12.3766 8.49664 12.4984C8.36505 12.6201 8.18657 12.6885 8.00047 12.6885C7.81437 12.6885 7.63589 12.6201 7.50429 12.4984C7.3727 12.3766 7.29877 12.2114 7.29877 12.0392V11.7146C7.29877 11.5424 7.3727 11.3772 7.50429 11.2555C7.63589 11.1337 7.81437 11.0653 8.00047 11.0653Z" fill="#FF3366"/>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.78488 2.03878C9.62343 1.72766 9.37026 1.46508 9.05455 1.28129C8.73885 1.09751 8.37345 1 8.00047 1C7.62748 1 7.26209 1.09751 6.94638 1.28129C6.63068 1.46508 6.37751 1.72766 6.21605 2.03878L0.521086 12.4048C-0.160262 13.6423 0.70072 15.2857 2.3048 15.2857H13.6954C15.3002 15.2857 16.1598 13.643 15.4799 12.4048L9.78488 2.03878ZM8.00047 5.54628C8.18657 5.54628 8.36505 5.61468 8.49664 5.73645C8.62824 5.85822 8.70216 6.02337 8.70216 6.19557V9.44205C8.70216 9.61425 8.62824 9.77941 8.49664 9.90117C8.36505 10.0229 8.18657 10.0913 8.00047 10.0913C7.81437 10.0913 7.63589 10.0229 7.50429 9.90117C7.3727 9.77941 7.29877 9.61425 7.29877 9.44205V6.19557C7.29877 6.02337 7.3727 5.85822 7.50429 5.73645C7.63589 5.61468 7.81437 5.54628 8.00047 5.54628ZM8.00047 11.0653C8.18657 11.0653 8.36505 11.1337 8.49664 11.2555C8.62824 11.3772 8.70216 11.5424 8.70216 11.7146V12.0392C8.70216 12.2114 8.62824 12.3766 8.49664 12.4984C8.36505 12.6201 8.18657 12.6885 8.00047 12.6885C7.81437 12.6885 7.63589 12.6201 7.50429 12.4984C7.3727 12.3766 7.29877 12.2114 7.29877 12.0392V11.7146C7.29877 11.5424 7.3727 11.3772 7.50429 11.2555C7.63589 11.1337 7.81437 11.0653 8.00047 11.0653Z"
fill="#FF3366"
/>
</svg>
</template>

View File

@@ -3,8 +3,26 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 9.33333C22.6274 9.33333 28 8.13943 28 6.66667C28 5.19391 22.6274 4 16 4C9.37258 4 4 5.19391 4 6.66667C4 8.13943 9.37258 9.33333 16 9.33333Z" stroke="#191C21" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 6.66666C4 9.63999 9.16133 15.5653 11.808 18.4067C12.7789 19.4358 13.3239 20.7945 13.3333 22.2093V25.3333C13.3333 26.0406 13.6143 26.7188 14.1144 27.2189C14.6145 27.719 15.2928 28 16 28C16.7072 28 17.3855 27.719 17.8856 27.2189C18.3857 26.7188 18.6667 26.0406 18.6667 25.3333V22.2093C18.6667 20.7947 19.228 19.4427 20.192 18.4067C22.84 15.5653 28 9.64132 28 6.66666" stroke="#191C21" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 9.33333C22.6274 9.33333 28 8.13943 28 6.66667C28 5.19391 22.6274 4 16 4C9.37258 4 4 5.19391 4 6.66667C4 8.13943 9.37258 9.33333 16 9.33333Z"
stroke="#191C21"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4 6.66666C4 9.63999 9.16133 15.5653 11.808 18.4067C12.7789 19.4358 13.3239 20.7945 13.3333 22.2093V25.3333C13.3333 26.0406 13.6143 26.7188 14.1144 27.2189C14.6145 27.719 15.2928 28 16 28C16.7072 28 17.3855 27.719 17.8856 27.2189C18.3857 26.7188 18.6667 26.0406 18.6667 25.3333V22.2093C18.6667 20.7947 19.228 19.4427 20.192 18.4067C22.84 15.5653 28 9.64132 28 6.66666"
stroke="#191C21"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>

View File

@@ -14,19 +14,19 @@ export const ONCE_RENDER_NUM_OF_DATA = 9;
export const PWD_VALID_LENGTH = 6;
/** @constant {string} Default grid line color (Tailwind slate-500). */
export const GRID_COLOR = '#64748b';
export const GRID_COLOR = "#64748b";
/** @constant {string} Modal type for creating a new account. */
export const MODAL_CREATE_NEW = 'MODAL_CREATE_NEW';
export const MODAL_CREATE_NEW = "MODAL_CREATE_NEW";
/** @constant {string} Modal type for editing an account. */
export const MODAL_ACCT_EDIT = 'MODAL_ACCT_EDIT';
export const MODAL_ACCT_EDIT = "MODAL_ACCT_EDIT";
/** @constant {string} Modal type for viewing account info. */
export const MODAL_ACCT_INFO = 'MODAL_ACCT_INFO';
export const MODAL_ACCT_INFO = "MODAL_ACCT_INFO";
/** @constant {string} Modal type for deleting an account. */
export const MODAL_DELETE = 'MODAL_DELETE';
export const MODAL_DELETE = "MODAL_DELETE";
/** @constant {string} LocalStorage key for saved Cytoscape node positions. */
export const SAVE_KEY_NAME = 'CYTOSCAPE_NODE_POSITION';
export const SAVE_KEY_NAME = "CYTOSCAPE_NODE_POSITION";
/** @constant {number} Duration (minutes) to highlight newly created accounts. */
export const JUST_CREATE_ACCOUNT_HOT_DURATION_MINS = 2;
@@ -36,171 +36,171 @@ export const JUST_CREATE_ACCOUNT_HOT_DURATION_MINS = 2;
* process insights (self-loops, short-loops, traces).
*/
export const INSIGHTS_FIELDS_AND_LABELS = [
['self_loops', 'Self-Loop'],
['short_loops', 'Short-Loop'],
['shortest_traces', 'Shortest Trace'],
['longest_traces', 'Longest Trace'],
['most_freq_traces', 'Most Frequent Trace'],
];
["self_loops", "Self-Loop"],
["short_loops", "Short-Loop"],
["shortest_traces", "Shortest Trace"],
["longest_traces", "Longest Trace"],
["most_freq_traces", "Most Frequent Trace"],
];
/** @constant {Object} Default Chart.js layout padding options. */
export const knownLayoutChartOption = {
padding: {
top: 16,
left: 8,
right: 8,
}
padding: {
top: 16,
left: 8,
right: 8,
},
};
/** @constant {Object} Default Chart.js scale options for line charts. */
export const knownScaleLineChartOptions = {
x: {
type: 'time',
export const knownScaleLineChartOptions = {
x: {
type: "time",
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
}
lineHeight: 2,
},
},
time: {
displayFormats: {
second: 'h:mm:ss', // ex: 1:11:11
minute: 'M/d h:mm', // ex: 1/1 1:11
hour: 'M/d h:mm', // ex: 1/1 1:11
day: 'M/d h', // ex: 1/1 1
month: 'y/M/d', // ex: 1911/1/1
},
displayFormats: {
second: "h:mm:ss", // ex: 1:11:11
minute: "M/d h:mm", // ex: 1/1 1:11
hour: "M/d h:mm", // ex: 1/1 1:11
day: "M/d h", // ex: 1/1 1
month: "y/M/d", // ex: 1911/1/1
},
},
ticks: {
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b',
source: 'labels', // Proportionally display the number of labels
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: "#64748b",
source: "labels", // Proportionally display the number of labels
},
border: {
color: '#64748b',
color: "#64748b",
},
grid: {
tickLength: 0, // Whether grid lines extend beyond the axis
}
},
y: {
tickLength: 0, // Whether grid lines extend beyond the axis
},
},
y: {
beginAtZero: true, // Scale includes 0
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
lineHeight: 2,
},
},
},
ticks:{
color: '#64748b',
padding: 8,
ticks: {
color: "#64748b",
padding: 8,
},
grid: {
color: '#64748b',
color: "#64748b",
},
border: {
display: false, // Hide the extra line on the left side
display: false, // Hide the extra line on the left side
},
},
},
};
/** @constant {Object} Default Chart.js scale options for horizontal charts. */
export const knownScaleHorizontalChartOptions = {
x: {
x: {
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
}
lineHeight: 2,
},
},
ticks: {
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b',
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: "#64748b",
},
grid: {
color: '#64748b',
tickLength: 0, // Whether grid lines extend beyond the axis
color: "#64748b",
tickLength: 0, // Whether grid lines extend beyond the axis
},
border: {
display:false,
display: false,
},
},
y: {
},
y: {
beginAtZero: true, // Scale includes 0
type: 'category',
type: "category",
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
lineHeight: 2,
},
},
},
ticks:{
color: '#64748b',
padding: 8,
ticks: {
color: "#64748b",
padding: 8,
},
grid: {
display:false,
color: '#64748b',
display: false,
color: "#64748b",
},
border: {
display: false, // Hide the extra line on the left side
display: false, // Hide the extra line on the left side
},
},
},
};
/** @constant {Object} Default Chart.js scale options for bar charts. */
export const knownScaleBarChartOptions = {
x: {
x: {
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
}
lineHeight: 2,
},
},
ticks: {
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b',
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: "#64748b",
},
grid: {
color: '#64748b',
tickLength: 0, // Whether grid lines extend beyond the axis
color: "#64748b",
tickLength: 0, // Whether grid lines extend beyond the axis
},
border: {
display:false,
display: false,
},
},
y: {
},
y: {
beginAtZero: true, // Scale includes 0
type: 'category',
type: "category",
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
},
lineHeight: 2,
},
},
ticks:{
color: '#64748b',
padding: 8,
ticks: {
color: "#64748b",
padding: 8,
},
grid: {
display:false,
color: '#64748b',
display: false,
color: "#64748b",
},
border: {
display: false, // Hide the extra line on the left side
display: false, // Hide the extra line on the left side
},
},
};
},
};

View File

@@ -9,24 +9,21 @@
* with browser language detection.
*/
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import enLocale from './en.json';
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import enLocale from "./en.json";
i18next
.use(LanguageDetector)
.init({
resources: {
en: {
translation: enLocale
},
i18next.use(LanguageDetector).init({
resources: {
en: {
translation: enLocale,
},
fallbackLng: 'en',
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage']
},
});
},
fallbackLng: "en",
detection: {
order: ["localStorage", "navigator"],
caches: ["localStorage"],
},
});
export default i18next;
export default i18next;

View File

@@ -14,45 +14,45 @@ import { createApp, markRaw } from "vue";
import App from "./App.vue";
import router from "./router";
import pinia from '@/stores/main';
import ToastPlugin from 'vue-toast-notification';
import cytoscape from 'cytoscape';
import dagre from 'cytoscape-dagre';
import popper from 'cytoscape-popper';
import draggable from 'vuedraggable';
import VueSweetalert2 from 'vue-sweetalert2';
import pinia from "@/stores/main";
import ToastPlugin from "vue-toast-notification";
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import popper from "cytoscape-popper";
import draggable from "vuedraggable";
import VueSweetalert2 from "vue-sweetalert2";
// import CSS
import "./assets/main.css";
import 'vue-toast-notification/dist/theme-sugar.css';
import 'sweetalert2/dist/sweetalert2.min.css';
import "vue-toast-notification/dist/theme-sugar.css";
import "sweetalert2/dist/sweetalert2.min.css";
// import PrimeVue
import PrimeVue from 'primevue/config';
import Aura from '@primevue/themes/aura';
import 'primeicons/primeicons.css'; //icons
import Sidebar from 'primevue/sidebar';
import Dropdown from 'primevue/dropdown';
import Tag from 'primevue/tag';
import ProgressBar from 'primevue/progressbar';
import TabView from 'primevue/tabview';
import TabPanel from 'primevue/tabpanel';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import ColumnGroup from 'primevue/columngroup'; // optional
import Row from 'primevue/row'; // optional
import RadioButton from 'primevue/radiobutton';
import Timeline from 'primevue/timeline';
import InputSwitch from 'primevue/inputswitch';
import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import Chart from 'primevue/chart';
import Slider from 'primevue/slider';
import Calendar from 'primevue/calendar';
import Tooltip from 'primevue/tooltip';
import Checkbox from 'primevue/checkbox';
import Dialog from 'primevue/dialog';
import ContextMenu from 'primevue/contextmenu';
import PrimeVue from "primevue/config";
import Aura from "@primevue/themes/aura";
import "primeicons/primeicons.css"; //icons
import Sidebar from "primevue/sidebar";
import Dropdown from "primevue/dropdown";
import Tag from "primevue/tag";
import ProgressBar from "primevue/progressbar";
import TabView from "primevue/tabview";
import TabPanel from "primevue/tabpanel";
import DataTable from "primevue/datatable";
import Column from "primevue/column";
import ColumnGroup from "primevue/columngroup"; // optional
import Row from "primevue/row"; // optional
import RadioButton from "primevue/radiobutton";
import Timeline from "primevue/timeline";
import InputSwitch from "primevue/inputswitch";
import InputNumber from "primevue/inputnumber";
import InputText from "primevue/inputtext";
import Chart from "primevue/chart";
import Slider from "primevue/slider";
import Calendar from "primevue/calendar";
import Tooltip from "primevue/tooltip";
import Checkbox from "primevue/checkbox";
import Dialog from "primevue/dialog";
import ContextMenu from "primevue/contextmenu";
const app = createApp(App);
@@ -62,39 +62,40 @@ pinia.use(({ store }) => {
});
// Cytoscape.js's style
cytoscape.use( dagre );
cytoscape.use( popper((ref) => ref) );
cytoscape.use(dagre);
cytoscape.use(popper((ref) => ref));
app.use(pinia);
app.use(router);
app.use(VueSweetalert2);
app.use(ToastPlugin, { // use `this.$toast` in Vue.js
position: 'bottom',
app.use(ToastPlugin, {
// use `this.$toast` in Vue.js
position: "bottom",
duration: 5000,
});
app.use(PrimeVue, { theme: { preset: Aura } });
app.component('Sidebar', Sidebar);
app.component('Dropdown', Dropdown);
app.component('Tag', Tag);
app.component('ProgressBar', ProgressBar);
app.component('TabView', TabView);
app.component('TabPanel', TabPanel);
app.component('DataTable', DataTable);
app.component('Column', Column);
app.component('ColumnGroup', ColumnGroup);
app.component('Row', Row);
app.component('RadioButton', RadioButton);
app.component('Timeline', Timeline);
app.component('InputSwitch', InputSwitch);
app.component('InputNumber', InputNumber);
app.component('InputText', InputText);
app.component('Chart', Chart);
app.component('Slider', Slider);
app.component('Calendar', Calendar);
app.component('Checkbox', Checkbox);
app.component('Dialog', Dialog);
app.component('ContextMenu', ContextMenu);
app.component('Draggable', draggable); // Drag and drop
app.directive('tooltip', Tooltip);
app.component("Sidebar", Sidebar);
app.component("Dropdown", Dropdown);
app.component("Tag", Tag);
app.component("ProgressBar", ProgressBar);
app.component("TabView", TabView);
app.component("TabPanel", TabPanel);
app.component("DataTable", DataTable);
app.component("Column", Column);
app.component("ColumnGroup", ColumnGroup);
app.component("Row", Row);
app.component("RadioButton", RadioButton);
app.component("Timeline", Timeline);
app.component("InputSwitch", InputSwitch);
app.component("InputNumber", InputNumber);
app.component("InputText", InputText);
app.component("Chart", Chart);
app.component("Slider", Slider);
app.component("Calendar", Calendar);
app.component("Checkbox", Checkbox);
app.component("Dialog", Dialog);
app.component("ContextMenu", ContextMenu);
app.component("Draggable", draggable); // Drag and drop
app.directive("tooltip", Tooltip);
app.mount("#app");

View File

@@ -19,23 +19,23 @@ export default function abbreviateNumber(totalSeconds) {
let minutes = 0;
let hours = 0;
let days = 0;
let result = '';
let symbols = ['d', 'h', 'm', 's'];
let result = "";
let symbols = ["d", "h", "m", "s"];
totalSeconds = parseInt(totalSeconds);
if(!isNaN(totalSeconds)) {
if (!isNaN(totalSeconds)) {
seconds = totalSeconds % 60;
minutes = (Math.floor(totalSeconds - seconds) / 60) % 60;
hours = (Math.floor(totalSeconds / 3600)) % 24;
hours = Math.floor(totalSeconds / 3600) % 24;
days = Math.floor(totalSeconds / (3600 * 24));
};
}
const units = [days, hours, minutes, seconds];
for(let i = 0; i < units.length; i++) {
if(units[i] > 0) result += units[i] + symbols[i] + " ";
for (let i = 0; i < units.length; i++) {
if (units[i] > 0) result += units[i] + symbols[i] + " ";
}
result = result.trim();
if(totalSeconds === 0) result = '0';
if (totalSeconds === 0) result = "0";
return result;
};
}

View File

@@ -6,24 +6,27 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module alertModal SweetAlert2 modal dialogs for user interactions. */
import Swal from 'sweetalert2';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { useFilesStore } from '@/stores/files';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useModalStore } from '@/stores/modal';
import { escapeHtml } from '@/utils/escapeHtml.js';
import Swal from "sweetalert2";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { useFilesStore } from "@/stores/files";
import { usePageAdminStore } from "@/stores/pageAdmin";
import { useModalStore } from "@/stores/modal";
import { escapeHtml } from "@/utils/escapeHtml.js";
const customClass = {
container: '!z-[99999]',
popup: '!w-[564px]',
title: '!text-xl !font-semibold !mb-2',
htmlContainer: '!text-sm !font-normal !h-full !mb-4 !leading-5',
inputLabel: '!text-sm !font-normal',
input: '!h-8 !text-sm !font-normal !shadow-inner !shadow-black !border-neutral-200',
validationMessage: '!bg-neutral-10 !text-danger',
confirmButton: '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px]',
cancelButton: '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ',
container: "!z-[99999]",
popup: "!w-[564px]",
title: "!text-xl !font-semibold !mb-2",
htmlContainer: "!text-sm !font-normal !h-full !mb-4 !leading-5",
inputLabel: "!text-sm !font-normal",
input:
"!h-8 !text-sm !font-normal !shadow-inner !shadow-black !border-neutral-200",
validationMessage: "!bg-neutral-10 !text-danger",
confirmButton:
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px]",
cancelButton:
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ",
};
/**
* Shows a modal dialog to save a new filter with a user-provided name.
@@ -33,47 +36,51 @@ const customClass = {
* @returns {Promise<boolean>} True if the filter was saved, false otherwise.
*/
export async function saveFilter(addFilterId, next = null) {
let fileName = '';
let fileName = "";
const pageAdminStore = usePageAdminStore();
const { value, isConfirmed } = await Swal.fire({
title: 'SAVE NEW FILTER',
input: 'text',
inputPlaceholder: 'Enter Filter Name.',
title: "SAVE NEW FILTER",
input: "text",
inputPlaceholder: "Enter Filter Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
inputValidator: (value) => {
if (!value) return 'You need to write something!';
if (!value) return "You need to write something!";
fileName = value;
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: '#0099FF',
reverseButtons:true,
confirmButtonColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
// Determine whether to redirect based on the return value
if(isConfirmed) { // Save succeeded
if (isConfirmed) {
// Save succeeded
await addFilterId(fileName);
// Show save complete notification
if (value) { // Example of value: yes
if (value) {
// Example of value: yes
savedSuccessfully(value);
}
// Clear the input field
fileName = '';
fileName = "";
return true;
} else { // Clicked cancel or outside the dialog; save failed.
} else {
// Clicked cancel or outside the dialog; save failed.
pageAdminStore.keepPreviousPage();
// Not every time we have nontrivial next value
if (next !== null) {
next(false);
}
}
return false;
}
}
@@ -84,17 +91,17 @@ export async function saveFilter(addFilterId, next = null) {
* @returns {Promise<void>}
*/
export async function savedSuccessfully(value) {
value = value || '';
value = value || "";
await Swal.fire({
title: 'SAVE COMPLETE',
title: "SAVE COMPLETE",
html: `<span class="text-primary">${escapeHtml(value)}</span> has been saved in Lucia.`,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Prompts the user to save unsaved filter changes before leaving the
* Map page. Handles confirm (save), cancel (discard), and backdrop
@@ -112,23 +119,23 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
const pageAdminStore = usePageAdminStore();
const result = await Swal.fire({
title: 'SAVE YOUR FILTER?',
html: 'If you want to continue using this filter in any other page, please select [Yes].',
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
title: "SAVE YOUR FILTER?",
html: "If you want to continue using this filter in any other page, please select [Yes].",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
if(result.isConfirmed) {
if(allMapDataStore.createFilterId) {
if (result.isConfirmed) {
if (allMapDataStore.createFilterId) {
await allMapDataStore.updateFilter();
if(allMapDataStore.isUpdateFilter) {
if (allMapDataStore.isUpdateFilter) {
await savedSuccessfully(allMapDataStore.filterName);
}
} else {
@@ -137,7 +144,7 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
}
logOut ? logOut() : next(toPath);
} else if(result.dismiss === 'cancel') {
} else if (result.dismiss === "cancel") {
// console.log('popup cancel case', );
// Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
@@ -145,18 +152,17 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
allMapDataStore.tempFilterId = null;
logOut ? logOut() : next(toPath);
} else if(result.dismiss === 'backdrop') {
} else if (result.dismiss === "backdrop") {
// console.log('popup backdrop case', );
// Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
pageAdminStore.keepPreviousPage();
if(!logOut){
if (!logOut) {
next(false);
};
}
}
};
}
/**
* Shows a modal dialog to save a new conformance rule with a
* user-provided name.
@@ -166,37 +172,40 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
* @returns {Promise<boolean>} True if the rule was saved, false otherwise.
*/
export async function saveConformance(addConformanceCreateCheckId) {
let fileName = '';
let fileName = "";
const { value, isConfirmed } = await Swal.fire({
title: 'SAVE NEW RULE',
input: 'text',
inputPlaceholder: 'Enter Rule Name.',
title: "SAVE NEW RULE",
input: "text",
inputPlaceholder: "Enter Rule Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
inputValidator: (value) => {
if (!value) return 'You need to write something!';
if (!value) return "You need to write something!";
fileName = value;
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: '#0099FF',
reverseButtons:true,
confirmButtonColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
// Determine whether to redirect based on the return value
if(isConfirmed) { // Save succeeded
if (isConfirmed) {
// Save succeeded
await addConformanceCreateCheckId(fileName);
// Show save complete notification
if (value) savedSuccessfully(value);
// Clear the input field
fileName = '';
fileName = "";
return true;
} else { // Clicked cancel or outside the dialog; save failed.
} else {
// Clicked cancel or outside the dialog; save failed.
return false;
}
}
@@ -212,10 +221,15 @@ export async function saveConformance(addConformanceCreateCheckId) {
* @param {Function} [logOut] - Optional logout function.
* @returns {Promise<void>}
*/
export async function leaveConformance(next, addConformanceCreateCheckId, toPath, logOut) {
export async function leaveConformance(
next,
addConformanceCreateCheckId,
toPath,
logOut,
) {
const conformanceStore = useConformanceStore();
const result = await showConfirmationDialog();
if (result.isConfirmed) {
await handleConfirmed(conformanceStore, addConformanceCreateCheckId);
} else {
@@ -228,16 +242,16 @@ export async function leaveConformance(next, addConformanceCreateCheckId, toPath
*/
async function showConfirmationDialog() {
return Swal.fire({
title: 'SAVE YOUR RULE?',
icon: 'warning',
iconColor: '#FF3366',
title: "SAVE YOUR RULE?",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
customClass: customClass
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
}
@@ -248,7 +262,10 @@ async function showConfirmationDialog() {
* @returns {Promise<void>}
*/
async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) {
if (conformanceStore.conformanceFilterCreateCheckId || conformanceStore.conformanceLogCreateCheckId) {
if (
conformanceStore.conformanceFilterCreateCheckId ||
conformanceStore.conformanceLogCreateCheckId
) {
await conformanceStore.updateConformance();
if (conformanceStore.isUpdateConformance) {
await savedSuccessfully(conformanceStore.conformanceFileName);
@@ -267,13 +284,19 @@ async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) {
* @param {Function} [logOut] - Optional logout function.
* @returns {Promise<void>}
*/
async function handleDismiss(dismissType, conformanceStore, next, toPath, logOut) {
async function handleDismiss(
dismissType,
conformanceStore,
next,
toPath,
logOut,
) {
switch (dismissType) {
case 'cancel':
case "cancel":
resetTempCheckId(conformanceStore);
logOut ? logOut() : next(toPath);
break;
case 'backdrop':
case "backdrop":
if (!logOut) {
next(false);
}
@@ -304,38 +327,38 @@ function resetTempCheckId(conformanceStore) {
export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
// msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv' | 'not a CSV file'
// type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix' | mime_type
let value = '';
let value = "";
switch (failureType) {
case 'size':
value = 'File size exceeds 90MB.';
case "size":
value = "File size exceeds 90MB.";
break;
case 'encoding':
case "encoding":
value = `Please use UTF-8 for character encoding: (Row #${failureLoc})`;
break;
case 'insufficient_columns':
value = 'Need at least five columns of data.';
case "insufficient_columns":
value = "Need at least five columns of data.";
break;
case 'empty':
value = 'Need at least one record of data.';
case "empty":
value = "Need at least one record of data.";
break;
case 'name_suffix':
case 'mime_type':
value = 'File is not in csv format.';
case "name_suffix":
case "mime_type":
value = "File is not in csv format.";
break;
default:
value = escapeHtml(failureMsg);
break;
}
await Swal.fire({
title: 'IMPORT FAILED',
title: "IMPORT FAILED",
html: value,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'error',
iconColor: '#FF3366',
customClass: customClass
})
};
icon: "error",
iconColor: "#FF3366",
customClass: customClass,
});
}
/**
* Shows an error modal for the second stage of upload validation failure,
* listing individual row-level errors.
@@ -345,73 +368,73 @@ export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
* @returns {Promise<void>}
*/
export async function uploadFailedSecond(detail) {
let srt = '';
let manySrt = '';
let srt = "";
let manySrt = "";
detail.forEach(i => {
let content = '';
let key = '';
detail.forEach((i) => {
let content = "";
let key = "";
switch (i.type) {
case 'too_many':
manySrt = 'There are more errors.';
case "too_many":
manySrt = "There are more errors.";
break;
case 'unrecognized':
case "unrecognized":
content = `<li>Data unrecognizable in Status Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'malformed':
case "malformed":
content = `<li>Data malformed in Timestamp Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'missing':
case "missing":
switch (i.loc[2]) {
case 'case id':
key = 'Case ID';
case "case id":
key = "Case ID";
break;
case 'timestamp':
key = 'Timestamp';
case "timestamp":
key = "Timestamp";
break;
case 'name':
key = 'Activity';
case "name":
key = "Activity";
break;
case 'instance':
key = 'Activity Instance ID';
case "instance":
key = "Activity Instance ID";
break;
case 'status':
key = 'Status';
case "status":
key = "Status";
break;
default:
key = i.loc[2];
break;
}
content = `<li>Data missing in ${key} Column: (Row #${i.loc[1]})</li>`;
break;
break;
}
srt += content;
});
await Swal.fire({
title: 'IMPORT FAILED',
title: "IMPORT FAILED",
html: `<div class="text-left mx-3 space-y-1"><p>Error(s) detected:</p><ul class="list-disc ml-6">${srt}</ul><p>${manySrt} Please check.</p></div>`,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'error',
iconColor: '#FF3366',
customClass: customClass
icon: "error",
iconColor: "#FF3366",
customClass: customClass,
});
srt = '';
};
srt = "";
}
/**
* Shows a timed success notification after a file upload completes.
* @returns {Promise<void>}
*/
export async function uploadSuccess() {
await Swal.fire({
title: 'IMPORT COMPLETED',
title: "IMPORT COMPLETED",
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Shows a confirmation dialog before uploading a file. Proceeds with
* the upload only if the user confirms.
@@ -422,23 +445,23 @@ export async function uploadSuccess() {
export async function uploadConfirm(fetchData) {
const filesStore = useFilesStore();
const result = await Swal.fire({
title: 'ARE YOU SURE?',
html: 'After importing, you wont be able to modify labels.',
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
title: "ARE YOU SURE?",
html: "After importing, you wont be able to modify labels.",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
if(result.isConfirmed) {
if (result.isConfirmed) {
filesStore.uploadLog(fetchData);
}
};
}
/**
* Shows a non-dismissable loading spinner during file upload.
* @returns {Promise<void>}
@@ -449,8 +472,8 @@ export async function uploadloader() {
showConfirmButton: false,
allowOutsideClick: false,
customClass: customClass,
})
};
});
}
/**
* Shows a modal dialog for renaming a file.
*
@@ -463,39 +486,40 @@ export async function uploadloader() {
export async function renameModal(rename, type, id, baseName) {
const fileName = baseName;
const { value, isConfirmed } = await Swal.fire({
title: 'RENAME',
input: 'text',
inputPlaceholder: 'Enter File Name.',
title: "RENAME",
input: "text",
inputPlaceholder: "Enter File Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">edit_square</span>',
iconColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">edit_square</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: '#0099FF',
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
const inputField = Swal.getInput();
inputField.addEventListener('input', function() {
inputField.addEventListener("input", function () {
if (!inputField.value.trim()) {
confirmButton.classList.add('disable-hover');
confirmButton.classList.add("disable-hover");
confirmButton.disabled = true;
} else {
confirmButton.classList.remove('disable-hover');
confirmButton.classList.remove("disable-hover");
confirmButton.disabled = false;
}
});
}
},
});
// Rename succeeded
if(isConfirmed) await rename(type, id, value);
if (isConfirmed) await rename(type, id, value);
// Clear the field: fileName = ''
}
/**
@@ -511,46 +535,50 @@ export async function deleteFileModal(files, type, id, name) {
const filesStore = useFilesStore();
const safeName = escapeHtml(name);
const htmlText = files.length === 0 ? `Do you really want to delete <span class="text-primary">${safeName}</span>?` : `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const htmlText =
files.length === 0
? `Do you really want to delete <span class="text-primary">${safeName}</span>?`
: `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const deleteCustomClass = { ...customClass };
deleteCustomClass.confirmButton = '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger';
deleteCustomClass.confirmButton =
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger";
const result = await Swal.fire({
title: 'CONFIRM DELETION',
title: "CONFIRM DELETION",
html: htmlText,
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonColor: '#ffffff',
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonColor: "#ffffff",
showCancelButton: true,
cancelButtonColor: '#FF3366',
cancelButtonColor: "#FF3366",
customClass: deleteCustomClass,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #FF3366';
}
confirmButton.style.border = "1px solid #FF3366";
},
});
if(result.isConfirmed) {
if (result.isConfirmed) {
filesStore.deleteFile(type, id);
}
};
}
/**
* Shows a timed success notification after file deletion.
* @returns {Promise<void>}
*/
export async function deleteSuccess() {
await Swal.fire({
title: 'FILE(S) DELETED',
title: "FILE(S) DELETED",
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Shows an informational modal about files deleted by other users,
* then records the deletions and refreshes the file list.
@@ -565,22 +593,25 @@ export async function reallyDeleteInformation(files, reallyDeleteData) {
const deleteCustomClass = { ...customClass };
const htmlText = `<div class="text-left mx-4 space-y-1"><p>The following file(s) have been deleted by other user(s):</p><ul class="list-disc ml-6">${files}</ul></div>`;
deleteCustomClass.confirmButton = '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-primary !bg-neutral-10 !border !border-primary';
deleteCustomClass.confirmButton =
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-primary !bg-neutral-10 !border !border-primary";
deleteCustomClass.cancelButton = null;
await Swal.fire({
title: 'FILE(S) DELETED BY OTHER USER(S)',
title: "FILE(S) DELETED BY OTHER USER(S)",
html: htmlText,
icon: 'info',
iconColor: '#0099FF',
icon: "info",
iconColor: "#0099FF",
customClass: deleteCustomClass,
confirmButtonColor: '#ffffff',
confirmButtonColor: "#ffffff",
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #0099FF';
}
confirmButton.style.border = "1px solid #0099FF";
},
});
await Promise.all(reallyDeleteData.map(file => filesStore.deletionRecord(file.id)));
await Promise.all(
reallyDeleteData.map((file) => filesStore.deletionRecord(file.id)),
);
await filesStore.fetchAllFiles();
}
@@ -589,21 +620,21 @@ export async function reallyDeleteInformation(files, reallyDeleteData) {
* page with unsaved edits.
* @returns {Promise<void>}
*/
export async function leaveAccountManagementToRemind(){
export async function leaveAccountManagementToRemind() {
const modalStore = useModalStore();
const result = await Swal.fire({
title: 'SAVE YOUR EDIT?',
icon: 'info',
title: "SAVE YOUR EDIT?",
icon: "info",
showCancelButton: true,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #0099FF';
}
confirmButton.style.border = "1px solid #0099FF";
},
});
if(result.isConfirmed) {
if (result.isConfirmed) {
return;
} else {
modalStore.openModal();
}
};
}

View File

@@ -6,8 +6,8 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module apiError Centralized API error handler with toast notifications. */
import {useToast} from 'vue-toast-notification';
import 'vue-toast-notification/dist/theme-sugar.css';
import { useToast } from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
/**
* Handles API errors by showing a toast notification.
@@ -19,5 +19,5 @@ import 'vue-toast-notification/dist/theme-sugar.css';
*/
export default function apiError(error, toastMessage) {
const $toast = useToast();
$toast.default(toastMessage, {position: 'bottom'});
$toast.default(toastMessage, { position: "bottom" });
}

View File

@@ -9,18 +9,18 @@
* interactive node/edge highlighting, tooltips, and position persistence.
*/
import cytoscape from 'cytoscape';
import spread from 'cytoscape-spread';
import dagre from 'cytoscape-dagre';
import fcose from 'cytoscape-fcose';
import cola from 'cytoscape-cola';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { useMapPathStore } from '@/stores/mapPathStore';
import { getTimeLabel } from '@/module/timeLabel.js';
import { createTooltipContent } from '@/module/tooltipContent.js';
import { useCytoscapeStore } from '@/stores/cytoscapeStore';
import { SAVE_KEY_NAME } from '@/constants/constants.js';
import cytoscape from "cytoscape";
import spread from "cytoscape-spread";
import dagre from "cytoscape-dagre";
import fcose from "cytoscape-fcose";
import cola from "cytoscape-cola";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import { useMapPathStore } from "@/stores/mapPathStore";
import { getTimeLabel } from "@/module/timeLabel.js";
import { createTooltipContent } from "@/module/tooltipContent.js";
import { useCytoscapeStore } from "@/stores/cytoscapeStore";
import { SAVE_KEY_NAME } from "@/constants/constants.js";
/**
* Composes display text for frequency-type data layer values.
@@ -33,8 +33,14 @@ import { SAVE_KEY_NAME } from '@/constants/constants.js';
*/
const composeFreqTypeText = (baseText, dataLayerOption, optionValue) => {
let text = baseText;
const textInt = dataLayerOption === 'rel_freq' ? baseText + optionValue * 100 + "%" : baseText + optionValue;
const textFloat = dataLayerOption === 'rel_freq' ? baseText + (optionValue * 100).toFixed(2) + "%" : baseText + optionValue.toFixed(2);
const textInt =
dataLayerOption === "rel_freq"
? baseText + optionValue * 100 + "%"
: baseText + optionValue;
const textFloat =
dataLayerOption === "rel_freq"
? baseText + (optionValue * 100).toFixed(2) + "%"
: baseText + optionValue.toFixed(2);
// Check if the value is an integer; if not, round to 2 decimal places.
text = Math.trunc(optionValue) === optionValue ? textInt : textFloat;
return text;
@@ -65,7 +71,14 @@ cytoscape.use(cola);
* @param {HTMLElement} graphId - The DOM container element for Cytoscape.
* @returns {cytoscape.Core} The configured Cytoscape instance.
*/
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
export default function cytoscapeMap(
mapData,
dataLayerType,
dataLayerOption,
curveStyle,
rank,
graphId,
) {
// Set the color and style for each node and edge
let nodes = mapData.nodes;
let edges = mapData.edges;
@@ -78,209 +91,234 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
edges: edges, // Edge data
},
layout: {
name: 'dagre',
name: "dagre",
rankDir: rank, // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
},
style: [
// Style changes when a node is selected
{
selector: 'node:selected',
selector: "node:selected",
style: {
'border-color': 'red',
'border-width': '3',
"border-color": "red",
"border-width": "3",
},
},
// Node styling
{
selector: 'node',
selector: "node",
style: {
'label':
function (node) { // Text to display on the node
// node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = '';
const STRING_LIMIT = 8;
if (node.data('label').length > STRING_LIMIT) {
// If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
// Using data() because Cytoscape converts array data to function calls
text = `${node.data('label').substr(0, STRING_LIMIT)}...\n\n`;
} else { // Pad with spaces to match the label width for consistent sizing
text = `${node.data('label').padEnd(STRING_LIMIT, ' ')}\n\n`
label: function (node) {
// Text to display on the node
// node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = "";
const STRING_LIMIT = 8;
if (node.data("label").length > STRING_LIMIT) {
// If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
// Using data() because Cytoscape converts array data to function calls
text = `${node.data("label").substr(0, STRING_LIMIT)}...\n\n`;
} else {
// Pad with spaces to match the label width for consistent sizing
text = `${node.data("label").padEnd(STRING_LIMIT, " ")}\n\n`;
}
// In elements, activity is categorized as default, so check if the node is an activity before adding text.
// Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
// Relative values need to be converted to percentages (%)
if (node.data("type") === "activity") {
let textDurRel;
let timeLabelInt;
let timeLabelFloat;
let textTimeLabel;
switch (dataLayerType) {
case "freq": // Frequency
text = composeFreqTypeText(
text,
dataLayerOption,
optionValue,
);
break;
case "duration": // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = text + getTimeLabel(optionValue);
timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// Check if the value is an integer; if not, round to 2 decimal places.
textTimeLabel =
Math.trunc(optionValue) === optionValue
? timeLabelInt
: timeLabelFloat;
text =
dataLayerOption === "rel_duration"
? textDurRel
: textTimeLabel;
break;
}
}
// In elements, activity is categorized as default, so check if the node is an activity before adding text.
// Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
// Relative values need to be converted to percentages (%)
if (node.data('type') === 'activity') {
let textDurRel;
let timeLabelInt;
let timeLabelFloat;
let textTimeLabel;
switch (dataLayerType) {
case 'freq': // Frequency
text = composeFreqTypeText(text, dataLayerOption, optionValue);
break;
case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = text + getTimeLabel(optionValue);
timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// Check if the value is an integer; if not, round to 2 decimal places.
textTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
text = dataLayerOption === 'rel_duration' ? textDurRel : textTimeLabel;
break;
}
}
return text;
},
'text-opacity': 0.7,
'background-color': 'data(backgroundColor)',
'border-color': 'data(bordercolor)',
'border-width':
function (node) {
return node.data('type') === 'activity' ? '1' : '2';
},
'background-image': 'data(nodeImageUrl)',
'background-opacity': 'data(backgroundOpacity)', // Transparent background
'border-opacity': 'data(borderOpacity)', // Transparent border
'shape': 'data(shape)',
'text-wrap': 'wrap',
'text-max-width': 'data(width)', // Wrap text within the node
'text-overflow-wrap': 'anywhere', // Allow wrapping at any position
'text-margin-x': function (node) {
return node.data('type') === 'activity' ? -5 : 0;
return text;
},
'text-margin-y': function (node) {
return node.data('type') === 'activity' ? 2 : 0;
"text-opacity": 0.7,
"background-color": "data(backgroundColor)",
"border-color": "data(bordercolor)",
"border-width": function (node) {
return node.data("type") === "activity" ? "1" : "2";
},
'padding': function (node) {
return node.data('type') === 'activity' ? 0 : 0;
"background-image": "data(nodeImageUrl)",
"background-opacity": "data(backgroundOpacity)", // Transparent background
"border-opacity": "data(borderOpacity)", // Transparent border
shape: "data(shape)",
"text-wrap": "wrap",
"text-max-width": "data(width)", // Wrap text within the node
"text-overflow-wrap": "anywhere", // Allow wrapping at any position
"text-margin-x": function (node) {
return node.data("type") === "activity" ? -5 : 0;
},
"text-margin-y": function (node) {
return node.data("type") === "activity" ? 2 : 0;
},
padding: function (node) {
return node.data("type") === "activity" ? 0 : 0;
},
"text-justification": "left",
"text-halign": "center",
"text-valign": "center",
height: "data(height)",
width: "data(width)",
color: "data(textColor)",
"line-height": "0.7rem",
"font-size": function (node) {
return node.data("type") === "activity" ? 14 : 14;
},
'text-justification': 'left',
'text-halign': 'center',
'text-valign': 'center',
'height': 'data(height)',
'width': 'data(width)',
'color': 'data(textColor)',
'line-height': '0.7rem',
'font-size':
function (node) {
return node.data('type') === 'activity' ? 14 : 14;
},
},
},
// Edge styling
{
selector: 'edge',
selector: "edge",
style: {
'content': function (edge) { // Text displayed on the edge
content: function (edge) {
// Text displayed on the edge
let optionValue = edge.data(`${dataLayerType}.${dataLayerOption}`);
let result = '';
let result = "";
let edgeInt;
let edgeFloat;
let edgeDurRel;
let timeLabelInt;
let timeLabelFloat;
let edgeTimeLabel;
if (optionValue === '') return optionValue;
if (optionValue === "") return optionValue;
switch (dataLayerType) {
case 'freq':
edgeInt = dataLayerOption === 'rel_freq' ? optionValue * 100 + "%" : optionValue;
edgeFloat = dataLayerOption === 'rel_freq' ? (optionValue * 100).toFixed(2) + "%" : optionValue.toFixed(2);
case "freq":
edgeInt =
dataLayerOption === "rel_freq"
? optionValue * 100 + "%"
: optionValue;
edgeFloat =
dataLayerOption === "rel_freq"
? (optionValue * 100).toFixed(2) + "%"
: optionValue.toFixed(2);
// Check if the value is an integer; if not, round to 2 decimal places.
result = Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
result =
Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
break;
case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
case "duration": // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
edgeDurRel = (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = getTimeLabel(optionValue);
timeLabelFloat = getTimeLabel(optionValue.toFixed(2));
edgeTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
edgeTimeLabel =
Math.trunc(optionValue) === optionValue
? timeLabelInt
: timeLabelFloat;
result = dataLayerOption === 'rel_duration' ? edgeDurRel : edgeTimeLabel;
result =
dataLayerOption === "rel_duration"
? edgeDurRel
: edgeTimeLabel;
break;
};
}
return result;
},
'curve-style': curveStyle, // unbundled-bezier | taxi
'overlay-opacity': 0, // Set overlay-opacity to 0 to remove the gray shadow
'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc
"curve-style": curveStyle, // unbundled-bezier | taxi
"overlay-opacity": 0, // Set overlay-opacity to 0 to remove the gray shadow
"target-arrow-shape": "triangle", // Arrow shape pointing to target: triangle
color: "gray", //#0066cc
//'control-point-step-size':100, // Distance between Bezier curve control points
'width': 'data(lineWidth)',
'line-style': 'data(edgeStyle)',
width: "data(lineWidth)",
"line-style": "data(edgeStyle)",
"text-margin-y": "0.7rem",
//"text-rotation": "autorotate",
}
}, {
selector: '.highlight-edge',
style: {
'color': '#0099FF',
'line-color': '#0099FF',
'overlay-color': '#0099FF',
'overlay-opacity': 0.2,
'overlay-padding': '5px',
},
}, {
selector: '.highlight-node',
},
{
selector: ".highlight-edge",
style: {
'overlay-color': '#0099FF',
'overlay-opacity': 0.01,
'overlay-padding': '5px',
color: "#0099FF",
"line-color": "#0099FF",
"overlay-color": "#0099FF",
"overlay-opacity": 0.2,
"overlay-padding": "5px",
},
}, {
selector: 'edge[source = target]', // Select self-loop edges
},
{
selector: ".highlight-node",
style: {
'loop-direction': '0deg', // Control the loop direction
'loop-sweep': '-60deg', // Control the loop arc; adjust to change size
'control-point-step-size': 50 // Control the loop radius; increase to enlarge the loop
}
"overlay-color": "#0099FF",
"overlay-opacity": 0.01,
"overlay-padding": "5px",
},
},
{
selector: "edge[source = target]", // Select self-loop edges
style: {
"loop-direction": "0deg", // Control the loop direction
"loop-sweep": "-60deg", // Control the loop arc; adjust to change size
"control-point-step-size": 50, // Control the loop radius; increase to enlarge the loop
},
},
],
});
// When an edge is clicked, apply glow effect to the edge and its label
cy.on('tap', 'edge', function (event) {
cy.edges().removeClass('highlight-edge');
event.target.addClass('highlight-edge');
cy.on("tap", "edge", function (event) {
cy.edges().removeClass("highlight-edge");
event.target.addClass("highlight-edge");
});
// When a node is clicked, apply glow effect to the node and adjacent edges
cy.on('tap, mousedown', 'node', function (event) {
cy.on("tap, mousedown", "node", function (event) {
useMapPathStore().onNodeClickHighlightEdges(event.target);
});
// When an edge is clicked, apply glow effect to the edge and both endpoint nodes
cy.on('tap, mousedown', 'edge', function (event) {
cy.on("tap, mousedown", "edge", function (event) {
useMapPathStore().onEdgeClickHighlightNodes(event.target);
});
// creat tippy.js
let tip;
cy.on('mouseover', 'node', function (event) {
cy.on("mouseover", "node", function (event) {
const node = event.target;
let ref = node.popperRef()
let dummyDomEle = document.createElement('div');
let content = createTooltipContent(node.data('label'));
tip = new tippy(dummyDomEle, { // tippy props:
let ref = node.popperRef();
let dummyDomEle = document.createElement("div");
let content = createTooltipContent(node.data("label"));
tip = new tippy(dummyDomEle, {
// tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: 'manual',
content: content
trigger: "manual",
content: content,
});
if (node.data("label").length > 10) tip.show();
});
cy.on('mouseout', 'node', function (event) {
cy.on("mouseout", "node", function (event) {
tip?.hide();
});
@@ -290,12 +328,20 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
cytoscapeStore.loadPositionsFromStorage(rank);
// Check if localStorage has previously saved visit data.
// If saved node positions exist, restore them for rendering.
if (localStorage.getItem(SAVE_KEY_NAME) && JSON.parse(localStorage.getItem(SAVE_KEY_NAME))) {
const allGraphsRemembered = JSON.parse(localStorage.getItem(SAVE_KEY_NAME));
const currentGraphNodesRemembered =
allGraphsRemembered[cytoscapeStore.currentGraphId] ? allGraphsRemembered[cytoscapeStore.currentGraphId][rank] : null; // May be undefined
if (
localStorage.getItem(SAVE_KEY_NAME) &&
JSON.parse(localStorage.getItem(SAVE_KEY_NAME))
) {
const allGraphsRemembered = JSON.parse(
localStorage.getItem(SAVE_KEY_NAME),
);
const currentGraphNodesRemembered = allGraphsRemembered[
cytoscapeStore.currentGraphId
]
? allGraphsRemembered[cytoscapeStore.currentGraphId][rank]
: null; // May be undefined
if (currentGraphNodesRemembered) {
currentGraphNodesRemembered.forEach(nodeRemembered => {
currentGraphNodesRemembered.forEach((nodeRemembered) => {
const nodeToDecide = cy.getElementById(nodeRemembered.id);
if (nodeToDecide) {
nodeToDecide.position(nodeRemembered.position);
@@ -305,15 +351,23 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
}
// Save the current positions of all nodes when the view is first entered
const allNodes = cy.nodes();
allNodes.forEach(nodeFirstlySave => {
cytoscapeStore.saveNodePosition(nodeFirstlySave.id(), nodeFirstlySave.position(), rank);
allNodes.forEach((nodeFirstlySave) => {
cytoscapeStore.saveNodePosition(
nodeFirstlySave.id(),
nodeFirstlySave.position(),
rank,
);
});
// After node positions change, save the updated positions.
// rank represents whether the user is in horizontal or vertical layout mode.
cy.on('dragfree', 'node', (event) => {
cy.on("dragfree", "node", (event) => {
const nodeToSave = event.target;
cytoscapeStore.saveNodePosition(nodeToSave.id(), nodeToSave.position(), rank);
cytoscapeStore.saveNodePosition(
nodeToSave.id(),
nodeToSave.position(),
rank,
);
});
});

View File

@@ -9,13 +9,13 @@
* individual trace visualization.
*/
import cytoscape from 'cytoscape';
import dagre from 'cytoscape-dagre';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { createTooltipContent } from '@/module/tooltipContent.js';
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import { createTooltipContent } from "@/module/tooltipContent.js";
cytoscape.use( dagre );
cytoscape.use(dagre);
/**
* Creates a Cytoscape.js instance for rendering a single trace's
@@ -36,75 +36,79 @@ export default function cytoscapeMapTrace(nodes, edges, graphId) {
edges: edges, // Edge data
},
layout: {
name: 'dagre',
rankDir: 'LR' // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
name: "dagre",
rankDir: "LR", // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
},
style: [
// Node styling
{
selector: 'node',
selector: "node",
style: {
'label':
function(node) { // Text to display on the node
let text = '';
label: function (node) {
// Text to display on the node
let text = "";
// node.data('label') accesses the original array value at node.data.label
text = node.data('label').length > 18 ? `${node.data('label').substr(0,15)}...` : `${node.data('label')}`;
text =
node.data("label").length > 18
? `${node.data("label").substr(0, 15)}...`
: `${node.data("label")}`;
return text
return text;
},
'text-opacity': 0.7,
'background-color': 'data(backgroundColor)',
'border-color': 'data(bordercolor)',
'border-width': '1',
'shape': 'data(shape)',
'text-wrap': 'wrap',
'text-max-width': 75,
'text-halign': 'center',
'text-valign': 'center',
'height': 'data(height)',
'width': 'data(width)',
'color': '#001933',
'font-size': 14,
}
"text-opacity": 0.7,
"background-color": "data(backgroundColor)",
"border-color": "data(bordercolor)",
"border-width": "1",
shape: "data(shape)",
"text-wrap": "wrap",
"text-max-width": 75,
"text-halign": "center",
"text-valign": "center",
height: "data(height)",
width: "data(width)",
color: "#001933",
"font-size": 14,
},
},
// Edge styling
{
selector: 'edge',
selector: "edge",
style: {
'curve-style': 'taxi', // unbundled-bezier | taxi
'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc
'width': 'data(lineWidth)',
'line-style': 'data(style)',
}
"curve-style": "taxi", // unbundled-bezier | taxi
"target-arrow-shape": "triangle", // Arrow shape pointing to target: triangle
color: "gray", //#0066cc
width: "data(lineWidth)",
"line-style": "data(style)",
},
},
// Style changes when a node is selected
{
selector: 'node:selected',
style:{
'border-color': 'red',
'border-width': '3',
}
selector: "node:selected",
style: {
"border-color": "red",
"border-width": "3",
},
},
],
});
// creat tippy.js
let tip;
cy.on('mouseover', 'node', function(event) {
const node = event.target
let ref = node.popperRef()
let dummyDomEle = document.createElement('div');
let content = createTooltipContent(node.data('label'));
tip = new tippy(dummyDomEle, { // tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: 'manual',
content:content
});
cy.on("mouseover", "node", function (event) {
const node = event.target;
let ref = node.popperRef();
let dummyDomEle = document.createElement("div");
let content = createTooltipContent(node.data("label"));
tip = new tippy(dummyDomEle, {
// tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: "manual",
content: content,
});
tip.show();
})
cy.on('mouseout', 'node', function(event) {
});
cy.on("mouseout", "node", function (event) {
tip.hide();
});
}

View File

@@ -12,13 +12,13 @@
* @returns {string} The formatted string with commas (e.g. "1,000,000").
*/
const formatNumberWithCommas = (numberStr) => {
let reversedStr = numberStr.split('').reverse().join('');
let reversedStr = numberStr.split("").reverse().join("");
let groupedStr = reversedStr.match(/.{1,3}/g);
let joinedStr = groupedStr.join(',');
let finalStr = joinedStr.split('').reverse().join('');
let joinedStr = groupedStr.join(",");
let finalStr = joinedStr.split("").reverse().join("");
return finalStr;
}
};
/**
* Converts a number to a string with comma-separated thousands.
@@ -29,7 +29,7 @@ const formatNumberWithCommas = (numberStr) => {
* @returns {string} The formatted number string (e.g. "1,234.56").
*/
export default function numberLabel(num) {
let parts = num.toString().split('.');
let parts = num.toString().split(".");
parts[0] = formatNumberWithCommas(parts[0]);
return parts.join('.');
return parts.join(".");
}

View File

@@ -6,7 +6,7 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module setChartData Chart.js data transformation utilities. */
import getMoment from 'moment';
import getMoment from "moment";
/**
* Extends backend chart data with extrapolated boundary points for
@@ -24,30 +24,30 @@ import getMoment from 'moment';
* with boundary points.
*/
export function setLineChartData(baseData, xMax, xMin, isPercent, yMax, yMin) {
// Convert baseData to an array of objects with x and y properties
let data = baseData.map(i => ({ x: i.x, y: i.y }));
// Convert baseData to an array of objects with x and y properties
let data = baseData.map((i) => ({ x: i.x, y: i.y }));
// Calculate the Y-axis minimum value
let b = calculateYMin(baseData, isPercent, yMin, yMax);
// Calculate the Y-axis maximum value
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// Prepend the minimum value
data.unshift({
x: xMin,
y: b,
});
// Append the maximum value
data.push({
x: xMax,
y: mf,
});
return data;
};
/**
// Calculate the Y-axis minimum value
let b = calculateYMin(baseData, isPercent, yMin, yMax);
// Calculate the Y-axis maximum value
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// Prepend the minimum value
data.unshift({
x: xMin,
y: b,
});
// Append the maximum value
data.push({
x: xMax,
y: mf,
});
return data;
}
/**
* Extrapolates the Y-axis minimum boundary value using linear
* interpolation from the first two data points.
*
@@ -65,7 +65,7 @@ function calculateYMin(baseData, isPercent, yMin, yMax) {
let f = baseData[1].y;
let b = (e * d - a * d - f * a - f * c) / (e - c - a);
return clampValue(b, isPercent, yMin, yMax);
};
}
/**
* Extrapolates the Y-axis maximum boundary value using linear
* interpolation from the last two data points.
@@ -84,7 +84,7 @@ function calculateYMax(baseData, isPercent, yMin, yMax) {
let me = 11;
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
return clampValue(mf, isPercent, yMin, yMax);
};
}
/**
* Clamps a value within a specified range. If isPercent is true, the
* value is clamped to [0, 1]; otherwise to [min, max].
@@ -107,11 +107,11 @@ function clampValue(value, isPercent, min, max) {
return max;
}
if (value <= min) {
return min;
return min;
}
}
return value;
};
}
/**
* Converts backend chart data timestamps to formatted date strings
* for bar charts.
@@ -122,14 +122,14 @@ function clampValue(value, isPercent, min, max) {
* as "YYYY/M/D hh:mm:ss".
*/
export function setBarChartData(baseData) {
let data = baseData.map(i =>{
let data = baseData.map((i) => {
return {
x: getMoment(i.x).format('YYYY/M/D hh:mm:ss'),
y: i.y
}
})
return data
};
x: getMoment(i.x).format("YYYY/M/D hh:mm:ss"),
y: i.y,
};
});
return data;
}
/**
* Divides a time range into evenly spaced time points.
*
@@ -149,9 +149,9 @@ export function timeRange(minTime, maxTime, amount) {
for (let i = 0; i < amount; i++) {
timeRange.push(startTime + timeGap * i);
}
timeRange = timeRange.map(value => Math.round(value));
timeRange = timeRange.map((value) => Math.round(value));
return timeRange;
};
}
/**
* Generates smooth Y-axis values using cubic Bezier interpolation
* to produce evenly spaced ticks matching the X-axis divisions.
@@ -163,39 +163,40 @@ export function timeRange(minTime, maxTime, amount) {
*/
export function yTimeRange(data, yAmount, yMax) {
const yRange = [];
const yGap = (1/ (yAmount-1));
const yGap = 1 / (yAmount - 1);
// Cubic Bezier curve formula
const threebsr = function (t, a1, a2, a3, a4) {
return (
(1 - t) * (1 - t) * (1 - t) * a1 +
3 * t * (1 - t)* (1 - t) * a2 +
3 * t * t * (1 - t) * a3 +
t * t * t * a4
)
3 * t * (1 - t) * (1 - t) * a2 +
3 * t * t * (1 - t) * a3 +
t * t * t * a4
);
};
for (let j = 0; j < data.length - 1; j++) {
for (let i = 0; i <= 1; i += yGap*11) {
yRange.push(threebsr(i, data[j].y, data[j].y, data[j + 1].y, data[j + 1].y));
for (let j = 0; j < data.length - 1; j++) {
for (let i = 0; i <= 1; i += yGap * 11) {
yRange.push(
threebsr(i, data[j].y, data[j].y, data[j + 1].y, data[j + 1].y),
);
}
}
if(yRange.length < yAmount) {
if (yRange.length < yAmount) {
let len = yAmount - yRange.length;
for (let i = 0; i < len; i++) {
yRange.push(yRange[yRange.length - 1]);
}
}
else if(yRange.length > yAmount) {
} else if (yRange.length > yAmount) {
let len = yRange.length - yAmount;
for(let i = 0; i < len; i++) {
for (let i = 0; i < len; i++) {
yRange.splice(1, 1);
}
}
return yRange;
};
}
/**
* Finds the index of the closest value in an array to the given target.
*
@@ -217,7 +218,7 @@ export function getXIndex(data, xValue) {
}
return closestIndex;
};
}
/**
* Formats a duration in seconds to a compact string with d/h/m/s units.
*
@@ -226,13 +227,13 @@ export function getXIndex(data, xValue) {
* or null if the input is NaN.
*/
export function formatTime(seconds) {
if(!isNaN(seconds)) {
if (!isNaN(seconds)) {
const remainingSeconds = seconds % 60;
const minutes = (Math.floor(seconds - remainingSeconds) / 60) % 60;
const hours = (Math.floor(seconds / 3600)) % 24;
const hours = Math.floor(seconds / 3600) % 24;
const days = Math.floor(seconds / (3600 * 24));
let result = '';
let result = "";
if (days > 0) {
result += `${days}d`;
}
@@ -258,20 +259,20 @@ export function formatTime(seconds) {
export function formatMaxTwo(times) {
const formattedTimes = [];
for (let time of times) {
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = '';
let count = 0;
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = "";
let count = 0;
// Keep only the two largest units
for (let unit of units) {
if (count >= 2) {
break;
}
formattedTime += unit + ' ';
count++;
// Keep only the two largest units
for (let unit of units) {
if (count >= 2) {
break;
}
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
formattedTime += unit + " ";
count++;
}
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
}
return formattedTimes;
}

View File

@@ -25,5 +25,5 @@ export default function shortScaleNumber(number) {
index++;
}
num = Math.ceil(num * 10) / 10;
return num + abbreviations[index] + " " ;
return num + abbreviations[index] + " ";
}

View File

@@ -25,7 +25,7 @@ export function sortNumEngZhtw(data) {
if (isANumber) return -1;
if (isBNumber) return 1;
return a.localeCompare(b, 'zh-Hant-TW', { sensitivity: 'accent' });
return a.localeCompare(b, "zh-Hant-TW", { sensitivity: "accent" });
});
}
@@ -48,5 +48,5 @@ export function sortNumEngZhtwForFilter(a, b) {
if (isANumber) return -1;
if (isBNumber) return 1;
return a.localeCompare(b, 'zh-Hant-TW', { sensitivity: 'accent' });
return a.localeCompare(b, "zh-Hant-TW", { sensitivity: "accent" });
}

View File

@@ -6,7 +6,7 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module timeLabel Time formatting and chart axis tick utilities. */
import moment from 'moment';
import moment from "moment";
/** @constant {number} Number of decimal places for formatted time values. */
const TOFIXED_DECIMAL = 1;
@@ -20,12 +20,12 @@ const TOFIXED_DECIMAL = 1;
* and the time unit character ("d", "h", "m", or "s").
*/
export const getStepSizeOfYTicks = (maxTimeInSecond, numOfParts) => {
const {unitToUse, timeValue} = getTimeUnitAndValueToUse(maxTimeInSecond);
const getLarger = 1 + (1 / (numOfParts - 1));
const resultStepSize = (timeValue * getLarger / numOfParts);
const { unitToUse, timeValue } = getTimeUnitAndValueToUse(maxTimeInSecond);
const getLarger = 1 + 1 / (numOfParts - 1);
const resultStepSize = (timeValue * getLarger) / numOfParts;
return {resultStepSize, unitToUse};
}
return { resultStepSize, unitToUse };
};
/**
* Determines the most appropriate time unit for a given number of seconds
@@ -62,7 +62,7 @@ const getTimeUnitAndValueToUse = (secondToDecide) => {
return {
unitToUse: "s",
timeValue: secondToDecide,
}
};
}
};
@@ -75,12 +75,14 @@ const getTimeUnitAndValueToUse = (secondToDecide) => {
* @param {string} unitToUse - The time unit suffix ("d", "h", "m", or "s").
* @returns {string} The formatted tick label (e.g. "2.5h").
*/
export function getYTicksByIndex(stepSize, index, unitToUse){
export function getYTicksByIndex(stepSize, index, unitToUse) {
const rawStepsizeMultIndex = (stepSize * index).toString();
const shortenStepsizeMultIndex = rawStepsizeMultIndex.substring(
0, rawStepsizeMultIndex.indexOf('.') + 1 + TOFIXED_DECIMAL);
0,
rawStepsizeMultIndex.indexOf(".") + 1 + TOFIXED_DECIMAL,
);
return `${shortenStepsizeMultIndex}${unitToUse}`;
};
}
/**
* Converts seconds to a human-readable time string with full unit names.
@@ -101,17 +103,15 @@ export function getTimeLabel(second, fixedNumber = 0) {
const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes);
if(dd > 0){
return (second / day).toFixed(fixedNumber) + " days";
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + " days";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + " hrs";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + " mins";
}
else if(hh > 0){
return ((second % day) / hour).toFixed(fixedNumber) + " hrs";
}
else if(mm > 0){
return ((second % hour) / minutes).toFixed(fixedNumber) + " mins";
}
if(second == 0){
return second + " sec";
if (second == 0) {
return second + " sec";
}
return second + " sec";
}
@@ -134,17 +134,15 @@ export function simpleTimeLabel(second, fixedNumber = 0) {
const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes);
if(dd > 0){
return (second / day).toFixed(fixedNumber) + "d";
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + "d";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + "h";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + "m";
}
else if(hh > 0){
return ((second % day) / hour).toFixed(fixedNumber) + "h";
}
else if(mm > 0){
return ((second % hour) / minutes).toFixed(fixedNumber) + "m";
}
if(second == 0){
return second + "s";
if (second == 0) {
return second + "s";
}
return second + "s";
}
@@ -167,49 +165,48 @@ export function followTimeLabel(second, max, fixedNumber = 0) {
const dd = max / day;
const hh = max / hour;
const mm = max/ minutes;
let maxUnit = '';
const mm = max / minutes;
let maxUnit = "";
let result = "";
if (dd > 1) {
maxUnit = 'd';
maxUnit = "d";
} else if (hh > 1) {
maxUnit = 'h';
maxUnit = "h";
} else if (mm > 1) {
maxUnit = 'm';
maxUnit = "m";
} else {
maxUnit = 's';
maxUnit = "s";
}
switch (maxUnit) {
case 'd':
if((second / day) === 0) {
case "d":
if (second / day === 0) {
fixedNumber = 0;
}
result = (second / day).toFixed(fixedNumber) + 'd';
result = (second / day).toFixed(fixedNumber) + "d";
break;
case 'h':
if((second / hour) === 0) {
case "h":
if (second / hour === 0) {
fixedNumber = 0;
}
result = (second / hour).toFixed(fixedNumber) + 'h';
result = (second / hour).toFixed(fixedNumber) + "h";
break;
case 'm':
if((second / minutes) === 0) {
case "m":
if (second / minutes === 0) {
fixedNumber = 0;
}
result = (second / minutes).toFixed(fixedNumber) + 'm';
result = (second / minutes).toFixed(fixedNumber) + "m";
break;
case 's':
if(second === 0) {
case "s":
if (second === 0) {
fixedNumber = 0;
}
result = second.toFixed(fixedNumber) + 's';
result = second.toFixed(fixedNumber) + "s";
break;
}
return result;
}
/**
* Selects an appropriate moment.js date format string based on the
* difference between the minimum and maximum timestamps.
@@ -221,20 +218,25 @@ export function followTimeLabel(second, max, fixedNumber = 0) {
* @param {number} maxTimeStamp - The maximum timestamp in seconds.
* @returns {string} A moment.js format string.
*/
export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeStamp) => {
export const setTimeStringFormatBaseOnTimeDifference = (
minTimeStamp,
maxTimeStamp,
) => {
const timeDifferenceInSeconds = maxTimeStamp - minTimeStamp;
let dateFormat;
if (timeDifferenceInSeconds < 60) {
dateFormat = 'HH:mm:ss'; // Seconds range
dateFormat = "HH:mm:ss"; // Seconds range
} else if (timeDifferenceInSeconds < 3600) {
dateFormat = 'MM/DD HH:mm'; // Minutes range
} else if (timeDifferenceInSeconds < 86400) { // 86400 seconds = 24 hours
dateFormat = 'MM/DD HH:mm'; // Hours range
} else if (timeDifferenceInSeconds < 2592000) { // 2592000 seconds = 30 days
dateFormat = 'YYYY/MM/DD'; // Days range
dateFormat = "MM/DD HH:mm"; // Minutes range
} else if (timeDifferenceInSeconds < 86400) {
// 86400 seconds = 24 hours
dateFormat = "MM/DD HH:mm"; // Hours range
} else if (timeDifferenceInSeconds < 2592000) {
// 2592000 seconds = 30 days
dateFormat = "YYYY/MM/DD"; // Days range
} else {
dateFormat = 'YYYY/MM/DD'; // Months range
dateFormat = "YYYY/MM/DD"; // Months range
}
return dateFormat;
@@ -251,6 +253,6 @@ export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeSta
*/
export const mapTimestampToAxisTicksByFormat = (timeStampArr, timeFormat) => {
if (timeStampArr) {
return timeStampArr.map(ts => moment(ts).format(timeFormat));
}
};
return timeStampArr.map((ts) => moment(ts).format(timeFormat));
}
};

View File

@@ -11,7 +11,7 @@
* @returns {HTMLDivElement} A div element with text-only content.
*/
export function createTooltipContent(label) {
const content = document.createElement('div');
content.textContent = String(label ?? '');
const content = document.createElement("div");
content.textContent = String(label ?? "");
return content;
}

View File

@@ -9,29 +9,29 @@
* navigation guards, and authentication redirect logic.
*/
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/Files.vue';
import Upload from '@/views/Upload/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/Compare.vue';
import MapCompare from '@/views/Compare/MapCompare.vue';
import AccountAdmin from '@/views/AccountManagement/AccountAdmin/AccountAdmin.vue';
import MyAccount from '@/views/AccountManagement/MyAccount.vue';
import MemberArea from '@/views/MemberArea/index.vue';
import NotFound404 from '@/views/NotFound404.vue';
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/Files.vue";
import Upload from "@/views/Upload/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/Compare.vue";
import MapCompare from "@/views/Compare/MapCompare.vue";
import AccountAdmin from "@/views/AccountManagement/AccountAdmin/AccountAdmin.vue";
import MyAccount from "@/views/AccountManagement/MyAccount.vue";
import MemberArea from "@/views/MemberArea/index.vue";
import NotFound404 from "@/views/NotFound404.vue";
const routes = [
{
path: '/', // Default entry route
redirect: '/files', // Redirect to /files
},
path: "/", // Default entry route
redirect: "/files", // Redirect to /files
},
{
path: '/',
path: "/",
name: "AuthContainer",
component: AuthContainer,
children: [
@@ -40,7 +40,7 @@ const routes = [
name: "Login",
component: Login,
},
]
],
},
{
path: "/",
@@ -48,7 +48,7 @@ const routes = [
component: MainContainer,
meta: {
title: "MainContainer",
requiresAuth: true
requiresAuth: true,
},
children: [
{
@@ -70,16 +70,17 @@ const routes = [
name: "AcctAdmin",
component: AccountAdmin,
},
]
},{
path: "/my-account",
],
},
{
path: "/my-account",
name: "My Account",
component: MyAccount,
},
{
path: "/upload", // router.push({ replace: true }) does not add the path to history
name: "Upload",
component: Upload
component: Upload,
},
{
path: "/discover",
@@ -114,7 +115,7 @@ const routes = [
component: Map,
meta: {
file: {}, // parent log or parent filter
}
},
},
{
// type: log | filter, the parameter can be either.
@@ -124,7 +125,7 @@ const routes = [
component: Conformance,
meta: {
file: {}, // parent log or parent filter
}
},
},
{
// type: log | filter, the parameter can be either.
@@ -134,9 +135,9 @@ const routes = [
component: Performance,
meta: {
file: {}, // parent log or parent filter
}
},
},
]
],
},
{
path: "/compare",
@@ -146,14 +147,15 @@ const routes = [
path: "/compare/dashboard/:primaryType/:primaryId/:secondaryType/:secondaryId",
name: "CompareDashboard",
component: CompareDashboard,
}, {
},
{
path: "/compare/map/:primaryType/:primaryId/:secondaryType/:secondaryId",
name: "MapCompare",
component: MapCompare,
}
]
},
],
},
]
],
},
{
path: "/:pathMatch(.*)*",
@@ -166,7 +168,7 @@ const base_url = import.meta.env.BASE_URL;
const router = createRouter({
history: createWebHistory(base_url), //(/)
// history: createWebHashHistory(base_url), // (/#)
routes
routes,
});
// Global navigation guard
@@ -175,9 +177,11 @@ router.beforeEach((to, from) => {
// from: Route: the current route being navigated away from
// When navigating to the login page, redirect to Files if already logged in
if (to.name === 'Login') {
const isLoggedIn = document.cookie.split(';').some(c => c.trim().startsWith('isLuciaLoggedIn='));
if (isLoggedIn) return { name: 'Files' };
if (to.name === "Login") {
const isLoggedIn = document.cookie
.split(";")
.some((c) => c.trim().startsWith("isLuciaLoggedIn="));
if (isLoggedIn) return { name: "Files" };
}
});

Some files were not shown because too many files have changed in this diff Show More