init frontend project
Change-Id: I4abb215506f0c24de5e99eae7badeeb56b662492
This commit is contained in:
parent
78ce3a87f7
commit
a4e7c8f971
29
cfsb-frontend/README.md
Normal file
29
cfsb-frontend/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# fogbrokerfront
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
19
cfsb-frontend/index.html
Normal file
19
cfsb-frontend/index.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="height: 100%">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NebulOuS Cloud Fog Service Broker</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body style="min-height: calc(100% - 8px * 2);">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
8
cfsb-frontend/jsconfig.json
Normal file
8
cfsb-frontend/jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
1456
cfsb-frontend/package-lock.json
generated
Normal file
1456
cfsb-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
cfsb-frontend/package.json
Normal file
20
cfsb-frontend/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "fogbrokerfront",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.1",
|
||||
"vue-router": "^4.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.1",
|
||||
"vite": "^5.0.10",
|
||||
"vue": "^3.3.11"
|
||||
}
|
||||
}
|
BIN
cfsb-frontend/public/favicon.ico
Normal file
BIN
cfsb-frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
cfsb-frontend/public/images/Broker.png
Normal file
BIN
cfsb-frontend/public/images/Broker.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 KiB |
BIN
cfsb-frontend/public/images/nebulus_logo.png
Normal file
BIN
cfsb-frontend/public/images/nebulus_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
110
cfsb-frontend/src/App.vue
Normal file
110
cfsb-frontend/src/App.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- Header section (if any) -->
|
||||
<header>
|
||||
<!-- Navigation, branding, etc. -->
|
||||
</header>
|
||||
|
||||
<nav class="navbar navbar-expand-lg bg-color-primary" data-bs-theme="dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">NebulOuS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<router-link to="/" class="nav-link text-white">Home</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/criteria-selection" class="nav-link text-white">Criteria Selection</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<router-view></router-view>
|
||||
|
||||
<!-- Main content where routed components will be displayed -->
|
||||
<!-- <router-view></router-view>
|
||||
<button v-if="showCriteriaSelectionButton" @click="goToCriteriaSelection">Go to Criteria Selection</button> -->
|
||||
<footer class="footer text-center p-2">
|
||||
<span class="text-white">© NebulOus</span>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--main-color: #7030A0;
|
||||
--secondary-color: #e0cffc;
|
||||
--color-indigo-700: #3d0a91;
|
||||
--light-gray-color: #f8f9fa;
|
||||
--medium-gray-color: #6c757d;
|
||||
}
|
||||
|
||||
.color-primary {
|
||||
color: var(--main-color);
|
||||
}
|
||||
.bg-color-primary {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background-color: var(--main-color); /* Blue color */
|
||||
color: #fff; /* White text color */
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background-color: var(--secondary-color); /* Lighter shade of purple on hover */
|
||||
color: var(--main-color);
|
||||
border: 2px;
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.border-radius-sm {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.border-radius-md {
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.spacer-sm {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.spacer-sm {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
methods: {
|
||||
goToCriteriaSelection() {
|
||||
this.$router.push('/criteria-selection');
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showCriteriaSelectionButton() {
|
||||
return this.$route.path === '/'; /* other conditions */
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
86
cfsb-frontend/src/assets/base.css
Normal file
86
cfsb-frontend/src/assets/base.css
Normal file
@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
1
cfsb-frontend/src/assets/logo.svg
Normal file
1
cfsb-frontend/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
35
cfsb-frontend/src/assets/main.css
Normal file
35
cfsb-frontend/src/assets/main.css
Normal file
@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
/*display: grid;*/
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
64
cfsb-frontend/src/components/CriteriaSelection.vue
Normal file
64
cfsb-frontend/src/components/CriteriaSelection.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="row" style="padding-bottom: 2rem">
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col col-12 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Selection of Criteria</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<HierarchicalCategoryList
|
||||
:items="hierarchicalCategoryList"
|
||||
@selected-items="updateSelectedItems"
|
||||
></HierarchicalCategoryList>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div>Selected Items Length: {{ selectedItems.length }}</div>
|
||||
<button v-if="selectedItems.length > 0" @click="navigateToDataGrid">Go to DataGrid</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HierarchicalCategoryList from "@/components/HierarchicalCategoryList.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HierarchicalCategoryList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hierarchicalCategoryList: [],
|
||||
selectedItems: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log('CriteriaSelection.vue mounted');
|
||||
this.fetchHierarchicalCategoryList();
|
||||
},
|
||||
methods: {
|
||||
async fetchHierarchicalCategoryList() {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/get_hierarchical_category_list');
|
||||
const data = await response.json();
|
||||
this.hierarchicalCategoryList = data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching hierarchical category list:', error);
|
||||
}
|
||||
},
|
||||
navigateToDataGrid() {
|
||||
console.log('Navigating to DataGrid');
|
||||
this.$router.push({ name: 'DataGrid' });
|
||||
},
|
||||
updateSelectedItems(newSelectedItems) {
|
||||
//console.log('Updating selected items in CriteriaSelection.vue:', newSelectedItems);
|
||||
this.selectedItems = newSelectedItems;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
209
cfsb-frontend/src/components/DataGrid.vue
Normal file
209
cfsb-frontend/src/components/DataGrid.vue
Normal file
@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="p-4">
|
||||
<h2>Edge / Fog Nodes Data</h2>
|
||||
</div>
|
||||
|
||||
<table v-if="Object.keys(gridData).length" class="grid-cell-class">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Edge / Fog Nodes</th>
|
||||
<th v-for="(values, column) in gridData" :key="column">{{ values.title }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="index in rowCount" :key="index">
|
||||
<!-- (-1) Because there is a different indexing in the gridData and the fog node titles that starts from 0 -->
|
||||
<td>{{ fogNodesTitles[index-1] }}</td>
|
||||
<td v-for="(values, column) in gridData" :key="`${column}-${index}`">
|
||||
<select v-if="Ordinal_Variables.includes(column)" v-model="values.data_values[index - 1]">
|
||||
<option v-for="option in dropdownOptions" :value="option" :key="option">{{ option }}</option>
|
||||
</select>
|
||||
<input v-else type="text" v-model="values.data_values[index - 1]" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else>
|
||||
No data to display.
|
||||
</div>
|
||||
<div class="pt-4"></div>
|
||||
<!-- <button @click="SaveDataforEvaluation" class="bg-color-primary">Save and Run Evaluation</button> -->
|
||||
<button @click="SaveDataforWR" class="bg-color-primary">Save and Add Weight Restrictions</button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fogNodesTitles: [],
|
||||
gridData: [], // Data for the grid
|
||||
selectedItemsFromBack: [],
|
||||
Ordinal_Variables: ['attr-reputation', 'attr-assurance', 'attr-security'],
|
||||
dropdownOptions: ['High', 'Medium', 'Low'], // Options for the dropdown
|
||||
};
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
return {
|
||||
router
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const selectedItems = this.$route.params.selectedItems || [];
|
||||
if (selectedItems.length > 0) {
|
||||
this.fetchGridData(selectedItems);
|
||||
}
|
||||
this.fetchFogNodesTitles();
|
||||
},
|
||||
computed: {
|
||||
rowCount() {
|
||||
// Check if gridData has any keys and use the first key to find the row count
|
||||
const firstKey = Object.keys(this.gridData)[0];
|
||||
return firstKey ? this.gridData[firstKey].data_values.length : 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Receives the Grid Data 1st time
|
||||
async fetchGridData(selectedItems) {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/process_selected_items', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({selectedItems}),
|
||||
});
|
||||
if (response.ok) {
|
||||
const criteria_data = await response.json();
|
||||
this.gridData = criteria_data.gridData; // Assigning the gridData from the response
|
||||
console.log('DataGrid.vue received the criteria data from the Backend:', this.gridData); // Log the received grid data
|
||||
} else {
|
||||
throw new Error('Failed to fetch grid data');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching grid data:', error);
|
||||
}
|
||||
},
|
||||
fetchFogNodesTitles() { // Receives the names of fog nodes (grid's 1st column)
|
||||
fetch('http://127.0.0.1:5000/get-fog-nodes-titles')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 'data' is an array like ['Fog Node 1', 'Fog Node 2', ...]
|
||||
this.fogNodesTitles = data;
|
||||
})
|
||||
.catch(error => console.error('Error fetching fog nodes titles:', error));
|
||||
},
|
||||
validateGridData() {
|
||||
for (const key in this.gridData) {
|
||||
if (this.gridData.hasOwnProperty(key)) {
|
||||
const dataValues = this.gridData[key].data_values;
|
||||
for (const value of dataValues) {
|
||||
if (value === 0 || value === null || value === '') {
|
||||
return false; // Invalid data found
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true; // All data is valid
|
||||
},
|
||||
async SaveDataforWR() {
|
||||
if (!this.validateGridData()) {
|
||||
alert('Invalid input: Zero or null values are not accepted.');
|
||||
return; // Stop submission if validation fails
|
||||
}
|
||||
else{
|
||||
try {
|
||||
const DataforWR = JSON.stringify({
|
||||
gridData: this.gridData
|
||||
});
|
||||
// Navigate to WR component with data
|
||||
this.router.push({ name: 'WR', params: { data: DataforWR } });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
async SaveDataforEvaluation() {
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Basic table styling */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
th {
|
||||
background-color: #813F8F; /* Primary color */
|
||||
color: #FFFFFF; /* White text */
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Row styling */
|
||||
td {
|
||||
background-color: #E7E7E7; /* Light grey */
|
||||
color: #374591; /* Secondary color */
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Alternate row colors for better readability */
|
||||
tr:nth-child(even) {
|
||||
background-color: #E4DCD5; /* Light tan */
|
||||
}
|
||||
|
||||
/* Hover effect on rows */
|
||||
tr:hover {
|
||||
background-color: #6FBFFF; /* Light blue */
|
||||
}
|
||||
|
||||
/* Additional styles for editable input fields in the table */
|
||||
table input[type="text"] {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table input[type="text"]:focus {
|
||||
outline: none;
|
||||
background-color: #fff; /* Change color on focus for visibility */
|
||||
}
|
||||
|
||||
/* Style for the submit button */
|
||||
button {
|
||||
background-color: var(--main-color); /* Blue color */
|
||||
color: #fff; /* White text color */
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--secondary-color); /* Lighter shade of purple on hover */
|
||||
color: var(--main-color);
|
||||
border:2px;
|
||||
border-color:var(--main-color);
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.grid-cell-class {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
25
cfsb-frontend/src/components/Evaluation.vue
Normal file
25
cfsb-frontend/src/components/Evaluation.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<!-- HTML content of your component -->
|
||||
<div>
|
||||
<!-- Your component's template goes here -->
|
||||
<h1>Evaluation Component</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
// JavaScript for your component (data, methods, etc.)
|
||||
data() {
|
||||
return {
|
||||
// Your data properties
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// Your methods
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* CSS for your component */
|
||||
</style>
|
220
cfsb-frontend/src/components/HierarchicalCategoryList.vue
Normal file
220
cfsb-frontend/src/components/HierarchicalCategoryList.vue
Normal file
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div>
|
||||
<form v-if="!isChild" @submit.prevent="submitSelection">
|
||||
<ul class="list-group">
|
||||
<li v-for="item in items" :key="item.name" class="list-group-item criteria-card">
|
||||
<span v-if="item.children.length > 0" @click="toggleCategory(item)" class="float-end" v-bind:title="'Expand ' + item.title"><i class="bi bi-arrow-bar-down" v-bind:aria-label="'Expand ' + item.title"></i></span>
|
||||
<label>
|
||||
<!--
|
||||
<input v-model="item.checked" type="checkbox" @change="handleCheckboxChange(item)" />
|
||||
<span @click="toggleCategory(item)" v-bind:title="item.description">{{ item.title }}</span> -->
|
||||
<input type="checkbox" :checked="item.checked" @change="() => handleCheckboxChange(item)" />
|
||||
<span @click="toggleCategory(item)">{{ item.title }}</span>
|
||||
</label>
|
||||
<ul v-show="item.showChildren" class="list-group">
|
||||
<!-- Recursive call without Submit button -->
|
||||
<HierarchicalCategoryList :isChild="true" :items="item.children" @selected-items="updateSelectedItems" />
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Submit button outside the recursive structure -->
|
||||
<button type="submit" class="bg-color-primary">Submit</button>
|
||||
</form>
|
||||
<div v-else>
|
||||
<li v-for="item in items" :key="item.name" class="list-group-item criteria-card">
|
||||
<span v-if="item.children.length > 0" @click="toggleCategory(item)" class="float-end" v-bind:title="'Expand ' + item.title"><i class="bi bi-arrow-bar-down" v-bind:aria-label="'Expand ' + item.title"></i></span>
|
||||
<label>
|
||||
<!-- <input v-model="item.checked" type="checkbox" @change="handleCheckboxChange(item)" />
|
||||
<span @click="toggleCategory(item)" v-bind:title="item.description">{{ item.title }}</span> -->
|
||||
<input type="checkbox" :checked="item.checked" @change="() => handleCheckboxChange(item)" />
|
||||
<span @click="toggleCategory(item)">{{ item.title }}</span>
|
||||
</label>
|
||||
<ul v-show="item.showChildren">
|
||||
<!-- Recursive call without Submit button -->
|
||||
<HierarchicalCategoryList :isChild="true" :items="item.children" @selected-items="updateSelectedItems" />
|
||||
</ul>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: Array,
|
||||
isChild: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localSelectedItems: [],
|
||||
selectedItemsFromBack: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
computedSelectedItems() {
|
||||
return this.localSelectedItems.slice();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCategory(item) {
|
||||
item.showChildren = !item.showChildren;
|
||||
},
|
||||
handleCheckboxChange(changedItem) {
|
||||
// Toggle the checked state of the current item
|
||||
changedItem.checked = !changedItem.checked;
|
||||
|
||||
// If the current item is a child and is being checked, uncheck its parent
|
||||
if (changedItem.parentId && changedItem.checked) {
|
||||
this.uncheckParent(changedItem.parentId);
|
||||
}
|
||||
|
||||
// Update the selected items list
|
||||
this.updateSelectedItems();
|
||||
},
|
||||
uncheckParent(parentId) {
|
||||
const parentItem = this.findItemById(parentId, this.items);
|
||||
if (parentItem) {
|
||||
parentItem.checked = false;
|
||||
}
|
||||
},
|
||||
findItemById(id, items) {
|
||||
// Recursive function to find an item by id
|
||||
for (const item of items) {
|
||||
if (item.id === id) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const foundItem = this.findItemById(id, item.children);
|
||||
if (foundItem) {
|
||||
return foundItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
updateSelectedItems() {
|
||||
// Update the selected items list
|
||||
this.localSelectedItems = this.collectSelectedItems(this.items);
|
||||
// Emit the updated list
|
||||
this.$emit('selected-items', this.localSelectedItems);
|
||||
},
|
||||
collectSelectedItems(items) {
|
||||
let selectedItems = [];
|
||||
for (const item of items) {
|
||||
if (item.checked) {
|
||||
selectedItems.push(item.name);
|
||||
}
|
||||
if (item.children) {
|
||||
selectedItems = selectedItems.concat(this.collectSelectedItems(item.children));
|
||||
}
|
||||
}
|
||||
return selectedItems;
|
||||
},
|
||||
submitSelection() {
|
||||
const selectedItems = this.collectSelectedItems(this.items);
|
||||
|
||||
if (selectedItems.length < 2) {
|
||||
alert('Please select at least two items before submitting.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Emitting the selected items - useful if there's a parent component listening to this event
|
||||
this.$emit('selected-items', selectedItems);
|
||||
|
||||
// Programmatic navigation to the DataGrid page, passing the selected items as route parameters
|
||||
this.$router.push({ name: 'DataGrid', params: { selectedItems: selectedItems } });
|
||||
},
|
||||
async postSelectedItems(selectedItems) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({selectedItems}),
|
||||
};
|
||||
const response = await fetch('http://127.0.0.1:5000/process_selected_items', requestOptions);
|
||||
const data = await response.json();
|
||||
//console.log('Send Selected items to back', data);
|
||||
this.selectedItemsFromBack = data;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add your styling here if needed */
|
||||
.custom-container {
|
||||
background-color: #f0f0f0; /* Light grey background */
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Style for checkbox label */
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Style for checkbox */
|
||||
input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Style for category name */
|
||||
span {
|
||||
cursor: pointer;
|
||||
//color: #3498db; /* Blue color */
|
||||
color: var(--main-color);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
//color: #217dbb; /* Darker shade of blue on hover */
|
||||
color: var(--color-indigo-700);
|
||||
}
|
||||
|
||||
/* Style for the category item */
|
||||
.category-item {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.category-item:hover {
|
||||
background-color: #e0e0e0; /* Light grey background on hover */
|
||||
}
|
||||
|
||||
/* Style for the submit button */
|
||||
button {
|
||||
background-color: var(--main-color); /* Blue color */
|
||||
color: #fff; /* White text color */
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--secondary-color); /* Lighter shade of purple on hover */
|
||||
color: var(--main-color);
|
||||
border: 2px;
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.criteria-card {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
margin: 0.5rem;
|
||||
box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);
|
||||
}
|
||||
|
||||
.criteria-card:hover {
|
||||
background-color: var(--light-gray-color);
|
||||
color: var(--main-color);
|
||||
}
|
||||
</style>
|
62
cfsb-frontend/src/components/HomePage.vue
Normal file
62
cfsb-frontend/src/components/HomePage.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "HomePage"
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="container">
|
||||
<div class="row p-4 text-center">
|
||||
<div class="col col-12">
|
||||
<h1 class="display-2">Welcome to <span style="color: var(--main-color);">NebulOus</span></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col col-12 col-md-6 col-lg-6">
|
||||
<p class="lead">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p>
|
||||
</div>
|
||||
<div class="col col-12 col-md-6 col-lg-6">
|
||||
<img src="https://img.freepik.com/free-vector/cloud-network-system-background-vector-social-media-banner_53876-111851.jpg?w=1380&t=st=1704656229~exp=1704656829~hmac=74f300e7d16d8e290cf82299d18aeaf12ec4705f2da8aa41504a519aa6afdb00" class="img-fluid border-radius-md" alt="Nebulous">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col col-12 col-md-6 col-lg-6">
|
||||
<img src="https://img.freepik.com/free-vector/cloud-services-isometric-composition-with-big-cloud-computing-infrastructure-elements-connected-with-dashed-lines-vector-illustration_1284-30495.jpg?w=900&t=st=1704654924~exp=1704655524~hmac=46d02fbd782b895b4f119eb0cf21e0b02d1b4bf736fc8d66b357b7ca10a7908f" class="img-fluid border-radius-md" alt="Nebulous">
|
||||
</div>
|
||||
<div class="col col-12 col-md-6 col-lg-6">
|
||||
<p class="lead">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer-sm"></div>
|
||||
|
||||
<div class="row text-center p-4 bg-row border-radius-sm">
|
||||
<div class="col col-12 col-md-10 offset-1">
|
||||
<h3 class="display-6">Try now</h3>
|
||||
<router-link to="/criteria-selection" class="btn button-primary btn-lg">Criteria Selection</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer-sm"></div>
|
||||
|
||||
<div class="row p-4 text-center">
|
||||
<div class="col col-12">
|
||||
<h2 class="display-4">How does it work</h2>
|
||||
</div>
|
||||
<div class="col col-12">
|
||||
<img src="/images/Broker.png" class="img-fluid border-radius-md" alt="...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.bg-row {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
</style>
|
48
cfsb-frontend/src/components/ParentComponent.vue
Normal file
48
cfsb-frontend/src/components/ParentComponent.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Vue Hierarchical Category List Example</h2>
|
||||
<HierarchicalCategoryList
|
||||
:items="hierarchicalCategoryList"
|
||||
@selected-items="updateSelectedItems"
|
||||
></HierarchicalCategoryList>
|
||||
|
||||
<h2>Selected Items</h2>
|
||||
<ul>
|
||||
<li v-for="item in selectedItems" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
|
||||
<DataGrid :items="selectedItemsFromBackend"></DataGrid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HierarchicalCategoryList from "@/components/HierarchicalCategoryList";
|
||||
import DataGrid from "@/components/DataGrid.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HierarchicalCategoryList,
|
||||
DataGrid,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hierarchicalCategoryList: [], // Initialize with your data
|
||||
selectedItems: [],
|
||||
selectedItemsFromBackend: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateSelectedItems(newSelectedItems) {
|
||||
console.log('Updating selected items in ParentComponent:', newSelectedItems);
|
||||
this.selectedItems = newSelectedItems;
|
||||
},
|
||||
|
||||
updateSelectedItemsFromBackend(newSelectedItems) {
|
||||
console.log('Updating selected items from backend in ParentComponent:', newSelectedItems);
|
||||
this.selectedItemsFromBackend = newSelectedItems;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
28
cfsb-frontend/src/components/RecursiveList.vue
Normal file
28
cfsb-frontend/src/components/RecursiveList.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<!-- RecursiveList.vue -->
|
||||
<template>
|
||||
<ul>
|
||||
<li v-for="item in items" :key="item.name">
|
||||
{{ item.name }}
|
||||
<recursive-list v-if="item.children" :items="item.children" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
RecursiveList: () => import('./RecursiveList.vue'), // Lazy load to handle recursion
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add your styles here if needed */
|
||||
</style>
|
207
cfsb-frontend/src/components/Results.vue
Normal file
207
cfsb-frontend/src/components/Results.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="results-container">
|
||||
<h2>Evaluation Results</h2>
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<div v-else>
|
||||
<!-- Table for displaying the results -->
|
||||
<table v-if="results.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fog Node</th>
|
||||
<th>Score (%)</th>
|
||||
<th>Ranking</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(result, index) in results" :key="index">
|
||||
<td>{{ result.Title }}</td>
|
||||
<td>{{ formatPercentage(result['DEA Score']) }}</td>
|
||||
<td>{{ result.Rank }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Chart Container -->
|
||||
<div class="charts-container">
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="deascoresChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="ranksChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button @click="goBackToWR">Add/Modify Weight Restrictions</button>
|
||||
<button @click="saveProjectResults">Save Project</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
results: [],
|
||||
loading: true,
|
||||
deaScoresChart: null,
|
||||
ranksChart: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchResults();
|
||||
},
|
||||
methods: {
|
||||
goBackToWR() {
|
||||
// Make sure 'WR' matches the name of the route in your router configuration
|
||||
this.$router.push({ name: 'WR' });
|
||||
},
|
||||
saveProjectResults() {
|
||||
// For now, this method is a placeholder
|
||||
console.log('Save Project Results button clicked');
|
||||
},
|
||||
fetchResults() {
|
||||
fetch('http://127.0.0.1:5000/get-evaluation-results')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.results = data;
|
||||
this.loading = false;
|
||||
this.createCharts();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching results:', error);
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
createCharts() {
|
||||
const titles = this.results.map(result => result.Title);
|
||||
const deaScores = this.results.map(result => result['DEA Score']);
|
||||
const ranks = this.results.map(result => result.Rank);
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.createBarChart(titles, deaScores, 'deascoresChart', 'Fog Node Scores');
|
||||
this.createHorizontalBarChart(titles, ranks, 'ranksChart', 'Fog Node Ranking');
|
||||
});
|
||||
},
|
||||
createBarChart(labels, data, chartId, label) {
|
||||
const ctx = document.getElementById(chartId).getContext('2d');
|
||||
if (this.deaScoresChart) {
|
||||
this.deaScoresChart.destroy();
|
||||
}
|
||||
this.deaScoresChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label,
|
||||
data,
|
||||
backgroundColor: 'rgba(181,141,243,0.56)',
|
||||
borderColor: 'rgb(102,16,242)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
//maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 1, // Set the maximum value of the y-axis to 1
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem) {
|
||||
let score = tooltipItem.raw * 100;
|
||||
return `Score: ${score.toFixed(2)}%`;
|
||||
}
|
||||
},
|
||||
bodyFontSize: 16, // Adjust font size as needed
|
||||
titleFontSize: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
createHorizontalBarChart(labels, data, chartId, label) {
|
||||
const ctx = document.getElementById(chartId).getContext('2d');
|
||||
if (this.ranksChart) {
|
||||
this.ranksChart.destroy();
|
||||
}
|
||||
// Assuming higher scores should have longer bars, so we invert the scores
|
||||
// as higher rank should have lower numerical value
|
||||
const invertedData = data.map(score => Math.max(...data) - score + Math.min(...data));
|
||||
this.ranksChart = new Chart(ctx, {
|
||||
type: 'bar', // In Chart.js 3.x, you specify horizontal bars using indexAxis
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label,
|
||||
data: invertedData,
|
||||
backgroundColor: 'rgba(110,108,229,0.55)',
|
||||
borderColor: 'rgb(60,54,235)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
//maintainAspectRatio: false, // Set to false to allow full width and controlled height
|
||||
indexAxis: 'y', // This will make the bar chart horizontal
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
// Since we inverted the data, we need to correct the value displayed in the tooltip
|
||||
const rankValue = Math.max(...data) - context.parsed.x + Math.min(...data);
|
||||
return `Rank: ${rankValue}`;
|
||||
}
|
||||
},
|
||||
bodyFontSize: 16, // Adjust font size as needed
|
||||
titleFontSize: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
formatPercentage(value) {
|
||||
const percentage = (value * 100).toFixed(2);
|
||||
return percentage === '100.00' ? '100%' : `${percentage}%`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.results-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
display: flex;
|
||||
flex-direction: row; /* Align charts horizontally */
|
||||
justify-content: space-around;
|
||||
padding: 0 20px; /* Add padding if needed */
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
flex: 1; /* Each chart will take equal space */
|
||||
/* Remove max-width or set it to a higher value if you want a specific limit */
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
18
cfsb-frontend/src/components/SummedData.vue
Normal file
18
cfsb-frontend/src/components/SummedData.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<!-- Component template -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// Define data properties to store the received data
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// Access route parameters or query
|
||||
const receivedData = this.$route.params.data; // or this.$route.query.data
|
||||
// Use receivedData as needed
|
||||
},
|
||||
};
|
||||
</script>
|
239
cfsb-frontend/src/components/WR.vue
Normal file
239
cfsb-frontend/src/components/WR.vue
Normal file
@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="wr-container">
|
||||
<div v-for="(condition, index) in conditions" :key="index" class="condition-row">
|
||||
<select v-model="condition.column1" @change="updateDropdowns(index)">
|
||||
<option value="" disabled>Select Criterion</option>
|
||||
<option v-for="col in availableColumns(index, 1)" :key="`1-${col}`" :value="col">{{ col }}</option>
|
||||
</select>
|
||||
|
||||
<select v-model="condition.operator">
|
||||
<option value="" disabled>Select Operator</option>
|
||||
<option v-for="op in operators" :key="op" :value="op">{{ op }}</option>
|
||||
</select>
|
||||
|
||||
<input type="number" v-model.number="condition.value" :min="0" placeholder="Value" />
|
||||
|
||||
<select v-model="condition.column2" @change="updateDropdowns(index)">
|
||||
<option value="" disabled>Select Criterion</option>
|
||||
<option v-for="col in availableColumns(index, 2)" :key="`2-${col}`" :value="col">{{ col }}</option>
|
||||
</select>
|
||||
|
||||
<button @click="removeCondition(index)">-</button>
|
||||
</div>
|
||||
|
||||
<button @click="addCondition">+</button>
|
||||
<!-- <button @click.prevent="sendWRData">Run Evaluation</button> -->
|
||||
<button @click="sendWRData">Run Evaluation</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useRouter } from 'vue-router';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
receivedGridData: null,
|
||||
conditions: [{ column1: '', operator: '', value: 0, column2: '' }],
|
||||
criteria_titles: [], // This is populated with the column titles
|
||||
operators: ['>=', '=', '<='],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.params.data) {
|
||||
// Parse the JSON string back into an object
|
||||
this.receivedGridData = JSON.parse(this.$route.params.data);
|
||||
}
|
||||
console.log('WR.vue Received gridData:', this.receivedGridData);
|
||||
this.fetchCriteriaTitles();
|
||||
},
|
||||
methods: {
|
||||
fetchCriteriaTitles() {
|
||||
fetch('http://127.0.0.1:5000/get-criteria-titles')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
this.criteria_titles = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching criteria titles:', error);
|
||||
});
|
||||
},
|
||||
addCondition() {
|
||||
this.conditions.push({column1: '', column2: '', operator: '', value: 0});
|
||||
},
|
||||
removeCondition(index) {
|
||||
this.conditions.splice(index, 1);
|
||||
},
|
||||
validateForm() {
|
||||
for (const condition of this.conditions) {
|
||||
if (!condition.operator) {
|
||||
alert('Please select an operator for each condition.');
|
||||
return false;
|
||||
}
|
||||
if (condition.value === null || condition.value === '') {
|
||||
alert('Please enter a numeric value for each.');
|
||||
return false;
|
||||
}
|
||||
if (condition.value < 0) {
|
||||
alert('Values cannot be less than zero.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const uniquePairs = new Set(
|
||||
this.conditions.map(c => [c.column1, c.column2].sort().join('-'))
|
||||
);
|
||||
|
||||
if (uniquePairs.size !== this.conditions.length) {
|
||||
alert('Each pair of criteria can only be used once in a restriction!');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
updateDropdowns(index) {
|
||||
// May be used to update dropdown availability
|
||||
},
|
||||
availableColumns(index, dropdownNumber) {
|
||||
if (dropdownNumber === 1) {
|
||||
// For the first dropdown, filter out the column selected in the second dropdown
|
||||
return this.criteria_titles.filter(col => col !== this.conditions[index].column2);
|
||||
} else {
|
||||
// For the second dropdown, filter out the column selected in the first dropdown
|
||||
return this.criteria_titles.filter(col => col !== this.conditions[index].column1);
|
||||
}
|
||||
},
|
||||
validateNonInvertedConditions() {
|
||||
let isValid = true;
|
||||
let conditionPairs = this.conditions.map(c => [c.column1, c.column2].sort().join('-'));
|
||||
|
||||
// Create a Set for unique pairs
|
||||
const uniquePairs = new Set(conditionPairs);
|
||||
|
||||
if (uniquePairs.size !== conditionPairs.length) {
|
||||
// There are duplicates
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
},
|
||||
async sendWRData() {
|
||||
// Check if any condition is set
|
||||
const isAnyConditionSet = this.conditions.some(condition => condition.column1 && condition.column2 && condition.operator);
|
||||
|
||||
// If no conditions are set, prompt the user
|
||||
if (!isAnyConditionSet) {
|
||||
const proceedWithoutWR = confirm("Would you like to proceed without imposing Weight Restrictions?");
|
||||
if (!proceedWithoutWR) {
|
||||
// User chose 'No', do nothing to stay on the current page
|
||||
return;
|
||||
}
|
||||
// User chose 'Yes', proceed with sending data
|
||||
} else {
|
||||
// Validate the form only if there are conditions set
|
||||
if (!this.validateForm() || !this.validateNonInvertedConditions()) {
|
||||
alert('Invalid Weight Restrictions, each pair of criteria can be used only once!');
|
||||
return; // Stop if validation fails
|
||||
}
|
||||
}
|
||||
|
||||
const operatorMapping = {
|
||||
'<=': -1,
|
||||
'=': 0,
|
||||
'>=': 1
|
||||
};
|
||||
|
||||
const processedWRData = this.conditions.map(condition => {
|
||||
return {
|
||||
LHSCriterion: condition.column1,
|
||||
Operator: operatorMapping[condition.operator],
|
||||
Intense: condition.value,
|
||||
RHSCriterion: condition.column2
|
||||
};
|
||||
});
|
||||
|
||||
const payload = {
|
||||
gridData: this.receivedGridData, // Data received from DataGrid.vue
|
||||
wrData: processedWRData
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/process-evaluation-data', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Response from backend:', data);
|
||||
|
||||
// Check if the response was successful
|
||||
if (response.ok && data.status === 'success') {
|
||||
// Redirect to Results.vue
|
||||
this.$router.push({ name: 'Results' });
|
||||
} /*else {
|
||||
// Handle error
|
||||
console.error('Error in response:', data.message);
|
||||
alert('Failed to process data: ' + data.message);
|
||||
} */
|
||||
} catch (error) {
|
||||
console.error('Error sending data to backend:', error);
|
||||
alert('Failed to send data to backend.');
|
||||
}
|
||||
},
|
||||
sendDataToBackend(payload) {
|
||||
console.log('Sending payload to backend:', payload);
|
||||
fetch('http://127.0.0.1:5000/process-evaluation-data', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Raw response:', response);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Response from backend:', data);
|
||||
// Handle the response from the backend
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sending data to backend:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wr-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.condition-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--main-color); /* Blue color */
|
||||
color: #fff; /* White text color */
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--secondary-color); /* Lighter shade of purple on hover */
|
||||
color: var(--main-color);
|
||||
border:2px;
|
||||
border-color:var(--main-color);
|
||||
}
|
||||
</style>
|
7
cfsb-frontend/src/components/icons/IconCommunity.vue
Normal file
7
cfsb-frontend/src/components/icons/IconCommunity.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
cfsb-frontend/src/components/icons/IconDocumentation.vue
Normal file
7
cfsb-frontend/src/components/icons/IconDocumentation.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
cfsb-frontend/src/components/icons/IconEcosystem.vue
Normal file
7
cfsb-frontend/src/components/icons/IconEcosystem.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
cfsb-frontend/src/components/icons/IconSupport.vue
Normal file
7
cfsb-frontend/src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
19
cfsb-frontend/src/components/icons/IconTooling.vue
Normal file
19
cfsb-frontend/src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
49
cfsb-frontend/src/index.js
Normal file
49
cfsb-frontend/src/index.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import DataGrid from '@/components/DataGrid.vue';
|
||||
import CriteriaSelection from '@/components/CriteriaSelection.vue'; // Import the new component
|
||||
//import SummedData from '@/components/SummedData.vue';
|
||||
import Evaluation from '@/components/Evaluation.vue'; // Import the Evaluation component
|
||||
import WR from '@/components/WR.vue';
|
||||
import Results from '@/components/Results.vue'; // Import the Results component
|
||||
import HomePage from "@/components/HomePage.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'HomePage',
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: '/criteria-selection',
|
||||
name: 'CriteriaSelection',
|
||||
component: CriteriaSelection
|
||||
},
|
||||
{
|
||||
path: '/data-grid',
|
||||
name: 'DataGrid',
|
||||
component: DataGrid
|
||||
},
|
||||
{ path: '/data-grid', component: DataGrid },
|
||||
// { path: '/wr', name: 'WR', component: () => import('@/components/WR.vue') },
|
||||
{ path: '/wr', name: 'WR', component: WR},
|
||||
{
|
||||
path: '/evaluation',
|
||||
name: 'Evaluation',
|
||||
component: Evaluation
|
||||
},
|
||||
{
|
||||
path: '/results',
|
||||
name: 'Results',
|
||||
component: Results // Route for the Results component
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
|
||||
console.log('Router setup:', routes);
|
7
cfsb-frontend/src/main.js
Normal file
7
cfsb-frontend/src/main.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './index.js';
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
24
cfsb-frontend/src/router.js
Normal file
24
cfsb-frontend/src/router.js
Normal file
@ -0,0 +1,24 @@
|
||||
// router.js
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import App from './App.vue';
|
||||
import HierarchicalCategoryList from '@/components/HierarchicalCategoryList.vue';
|
||||
import DataGrid from '@/components/DataGrid.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: HierarchicalCategoryList,
|
||||
},
|
||||
{
|
||||
path: '/datagrid',
|
||||
name: 'DataGrid',
|
||||
component: DataGrid,
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
12
cfsb-frontend/src/utils/event-bus.js
Normal file
12
cfsb-frontend/src/utils/event-bus.js
Normal file
@ -0,0 +1,12 @@
|
||||
// utils/event-bus.js
|
||||
export const EventBus = {
|
||||
events: {},
|
||||
dispatch(event, data) {
|
||||
if (!this.events[event]) return; // No subscribers
|
||||
this.events[event].forEach(callback => callback(data));
|
||||
},
|
||||
subscribe(event, callback) {
|
||||
if (!this.events[event]) this.events[event] = []; // New event
|
||||
this.events[event].push(callback);
|
||||
}
|
||||
};
|
16
cfsb-frontend/vite.config.js
Normal file
16
cfsb-frontend/vite.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user