diff --git a/cfsb-backend/.env.prod b/cfsb-backend/.env.prod index e9b7e4b..f3c9994 100644 --- a/cfsb-backend/.env.prod +++ b/cfsb-backend/.env.prod @@ -2,7 +2,7 @@ NEBULOUS_BROKER_URL=158.37.63.86 NEBULOUS_BROKER_PORT=31609 NEBULOUS_BROKER_USERNAME=admin NEBULOUS_BROKER_PASSWORD=admin -POSTGRES_DB_HOST=localhost +POSTGRES_DB_HOST=db POSTGRES_DB_NAME=fog_broker POSTGRES_DB_PORT=5432 POSTGRES_DB_USER=dbuser diff --git a/cfsb-backend/API_Functions.py b/cfsb-backend/API_Functions.py deleted file mode 100644 index e69de29..0000000 diff --git a/cfsb-backend/Evaluation.py b/cfsb-backend/Evaluation.py index 88424a3..8600faa 100644 --- a/cfsb-backend/Evaluation.py +++ b/cfsb-backend/Evaluation.py @@ -6,7 +6,7 @@ from scipy.stats import rankdata def perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_names, node_ids): print("Evaluation begun with perform_evaluation():") # print("Data Table:", data_table) - # Identify the boolean criteria columns by checking if all values are either 0 or 1 + # Identify the boolean criteria columns by checking if all values are either 0 or 1 # boolean_criteria = [criterion for criterion in data_table if set(data_table[criterion]) <= {0, 1}] boolean_criteria = [criterion for criterion in data_table if 'boolean' in criterion.lower()] # print("Boolean Criteria:", boolean_criteria) @@ -18,10 +18,10 @@ def perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_nam # The first category is for the all False and the last for the all True values fog_node_categories = {i: [] for i in range(len(boolean_criteria) + 1)} - # Iterate over the list of fog nodes to count the '1' (True) values and assign categories - for i in range(len(node_names)): + # Iterate over the list of nodes to count the '1' (True) values and assign categories + for i in range(len(node_ids)): true_count = sum(data_table[boolean][i] for boolean in boolean_criteria) - fog_node_categories[true_count].append(node_names[i]) + fog_node_categories[true_count].append(node_ids[i]) # Remove the boolean criteria from the data_table for boolean in boolean_criteria: @@ -41,8 +41,8 @@ def perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_nam for fog_node_high in fog_node_categories[sorted_categories[higher_cat]]: for fog_node_low in fog_node_categories[sorted_categories[higher_cat + 1]]: # Create a constraint for each pair of fog nodes (high > low) - high_scores = [-data_table[criterion][node_names.index(fog_node_high)] for criterion in data_table] - low_scores = [-data_table[criterion][node_names.index(fog_node_low)] for criterion in data_table] + high_scores = [-data_table[criterion][node_ids.index(fog_node_high)] for criterion in data_table] + low_scores = [-data_table[criterion][node_ids.index(fog_node_low)] for criterion in data_table] constraint = [h - l for h, l in zip(high_scores, low_scores)] A_boolean.append(constraint) b_boolean.append(0) # The score difference must be greater than 0 @@ -124,8 +124,8 @@ def perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_nam num_of_dmus = len(next(iter(data_table.values()))) Cols_No = len(criteria_list) DEA_Scores = [] - epsilon = 0.00000 # Lower bound of the variables - + # epsilon = 0.000001 # Lower bound of the variables + epsilon = 0 # Iterating over each DMU to Perform DEA for dmu_index in range(num_of_dmus): # Gathering values for the current DMU @@ -171,7 +171,7 @@ def perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_nam "DEA Score": DEA_Scores[i], "Rank": int(DEA_Scores_Ranked[i]) } - for i in range(len(node_names)) + for i in range(len(node_ids)) ] # Return successful results @@ -184,9 +184,9 @@ def perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_nam # relative_wr_data: [{'LHSCriterion': 'Accountability', 'Operator': 1, 'Intense': 2, 'RHSCriterion': 'Compliance'}] # immediate_wr_data: [{'Criterion': 'Compliance', 'Operator': 1, 'Value': 0.5}] # -# node_names = ['2ad4bd97-d932-42a5-860e-e607a50f161d', 'e917581d-1a62-496b-9d2e-05972fe309e9', '78aca9a8-8c14-4c7d-af34-72cef0da992d', 'd2bddce9-4118-41a9-b528-3bac32b13312'] +# node_ids = ['2ad4bd97-d932-42a5-860e-e607a50f161d', 'e917581d-1a62-496b-9d2e-05972fe309e9', '78aca9a8-8c14-4c7d-af34-72cef0da992d', 'd2bddce9-4118-41a9-b528-3bac32b13312'] # -# Evaluation_JSON = perform_evaluation(data_table, [], [], node_names) +# Evaluation_JSON = perform_evaluation(data_table, [], [], node_ids) # pretty_json = json.dumps(Evaluation_JSON) @@ -218,13 +218,13 @@ def perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_nam # # # "immediate_wr_data":[{"Criterion":"Accountability","Operator":1,"Value":0.2}]} # # # w1>=0.2 and w1<=0.5 # # -# node_names = ['Fog Node 1', 'Fog Node 2', 'Fog Node 3', 'Fog Node 4', 'Fog Node 5'] +# node_ids = ['Fog Node 1', 'Fog Node 2', 'Fog Node 3', 'Fog Node 4', 'Fog Node 5'] # -# Evaluation_JSON = perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_names) +# Evaluation_JSON = perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_ids) # print("Evaluation_JSON:", Evaluation_JSON) -# Evaluation_JSON = perform_evaluation(data_table, [], [], node_names) +# Evaluation_JSON = perform_evaluation(data_table, [], [], node_ids) # pretty_json = json.dumps(Evaluation_JSON) # print(pretty_json) # print("Evaluation_JSON:", Evaluation_JSON) diff --git a/cfsb-backend/User_Functions.py b/cfsb-backend/User_Functions.py index b6f0e8b..946f9ca 100644 --- a/cfsb-backend/User_Functions.py +++ b/cfsb-backend/User_Functions.py @@ -1,12 +1,8 @@ import os -# import read_file import get_data as file import random import json from datetime import datetime -import data_types as attr_data_types -from Evaluation import perform_evaluation -from data_types import get_attr_data_type import db.db_functions as db_functions # Boolean_Variables = ['Extend offered network capacity', 'Extend offered processing capacity', 'Extend offered memory capacity', @@ -16,48 +12,93 @@ Boolean_Variables = [ "0cf00a53-fd33-4887-bb38-e0bbb04e3f3e", "d95c1dae-1e22-4fb4-9cdc-743e96d0dddc", "8cd09fe9-c119-4ccd-b651-0f18334dbbe4", "7147995c-8e68-4106-ab24-f0a7673eb5f5", "c1c5b3c9-6178-4d67-a7e3-0285c2bf98ef"] -# Used to transform SAL's response before sending to DataGrid -# This version is designed to read the structure of SAL's response obtained from POSTMAN -def extract_node_candidate_data(json_file_path): - with open(json_file_path, 'r') as file: - json_data = json.load(file) +# Used to extract_SAL_node_candidate_data from Use Side for DataGrid +def extract_SAL_node_candidate_data_Front(json_data): + default_criteria_list = ["cores", "ram", "disk", "memoryPrice", "price"] + + if isinstance(json_data, dict): # Single node dictionary + json_data = [json_data] # Wrap it in a list extracted_data = [] node_ids = [] node_names = [] for item in json_data: - hardware_info = item.get("nodeCandidate", {}).get("hardware", {}) + hardware_info = item.get("hardware", {}) + # Extract default criteria values + default_criteria_values = {criteria: hardware_info.get(criteria, 0.0) if criteria in hardware_info else item.get(criteria, 0.0) for criteria in default_criteria_list} + + # Correctly extract the providerName from the cloud information + cloud_info = item.get("cloud", {}) # get the cloud info or default to an empty dict + api_info = cloud_info.get("api", {}) + provider_name = api_info.get("providerName", "Unknown Provider") + + # each item is now a dictionary node_data = { - "name": item['name'], - "id": item['id'], - "nodeId": item.get("nodeCandidate", {}).get("nodeId"), - "nodeCandidateType": item.get("nodeCandidate", {}).get("nodeCandidateType"), - "price": item.get("nodeCandidate", {}).get("price", 0.0), - "pricePerInvocation": item.get("nodeCandidate", {}).get("pricePerInvocation", 0.0), - "memoryPrice": item.get("nodeCandidate", {}).get("memoryPrice", 0.0), - "hardware": { - "id": hardware_info.get("id"), - "name": hardware_info.get("name"), - "providerId": hardware_info.get("providerId"), - "cores": hardware_info.get("cores"), - "ram": hardware_info.get("ram") * 1024 if hardware_info.get("ram") else None, # Assuming RAM needs conversion from GB to MB - "disk": hardware_info.get("disk"), - "fpga": hardware_info.get("fpga") - } + "nodeId": item.get("nodeId", ''), + "id": item.get('id', ''), + "nodeCandidateType": item.get("nodeCandidateType", ''), + **default_criteria_values, # Unpack default criteria values into node_data + "hardware": hardware_info, + "location": item.get("location", {}), + "image": item.get("image", {}), + "providerName": provider_name } extracted_data.append(node_data) - node_ids.append(item['id']) - node_names.append(item.get('name', '')) + node_ids.append(node_data["id"]) - number_of_nodes = len(json_data) + # print("Before create_node_name") + node_names.append(create_node_name(node_data)) # call create_node_name function + # print("After create_node_name") - return extracted_data, number_of_nodes, node_ids, node_names + return extracted_data, node_ids, node_names +# Used to create node names for DataGrid +def create_node_name(node_data): + node_type = node_data.get("nodeCandidateType", "UNKNOWN_TYPE") + # Initialize default values + node_city = "" + node_country = "" + node_os_family = "Unknown OS" + provider_name = node_data.get("providerName", "") -# Only 50 nodes + # Safely access nested properties for city and country + location = node_data.get("location") + if location and "geoLocation" in location and location["geoLocation"]: + geo_location = location["geoLocation"] + node_city = geo_location.get("city", "") + node_country = geo_location.get("country", "") + + image = node_data.get("image") + if image and "operatingSystem" in image and image["operatingSystem"]: + operating_system = image["operatingSystem"] + node_os_family = operating_system.get("operatingSystemFamily", node_os_family) + + cores = node_data.get("cores", "") + ram = node_data.get("ram", "") + + # Construct the node name with conditional inclusions + node_name_parts = [node_type] + if node_city and node_country: + node_name_parts.append(f"{node_city}, {node_country}") + + if provider_name: + node_name_parts.append(f"Provider: {provider_name}") + + node_name_parts.append(f"OS: {node_os_family}") + + if cores: + node_name_parts.append(f"Cores: {cores} ") + if ram: + node_name_parts.append(f"RAM: {ram} ") + + node_name = " - ".join(part for part in node_name_parts if part) # Only include non-empty parts + return node_name + +# Used to extract_SAL_node_candidate_data from App Side working with Optimizer def extract_SAL_node_candidate_data(json_string): + # print("Entered in extract_SAL_node_candidate_data") try: json_data = json.loads(json_string) # Ensure json_data is a list of dictionaries except json.JSONDecodeError as e: @@ -73,9 +114,9 @@ def extract_SAL_node_candidate_data(json_string): "nodeId": item.get("nodeId", ''), "id": item.get('id', ''), "nodeCandidateType": item.get("nodeCandidateType", ''), - "price": item.get("price", 0.0), - "pricePerInvocation": item.get("pricePerInvocation", 0.0), - "memoryPrice": item.get("memoryPrice", 0.0), + "price": item.get("price", ''), + "pricePerInvocation": item.get("pricePerInvocation", ''), + "memoryPrice": item.get("memoryPrice", ''), "hardware": item.get("hardware", {}) } extracted_data.append(node_data) @@ -85,48 +126,11 @@ def extract_SAL_node_candidate_data(json_string): number_of_nodes = len(extracted_data) node_ids = [node['id'] for node in extracted_data] node_names = [node['id'] for node in extracted_data] - - return extracted_data, number_of_nodes, node_ids, node_names - - - -# Used to transform SAL's response all nodes -# def extract_SAL_node_candidate_data(sal_reply): -# # Parse the JSON string in the body of the SAL reply -# body = sal_reply.get('body', '') -# extracted_data = [] -# -# try: -# json_data = json.loads(body) -# except json.JSONDecodeError as e: -# print(f"Error parsing JSON: {e}") -# return extracted_data -# -# for item in json_data: -# node_data = { -# "name": item.get('name', ''), -# "name": item.get('id', ''), -# "id": item.get('id', ''), -# "nodeId": item.get("nodeId", ''), -# "nodeCandidateType": item.get("nodeCandidateType", ''), -# "price": item.get("price", 0.0), -# "pricePerInvocation": item.get("pricePerInvocation", 0.0), -# "memoryPrice": item.get("memoryPrice", 0.0), -# "hardware": item.get("hardware", {}) -# } -# extracted_data.append(node_data) -# -# number_of_nodes = len(extracted_data) -# node_ids = [node['id'] for node in extracted_data] -# node_names = [node['name'] for node in extracted_data] -# if not node_names: -# node_names = node_ids -# -# return extracted_data, number_of_nodes, node_ids, node_names + return extracted_data, node_ids, node_names # Used to map the criteria from SAL's response with the selected criteria (from frontend) -def create_criteria_mapping(selected_items, extracted_data): +def create_criteria_mapping(): field_mapping = { # "Cost": "price", "Operating cost": "price", @@ -137,7 +141,8 @@ def create_criteria_mapping(selected_items, extracted_data): } return field_mapping -# Used to create the required structure for the Evaluation + +# Used to create the required structure for the Evaluation in process_evaluation_data endpoint def transform_grid_data_to_table(json_data): grid_data = json_data.get('gridData', []) relative_wr_data = json_data.get('relativeWRData', []) @@ -153,8 +158,9 @@ def transform_grid_data_to_table(json_data): boolean_value_mapping = {"True": 1, "False": 0} for node in grid_data: - node_name = node.get('name') + # node_name = node.get('name') node_ids.append(node.get('id')) + node_id = node.get('id') criteria_data = {} for criterion in node.get('criteria', []): @@ -177,7 +183,7 @@ def transform_grid_data_to_table(json_data): # Handle or log the error for values that can't be converted to float pass - temp_data_table[node_name] = criteria_data + temp_data_table[node_id] = criteria_data # Collect all criteria titles criteria_titles.extend(criteria_data.keys()) @@ -189,7 +195,7 @@ def transform_grid_data_to_table(json_data): data_table = {title: [] for title in criteria_titles} # Populate the final data table - for node_name, criteria_data in temp_data_table.items(): + for node_id, criteria_data in temp_data_table.items(): for title, value in criteria_data.items(): data_table[title].append(value) @@ -254,64 +260,6 @@ def check_json_file_exists(app_id): return os.path.exists(file_path) - -# Used to read ALL the saved Data for an Application -# def read_application_data(app_id): -# # Directory path and file path -# app_dir = os.path.join("app_dirs", app_id) -# file_path = os.path.join(app_dir, "data.json") -# -# # Check if the file exists -# if os.path.exists(file_path): -# # Read and parse the JSON file -# with open(file_path, 'r', encoding='utf-8') as f: -# data = json.load(f) -# # Extract specific parts of the data -# # selected_criteria = data.get("selectedCriteria", None) -# data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = transform_grid_data_to_table(data) -# else: -# print(f"No data found for application ID {app_id}.") # Return everything empty -# data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = [], [], [], [], [] -# -# return data_table, relative_wr_data, immediate_wr_data, node_names, node_ids - -# Used to read the saved Data of the Application ONLY for the Nodes returned by SAL -def read_application_data(app_id, node_ids_SAL): - # Directory path and file path - app_dir = os.path.join("app_dirs", app_id) - file_path = os.path.join(app_dir, f"{app_id}_data.json") - - # Initialize variables to return in case of no data or an error - data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = [], [], [], [], [] - - # Check if the file exists - if os.path.exists(file_path): - # Read and parse the JSON file - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - # Filter gridData based on node_ids_SAL - filtered_grid_data = [node for node in data['gridData'] if node['id'] in node_ids_SAL] - - # Create a new JSON structure with filtered gridData - filtered_json_data = { - "gridData": filtered_grid_data, - "relativeWRData": data['relativeWRData'], - "immediateWRData": data['immediateWRData'], - "nodeNames": [node['name'] for node in filtered_grid_data], # Assuming you want to filter nodeNames as well - "nodeIds": node_ids_SAL # Assuming you want to include nodeIds from the filtered list - } - - # Call transform_grid_data_to_table with the new filtered JSON data - data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = transform_grid_data_to_table(filtered_json_data) - else: - print(f"No data found for application ID {app_id}.") - - return data_table, relative_wr_data, immediate_wr_data, node_names - - - -#Used to create data table from SAL's response in app_side def create_data_table(selected_criteria, extracted_data, field_mapping): # Initialize the data table with lists for each criterion data_table = {criterion: [] for criterion in selected_criteria} @@ -339,59 +287,23 @@ def create_data_table(selected_criteria, extracted_data, field_mapping): return data_table - - -import random - -# def append_evaluation_results(sal_reply_body, scores_and_ranks): -# # Check if sal_reply_body is a string and convert it to a Python object -# if isinstance(sal_reply_body, str): -# sal_reply_body = json.loads(sal_reply_body) -# -# if scores_and_ranks: -# # Create a dictionary mapping Ids to scores and ranks -# eval_results_dict = {result['Id']: (result['DEA Score'], result['Rank']) -# for result in scores_and_ranks} -# -# # Iterate over each node in sal_reply_body and append Score and Rank -# for node in sal_reply_body: -# node_id = node.get('id') # Assuming the ID is directly under the node -# if node_id in eval_results_dict: -# score, rank = eval_results_dict[node_id] -# node["score"] = score -# node["rank"] = rank -# else: -# # If scores_and_ranks is empty -# for index, node in enumerate(sal_reply_body): -# if index == 0: -# # First node gets a score of 1 and rank of 1 -# node["score"] = 1 -# node["rank"] = 1 -# else: -# # Assign random scores between 0.33 and 0.93 to the rest -# node["score"] = random.uniform(0.33, 0.93) -# -# # Sort nodes by score in descending order to calculate ranks -# sorted_nodes = sorted(sal_reply_body[1:], key=lambda x: x["score"], reverse=True) -# -# # Assign ranks based on sorted order, starting from 2 since the first node is ranked 1 -# for rank, node in enumerate(sorted_nodes, start=2): -# node["rank"] = rank -# -# # Combine the first node with the rest -# sal_reply_body = [sal_reply_body[0]] + sorted_nodes -# -# return sal_reply_body - - +# Used to Append "Score" and "Rank" for each node in SAL's response JSON def append_evaluation_results(sal_reply_body, scores_and_ranks): # Check if sal_reply_body is a string and convert it to a Python object if isinstance(sal_reply_body, str): sal_reply_body = json.loads(sal_reply_body) + # Check if there is only one node and scores_and_ranks are empty + if len(sal_reply_body) == 1 and not scores_and_ranks: + # Directly assign score and rank to the single node + sal_reply_body[0]["score"] = 1 + sal_reply_body[0]["rank"] = 1 + return sal_reply_body + + # Proceed if there are multiple nodes or scores_and_ranks is not empty # Create a dictionary mapping Ids to scores and ranks eval_results_dict = {result['Id']: (result['DEA Score'], result['Rank']) - for result in scores_and_ranks} + for result in scores_and_ranks if scores_and_ranks} # Iterate over each node in sal_reply_body and append Score and Rank for node in sal_reply_body: @@ -404,37 +316,387 @@ def append_evaluation_results(sal_reply_body, scores_and_ranks): return sal_reply_body +def convert_value(value, criterion_info, is_matched): + if criterion_info['type'] == 5: # Boolean type + return 1 if value else 0 + elif criterion_info['type'] == 1: # Ordinal type + if is_matched: # For matched nodes, use the mapping + ordinal_value_mapping = {"High": 3, "Medium": 2, "Low": 1} + return ordinal_value_mapping.get(value, value) # Use the value from mapping, or keep it as is if not found + else: # For unmatched nodes, assign default value + return 1 + return value +# Used to read the saved application data CFSB when triggered by Optimizer +def read_application_data(app_id, sal_reply_body): + app_dir = os.path.join("app_dirs", app_id) + file_path = os.path.join(app_dir, f"{app_id}_data.json") + + data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = {}, [], [], [], [] + + if isinstance(sal_reply_body, str): + sal_reply_body = json.loads(sal_reply_body) + + if os.path.exists(file_path): + print(f"JSON file found for application ID {app_id}.") + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + selected_criteria = {criterion['title']: criterion for criterion in data.get('selectedCriteria', [])} + + # Define the default list criteria mapping + default_list_criteria_mapping = { + "Operating cost": "price", + "Memory Price": "memoryPrice", + "Number of CPU Cores": "cores", + "Memory Size": "ram", + "Storage Capacity": "disk" + } + + for criterion in selected_criteria: + data_table[criterion] = [] + + matched_node_ids = [node['id'] for node in data.get('gridData', []) if node['id'] in [n['id'] for n in sal_reply_body]] + unmatched_node_ids = [n['id'] for n in sal_reply_body if n['id'] not in matched_node_ids] + + # Process MATCHED nodes + for node in data.get('gridData', []): + if node['id'] in matched_node_ids: + node_ids.append(node['id']) + # node_names.append(node.get('name', 'Unknown')) + for crit, criterion_info in selected_criteria.items(): + value = next((criterion['value'] for criterion in node['criteria'] if criterion['title'] == crit), None) + converted_value = convert_value(value, criterion_info, is_matched=True) + data_table[crit].append(converted_value) + + # Process UNMATCHED nodes + for node_id in unmatched_node_ids: + node_data = next((node for node in sal_reply_body if node['id'] == node_id), {}) + node_ids.append(node_id) + for criterion, crit_info in selected_criteria.items(): + mapped_field = default_list_criteria_mapping.get(criterion, '') + value = node_data.get(mapped_field, 0.001 if crit_info['type'] == 2 else False) + converted_value = convert_value(value, crit_info, is_matched=False) + data_table[criterion].append(converted_value) + + node_names = node_ids + relative_wr_data, immediate_wr_data = data.get('relativeWRData', []), data.get('immediateWRData', []) + + else: # There is not any node id match - Proceed only with the nodes from SAL's reply + print(f"No JSON file found for application ID {app_id}. Proceed only with data from SAL.") + extracted_data_SAL, node_ids_SAL, node_names_SAL = extract_SAL_node_candidate_data(sal_reply_body) + selected_criteria = ["Number of CPU Cores", "Memory Size"] + field_mapping = create_criteria_mapping() + data_table = create_data_table(selected_criteria, extracted_data_SAL, field_mapping) + # Assign relativeWRData and immediateWRData regardless of node ID matches + relative_wr_data = [] + immediate_wr_data = [] + node_ids = node_ids_SAL + node_names = node_ids + + return data_table, relative_wr_data, immediate_wr_data, node_names, node_ids +# Used to generate random values for DataGrid +def random_value_based_on_type(data_type, criterion_info=None): + if data_type == 1: # Ordinal + # Assuming 'values' are part of criterion_info for ordinal types + return random.choice(criterion_info.get('values', ["High", "Medium", "Low"])) + elif data_type == 5: # Boolean + return random.choice([True, False]) + else: # Numeric + # Default case for numeric types + return round(random.uniform(1, 100), 2) +# Used to parse Patini's JSON +def parse_device_info_from_file(file_path): + with open(file_path, 'r') as file: + json_data = json.load(file) + device_names = [] + device_info = { + 'id': json_data['_id'], + 'name': json_data['name'], # Save the device name + 'deviceInfo': json_data['deviceInfo'], + 'creationDate': json_data['creationDate'], + 'lastUpdateDate': json_data['lastUpdateDate'], + 'status': json_data['status'], + 'metrics': { + 'cpu': json_data['metrics']['metrics']['cpu'], + 'uptime': json_data['metrics']['metrics']['uptime'], + 'disk': json_data['metrics']['metrics']['disk'], + 'ram': json_data['metrics']['metrics']['ram'] + } + } + + # Example of converting and handling ISODate strings, adjust accordingly + device_info['creationDate'] = datetime.fromisoformat(device_info['creationDate'].replace("ISODate('", "").replace("')", "")) + device_info['lastUpdateDate'] = datetime.fromisoformat(device_info['lastUpdateDate'].replace("ISODate('", "").replace("')", "")) + device_info['creationDate'] = device_info['creationDate'].isoformat() + device_info['lastUpdateDate'] = device_info['lastUpdateDate'].isoformat() + + # Update the global device_names list + device_names.append({'id': device_info['id'], 'name': device_info['name']}) + return device_names, device_info +#---------------Read Application Data - - - - -# Example usage -# extracted_data, NUMBER_OF_FOG_NODES, node_names = extract_node_candidate_data('dummy_data_node_candidates.json') -# print(NUMBER_OF_FOG_NODES) -# print(node_names) - -# app_id = 'd535cf554ea66fbebfc415ac837a5828' -# data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = read_app_specific_data(app_id) +# Used to read the saved Data of the Application ONLY for the Nodes returned by SAL +# def read_application_data(app_id, sal_reply_body): +# # Directory path and file path +# app_dir = os.path.join("app_dirs", app_id) +# file_path = os.path.join(app_dir, f"{app_id}_data.json") # -# print("Node Names:", node_names) -# print("data_table:", data_table) -# print("Relative WR Data:", relative_wr_data) -# print("Immediate WR Data:", immediate_wr_data) +# # Initialize variables to return in case of no data or an error +# data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = [], [], [], [], [] +# # Read data from SAL's reply +# extracted_data_SAL, node_ids_SAL, node_names_SAL = extract_SAL_node_candidate_data(sal_reply_body) # -# evaluation_results = perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_names, node_ids) -# print("evaluation_results:", evaluation_results) +# # Check if the file exists +# if os.path.exists(file_path): +# # Read and parse the JSON file +# with open(file_path, 'r', encoding='utf-8') as f: +# data = json.load(f) # -# # Extracting the results and saving them into a variable -# ScoresAndRanks = evaluation_results['results'] -# print("ScoresAndRanks:", ScoresAndRanks) +# # Filter gridData based on Nodes returned by SAL +# filtered_grid_data = [node for node in data.get('gridData', []) if node.get('id') in node_ids_SAL] +# +# if filtered_grid_data: # if there's at least 1 match +# # Create a new JSON structure and call transform_grid_data_to_table +# filtered_json_data = { +# "gridData": filtered_grid_data, +# "relativeWRData": relative_wr_data, +# "immediateWRData": immediate_wr_data, +# "nodeNames": [node.get('name') for node in filtered_grid_data], +# "nodeIds": node_ids_SAL +# } +# +# # Call transform_grid_data_to_table with the filtered JSON data +# # data_table, _, _, node_names, _ = transform_grid_data_to_table(filtered_json_data) +# data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = transform_grid_data_to_table(filtered_json_data) +# if not node_names: +# node_names = node_ids +# +# else: # There is not any node id match - Proceed only with the nodes from SAL's reply +# print("No matching node IDs found in the saved data. Proceed only with data from SAL") +# selected_criteria = ["Number of CPU Cores", "Memory Size"] +# field_mapping = create_criteria_mapping(selected_criteria, extracted_data_SAL) +# data_table = create_data_table(selected_criteria, extracted_data_SAL, field_mapping) +# # Assign relativeWRData and immediateWRData regardless of node ID matches +# relative_wr_data = [] +# immediate_wr_data = [] +# node_ids = node_ids_SAL +# node_names = node_ids +# if not node_names_SAL: +# node_names = node_ids +# else: +# print(f"No JSON file found for application ID {app_id}.") +# +# # Note: relative_wr_data and immediate_wr_data are returned regardless of the node IDs match +# return data_table, relative_wr_data, immediate_wr_data, node_names, node_ids -# append_evaluation_results('SAL_Response_11EdgeDevs.json', ScoresAndRanks) + + +#Used to create data table from SAL's response in app_side + +# def read_application_data(app_id, sal_reply_body): +# app_dir = os.path.join("app_dirs", app_id) +# file_path = os.path.join(app_dir, f"{app_id}_data.json") +# data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = {}, [], [], [], [] +# +# default_list_criteria_mapping = { +# "Operating cost": "price", +# "Memory Price": "memoryPrice", +# "Number of CPU Cores": "cores", +# "Memory Size": "ram", +# "Storage Capacity": "disk" +# } +# +# if isinstance(sal_reply_body, str): +# try: +# sal_reply_body = json.loads(sal_reply_body) +# except json.JSONDecodeError as e: +# print(f"Error parsing JSON: {e}") +# return data_table, relative_wr_data, immediate_wr_data, node_names, node_ids +# +# if os.path.exists(file_path): +# with open(file_path, 'r', encoding='utf-8') as f: +# data = json.load(f) +# selected_criteria = {criterion['title']: criterion for criterion in data.get('selectedCriteria', [])} +# +# for criterion in selected_criteria.keys(): +# data_table[criterion] = [] +# +# matched_node_ids = set(node['id'] for node in data.get('gridData', [])) & set(node['id'] for node in sal_reply_body) +# unmatched_node_ids = set(node['id'] for node in sal_reply_body) - matched_node_ids +# +# # Ordinal value mapping for MATCHED nodes +# ordinal_value_mapping = {"High": 3, "Medium": 2, "Low": 1} +# +# # Process MATCHED nodes from JSON file +# for node in data.get('gridData', []): +# if node['id'] in matched_node_ids: +# node_ids.append(node['id']) +# # node_names.append(node.get('name', 'Unknown')) +# for criterion, crit_info in selected_criteria.items(): +# value = next((c['value'] for c in node['criteria'] if c['title'] == criterion), None) +# if value is not None: +# value = 1 if value is True else (0 if value is False else value) +# else: # Apply default if criterion not found +# value = 0.00001 if crit_info['type'] == 2 else 0 +# data_table[criterion].append(value) +# +# # Process UNMATCHED nodes from sal_reply_body +# for node_id in unmatched_node_ids: +# node_data = next((node for node in sal_reply_body if node['id'] == node_id), {}) +# node_ids.append(node_id) +# for criterion, crit_info in selected_criteria.items(): +# mapped_field = default_list_criteria_mapping.get(criterion, '') +# value = node_data.get(mapped_field, 0.00001 if crit_info['type'] == 2 else False) +# value = 1 if value is True else (0 if value is False else value) +# data_table[criterion].append(value) +# +# # convert True/False to 1/0 in data_table for both boolean and string representations +# for criterion, values in data_table.items(): +# data_table[criterion] = [convert_bool(value) for value in values] +# node_names = node_ids +# relative_wr_data, immediate_wr_data = data.get('relativeWRData', []), data.get('immediateWRData', []) +# +# else: # There is not any node id match - Proceed only with the nodes from SAL's reply +# print(f"No JSON file found for application ID {app_id}. Proceed only with data from SAL.") +# extracted_data_SAL, node_ids_SAL, node_names_SAL = extract_SAL_node_candidate_data(sal_reply_body) +# selected_criteria = ["Number of CPU Cores", "Memory Size"] +# field_mapping = create_criteria_mapping(selected_criteria, extracted_data_SAL) +# data_table = create_data_table(selected_criteria, extracted_data_SAL, field_mapping) +# # Assign relativeWRData and immediateWRData regardless of node ID matches +# relative_wr_data = [] +# immediate_wr_data = [] +# node_ids = node_ids_SAL +# node_names = node_ids +# +# return data_table, relative_wr_data, immediate_wr_data, node_names, node_ids + + +# Used to transform SAL's response before sending to DataGrid +# This version is designed to read the structure of SAL's response obtained from POSTMAN +def extract_node_candidate_data(json_file_path): + with open(json_file_path, 'r') as file: + json_data = json.load(file) + + extracted_data = [] + node_ids = [] + node_names = [] + + for item in json_data: + hardware_info = item.get("nodeCandidate", {}).get("hardware", {}) + node_data = { + "name": item['name'], + "id": item['id'], + "nodeId": item.get("nodeCandidate", {}).get("nodeId"), + "nodeCandidateType": item.get("nodeCandidate", {}).get("nodeCandidateType"), + "price": item.get("nodeCandidate", {}).get("price", 0.0), + "pricePerInvocation": item.get("nodeCandidate", {}).get("pricePerInvocation", 0.0), + "memoryPrice": item.get("nodeCandidate", {}).get("memoryPrice", 0.0), + "hardware": { + "id": hardware_info.get("id"), + "name": hardware_info.get("name"), + "providerId": hardware_info.get("providerId"), + "cores": hardware_info.get("cores"), + "ram": hardware_info.get("ram") * 1024 if hardware_info.get("ram") else None, # Assuming RAM needs conversion from GB to MB + "disk": hardware_info.get("disk"), + "fpga": hardware_info.get("fpga") + } + } + extracted_data.append(node_data) + node_ids.append(item['id']) + node_names.append(item.get('name', '')) + + return extracted_data, node_ids, node_names + + +# Works for dummy_node_data +# def create_node_name(node_data): +# # dummy_node_data = '''{ +# # "id": "8a7481d98e702b64018e702cbe070000", +# # "nodeCandidateType": "EDGE", +# # "jobIdForByon": null, +# # "jobIdForEdge": "FCRnewLight0", +# # "price": 0.0, +# # "cloud": { +# # "id": "edge", +# # "endpoint": null, +# # "cloudType": "EDGE", +# # "api": null, +# # "credential": null, +# # "cloudConfiguration": { +# # "nodeGroup": null, +# # "properties": {} +# # }, +# # "owner": "EDGE", +# # "state": null, +# # "diagnostic": null +# # }, +# # "location": { +# # "id": "edge-location-KmVf4xDJKL7acBGc", +# # "name": null, +# # "providerId": null, +# # "locationScope": null, +# # "isAssignable": null, +# # "geoLocation": { +# # "city": "Warsaw", +# # "country": "Poland", +# # "latitude": 52.237049, +# # "longitude": 21.017532 +# # }, +# # "parent": null, +# # "state": null, +# # "owner": null +# # }, +# # "image": { +# # "id": "edge-image-KmVf4xDJKL7acBGc", +# # "name": "edge-image-name-UBUNTU-UNKNOWN", +# # "providerId": null, +# # "operatingSystem": { +# # "operatingSystemFamily": "UBUNTU", +# # "operatingSystemArchitecture": "UNKNOWN", +# # "operatingSystemVersion": 1804.00 +# # }, +# # "location": null, +# # "state": null, +# # "owner": null +# # }, +# # "hardware": { +# # "id": "edge-hardware-KmVf4xDJKL7acBGc", +# # "name": null, +# # "providerId": null, +# # "cores": 1, +# # "ram": 1, +# # "disk": 1.0, +# # "fpga": 0, +# # "location": null, +# # "state": null, +# # "owner": null +# # }, +# # "pricePerInvocation": 0.0, +# # "memoryPrice": 0.0, +# # "nodeId": null, +# # "environment": null +# # }''' +# # node_data = json.loads(dummy_node_data) +# # print("node_data in create node name") +# # print(node_data) +# node_type = node_data["nodeCandidateType"] +# # print(node_type) +# if node_data["location"]: +# node_location = node_data["location"]["geoLocation"] +# # print(json.dumps(node_location)) +# node_city = node_location["city"] +# node_country = node_location["country"] +# else: +# node_city = "" +# node_country = "" +# node_os = node_data["image"]["operatingSystem"]["operatingSystemFamily"] +# node_name = node_type + " - " + node_city + " , " + node_country + " - " + node_os +# # print("node name crated: " + node_name) +# return node_name \ No newline at end of file diff --git a/cfsb-backend/activemq.py b/cfsb-backend/activemq.py index dafaa01..be45ff9 100644 --- a/cfsb-backend/activemq.py +++ b/cfsb-backend/activemq.py @@ -1,8 +1,6 @@ # ActiveMQ communication logic import sys import threading -import json -import time sys.path.insert(0,'../exn') import logging from dotenv import load_dotenv @@ -18,9 +16,10 @@ from exn.core.handler import Handler from exn.handler.connector_handler import ConnectorHandler from User_Functions import * import uuid +from Evaluation import perform_evaluation -# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -# logging.getLogger('exn.connector').setLevel(logging.CRITICAL) +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logging.getLogger('exn.connector').setLevel(logging.CRITICAL) class SyncedHandler(Handler): def on_message(self, key, address, body, message: Message, context=None): @@ -34,77 +33,58 @@ class SyncedHandler(Handler): # logging.info("Entered in OPT-triggering'") # Save the correlation_id (We do not have it from the app_side) - uuid.uuid4().hex.encode("utf-8") # for Correlation id - + uuid.uuid4().hex.encode("utf-8") # for Correlation id correlation_id_optimizer = message.correlation_id if not correlation_id_optimizer: correlation_id_optimizer = '88334290cad34ad9b21eb468a9f8ff11' # dummy correlation_id + # logging.info(f"Optimizer_correlation_id {message.correlation_id}") - print("Optimizer Correlation Id: ", correlation_id_optimizer) + # print("Optimizer Correlation Id: ", correlation_id_optimizer) # application_id_optimizer = message.properties.application # can be taken also from message.annotations.application application_id_optimizer = message.subject # application_id_optimizer = 'd535cf554ea66fbebfc415ac837a5828' #dummy application_id_optimizer - print("Application Id: ", application_id_optimizer) + # print("Application Id: ", application_id_optimizer) try: # Read the Message Sent from Optimizer opt_message_data = body - print("Whole Message Sent from Optimizer:", opt_message_data) + # print("Whole Message Sent from Optimizer:", opt_message_data) # Extract 'body' from opt_message_data body_sent_from_optimizer = opt_message_data.get('body', {}) - # 100 Nodes + ## Example body # body_sent_from_optimizer = [ # { # "type": "NodeTypeRequirement", - # "nodeTypes": ["IAAS"], - # "jobIdForByon": "dummy-app-id", - # "jobIdForEDGE": "dummy-app-id" - # } - # ] - - - # 58 Nodes - # body_sent_from_optimizer = [ - # { - # "type": "NodeTypeRequirement", - # "nodeTypes": ["IAAS"], - # "jobIdForByon": "dummy-app-id", - # "jobIdForEDGE": "dummy-app-id" - # }, - # { - # "type": "AttributeRequirement", - # "requirementClass": "hardware", - # "requirementAttribute": "cores", - # "requirementOperator": "EQ", - # "value": "2" - # }, - # { - # "type": "AttributeRequirement", - # "requirementClass": "hardware", - # "requirementAttribute": "ram", - # "requirementOperator": "EQ", - # "value": "4096" + # # "nodeTypes": ["EDGES"] + # "nodeTypes": ["IAAS", "PAAS", "FAAS", "BYON", "EDGE", "SIMULATION"] + # # ,"jobIdForEDGE": "FCRnewLight0" # } + # # ,{ + # # "type": "AttributeRequirement", + # # "requirementClass": "hardware", + # # "requirementAttribute": "ram", + # # "requirementOperator": "EQ", + # # "value": "2" + # # } # ] # logging.info(body_sent_from_optimizer) - # print("Extracted body from Optimizer Message:", body_sent_from_optimizer) + print("Extracted body from Optimizer Message:", body_sent_from_optimizer) ## Prepare message to be send to SAL # Convert the body data to a JSON string + # body_json_string = json.dumps(body_sent_from_optimizer) # For Sender + body_json_string = body_sent_from_optimizer # For Optimizer - # body_json_string = json.dumps(body_sent_from_optimizer) - body_json_string = body_sent_from_optimizer RequestToSal = { # Dictionary "metaData": {"user": "admin"}, # key [String "metaData"] value [dictionary] "body": body_json_string # key [String "body"] value [JSON String] } # logging.info("RequestToSal: %s", RequestToSal) - print("RequestToSal:", RequestToSal) - + # print("RequestToSal:", RequestToSal) # print("Is RequestToSal a valid dictionary:", isinstance(RequestToSal, dict)) # print("Is the 'body' string in RequestToSal a valid JSON string:", is_json(RequestToSal["body"])) @@ -112,128 +92,106 @@ class SyncedHandler(Handler): sal_reply = context.publishers['SAL-GET'].send_sync(RequestToSal) ## Process SAL's Reply - # sal_reply_body = sal_reply.get('body') sal_body = sal_reply.get('body') # Get the 'body' as a JSON string - # try: - # # Parse the JSON string to a Python object - # nodes_data = json.loads(sal_body) - # total_nodes = len(nodes_data) # Get the total number of nodes - # - # # Check if more than 51 nodes exist - # if total_nodes > 58: - # print("More than 58 nodes exist. Only the first 51 nodes will be processed.") - # # Filter to only include the first 51 nodes - # sal_reply_body = nodes_data[:60] - # else: - # print(f"Total {total_nodes} nodes found. Processing all nodes.") - # sal_reply_body = sal_reply.get('body') - # - # except json.JSONDecodeError as e: - # print(f"Error parsing JSON: {e}") - - - # filename = 'SAL_Response_10EdgeDevs.json' - # with open(filename, 'r') as file: - # sal_reply_body = json.load(file) - # print("SAL's Reply from JSON File:", sal_reply_body) - try: # Parse the JSON string to a Python object nodes_data = json.loads(sal_body) - total_nodes = len(nodes_data) # Get the total number of nodes + # Check if there is any error in SAL's reply body + if 'key' in nodes_data and any(keyword in nodes_data['key'].lower() for keyword in ['error', 'exception']): + print("Error found in message body:", nodes_data['message']) + sal_reply_body = [] + else: # No error found in SAL's reply body + total_nodes = len(nodes_data) # Get the total number of nodes + print("Total Nodes in SAL's reply:", total_nodes) - # Check if more than 58 nodes exist - if total_nodes > 400: - print("More than 58 nodes exist. Only the first 51 nodes will be processed.") - # Filter to only include the first 51 nodes and convert back to JSON string - sal_reply_body = json.dumps(nodes_data[:400]) - else: - print(f"Total {total_nodes} nodes found. Processing all nodes.") - # Keep sal_reply_body as is since it's already a JSON string - sal_reply_body = sal_body + if total_nodes > 400: # Check if more than 400 nodes received + print("More than 400 nodes returned from SAL.") + # Filter to only include the first 400 nodes and convert back to JSON string + sal_reply_body = json.dumps(nodes_data[:400]) + elif total_nodes > 0 and total_nodes <= 400: + print(f"Total {total_nodes} nodes returned from SAL. Processing all nodes.") + # Keep sal_reply_body as is since it's already a JSON string + sal_reply_body = sal_body + else: + print(f"Total {total_nodes} nodes returned from SAL.") + sal_reply_body = [] except json.JSONDecodeError as e: - print(f"Error parsing JSON: {e}") - sal_reply_body = "[]" # Default to an empty JSON array as a string in case of error + print(f"Error parsing JSON reply from SAL: {e}") + sal_reply_body = [] # Default to an empty JSON array as a string in case of error - if sal_reply_body: # Check whether SAL's reply body is empty - # logging.info(f"Whole reply Received from SAL: {sal_reply}") + if sal_reply_body: # Check whether SAL's reply body is empty + # logging.info(f"Reply Received from SAL: {sal_reply}") # print("SAL reply Body:", sal_reply_body) - # Search for application_id, Read JSON and create data to pass to Evaluation - if check_json_file_exists(application_id_optimizer): # Application JSON exist in DB - print(f"JSON file for application ID {application_id_optimizer} exists.") - node_ids = extract_SAL_node_candidate_data(sal_reply)[2] # 0,1,2nd Position returns the function - # node_ids = ['8a7482868df473cc018df47d8ea60003', '8a7482868df473cc018df47d8fc70005', '8a7482868df473cc018df47d90e70007', '8a7482868df473cc018df47d92090009', '8a7482868df473cc018df47d9326000b', '8a7482868df473cc018df47d9445000d', '8a7482868df473cc018df47d957f000f', '8a7482868df473cc018df47d96a50011', '8a7482868df473cc018df47d97c70013', '8a7482868df473cc018df47d98e30015'] - # print("node_ids_SAL:", node_ids_SAL) - - # Check if there is any difference in available nodes between saved data in DB and SAL's reply - data_table, relative_wr_data, immediate_wr_data, node_names = read_application_data(application_id_optimizer, node_ids) - if not node_names: - node_names = node_ids - print("data_table filtered from DB:", data_table) - print("node_ids filtered from DB:", node_ids) - print("node_names filtered from DB:", node_names) - - # I need to use the most updated data for nodes sent from SAL, - # I can modify the function to retrieve only WR info but there is a problem if other criteria are used - # Maybe I have to use the new data only for the criteria with data coming from SAL and the saved ones for the - # rest criteria - # In case a new node sent from SAL which I have not data saved, then do not consider it if also other crieria - # exist rather than the ones - - else: # Application JSON does not exist in DB - print(f"JSON file for application ID {application_id_optimizer} does not exist.") - # Read data from SAL's response by calling the function extract_node_candidate_data() - # extracted_data, number_of_nodes, node_ids, node_names = extract_node_candidate_data('SAL_Response_11EdgeDevs.json') - extracted_data, number_of_nodes, node_ids, node_names = extract_SAL_node_candidate_data(sal_reply_body) - # print("extracted_data:", extracted_data) - print("node_ids:", node_ids) - - # Use the create_criteria_mapping() to get the criteria mappings - # selected_criteria = ["Operating cost", "Memory Price", "Number of CPU Cores", "Memory Size", "Storage Capacity"] - selected_criteria = ["Number of CPU Cores", "Memory Size"] - field_mapping = create_criteria_mapping(selected_criteria, extracted_data) - # Create data_table: - data_table = create_data_table(selected_criteria, extracted_data, field_mapping) - relative_wr_data = [] - immediate_wr_data = [] - print("created_data_table:", data_table) - # Check the number of nodes before Evaluation - print("There are " + str(len(node_ids)) + " elements in node_ids") + if total_nodes > 1: + # Search for application_id, Read JSON and create data to pass to Evaluation + if check_json_file_exists(application_id_optimizer): # Application JSON exist in DB + print(f"JSON file for application ID {application_id_optimizer} exists.") + # Check if there are differences in available nodes between saved data in JSON file and SAL's reply + data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = read_application_data(application_id_optimizer, sal_reply_body) + # print("sal_reply_body:", sal_reply_body) + # print("data_table filtered from JSON and SAL:", data_table) + # print("node_ids filtered from JSON and SAL:", node_ids) + # print("relative_wr_data:", relative_wr_data) + # print("immediate_wr_data:", immediate_wr_data) + # print("node_names filtered from JSON and SAL:", node_names) - ## Run evaluation - evaluation_results = perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_names, node_ids) - # print("Evaluation Results:", evaluation_results) + else: # Application does not exist in directory + print(f"JSON file for application ID {application_id_optimizer} does not exist.") + # Read data from SAL's response by calling the function extract_node_candidate_data() + # extracted_data_SAL, node_ids, node_names = extract_node_candidate_data('SAL_Response_11EdgeDevs.json') + extracted_data_SAL, node_ids, node_names = extract_SAL_node_candidate_data(sal_reply_body) + # print("extracted_data_SAL:", extracted_data_SAL) + # print("node_ids:", node_ids) - ## Extract and save the results - # ScoresAndRanks = evaluation_results['results'] - ScoresAndRanks = evaluation_results.get('results', []) - print("Scores and Ranks:", ScoresAndRanks) + # Use the create_criteria_mapping() to get the criteria mappings + # selected_criteria = ["Operating cost", "Memory Price", "Number of CPU Cores", "Memory Size", "Storage Capacity"] + selected_criteria = ["Number of CPU Cores", "Memory Size"] + field_mapping = create_criteria_mapping() + # Create data_table: + data_table = create_data_table(selected_criteria, extracted_data_SAL, field_mapping) + relative_wr_data = [] + immediate_wr_data = [] + # print("created_data_table:", data_table) - # Append the Score and Rank of each node to SAL's Response - SAL_and_Scores_Body = append_evaluation_results(sal_reply_body, ScoresAndRanks) - # SAL_and_Scores_Body = append_evaluation_results('SAL_Response_11EdgeDevs.json', ScoresAndRanks) - # print("SAL_and_Scores_Body:", SAL_and_Scores_Body) + # Check the number of nodes before Evaluation + print("There are " + str(len(node_ids)) + " nodes for Evaluation") + + ## Run evaluation + evaluation_results = perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_names, node_ids) + # print("Evaluation Results:", evaluation_results) + + ## Extract and save the results + # ScoresAndRanks = evaluation_results['results'] + ScoresAndRanks = evaluation_results.get('results', []) + # print("Scores and Ranks:", ScoresAndRanks) + + # Append the Score and Rank of each node to SAL's Response + SAL_and_Scores_Body = append_evaluation_results(sal_reply_body, ScoresAndRanks) + # print("SAL_and_Scores_Body:", SAL_and_Scores_Body) + else: + print("There is only one node!") + # Append the Score and Rank of each node to SAL's Response + SAL_and_Scores_Body = append_evaluation_results(sal_reply_body, []) ## Prepare message to be sent to OPTIMIZER # CFSBResponse = read_dummy_response_data_toOpt('CFSB_Body_Response.json') # Data and Scores for 5 Nodes - CFSBResponse = { - "metaData": {"user": "admin"}, - "body": SAL_and_Scores_Body + "metaData": {"user": "admin"}, + "body": SAL_and_Scores_Body } - print("CFSBResponse:", CFSBResponse) + # print("CFSBResponse:", CFSBResponse) + # Writing the formatted JSON to a json file formatted_json = json.dumps(CFSBResponse, indent=4) - # Writing the formatted JSON to a file named test.json with open('CFSBResponse.json', 'w') as file: file.write(formatted_json) print("Formatted JSON has been saved to CFSBResponse.json") - else: # Then SAL's reply body is empty send an empty body to Optimizer + else: # Then SAL's reply body is empty send an empty body to Optimizer print("No Body in reply from SAL!") # Send [] to Optimizer CFSBResponse = { @@ -248,6 +206,14 @@ class SyncedHandler(Handler): logging.error(f"Failed to parse message body from Optimizer as JSON: {e}") + + def requestSAL(self, RequestToSal): + sal_reply = Context.publishers['SAL-GET'].send_sync(RequestToSal) + # Process SAL's Reply + sal_body = sal_reply.get('body') # Get the 'body' as a JSON string + # print("sal_body requestSAL function:", sal_body) + return sal_body + class Bootstrap(ConnectorHandler): context = None def ready(self, context: Context): @@ -279,6 +245,13 @@ def start_exn_connector_in_background(): thread.daemon = True # Daemon threads will shut down immediately when the program exits thread.start() + +def call_publisher(body): + handler = SyncedHandler() + request = handler.requestSAL(body) + return request + + # Used to read dummy response and send to Optimizer using JSON # I have already sent to Optimizer using this function def read_dummy_response_data_toOpt(file_path): @@ -291,6 +264,7 @@ def read_dummy_response_data_toOpt(file_path): } return encapsulated_data + def is_json(myjson): try: json_object = json.loads(myjson) diff --git a/cfsb-backend/app.py b/cfsb-backend/app.py index 3458925..0bb3afe 100644 --- a/cfsb-backend/app.py +++ b/cfsb-backend/app.py @@ -1,11 +1,7 @@ from app_factory import create_app -from dotenv import load_dotenv from activemq import start_exn_connector_in_background -from activemqOLD import start_exn_connector_in_background1 from app_factory import create_app # Import your Flask app factory -load_dotenv() - app = create_app() # Start the EXN connector in the background start_exn_connector_in_background() diff --git a/cfsb-backend/docker-compose.yml b/cfsb-backend/docker-compose.yml deleted file mode 100644 index 31712b6..0000000 --- a/cfsb-backend/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '3.0' -services: - web: - build: - context: . - dockerfile: Dockerfile - ports: - - "8001:8001" - env_file: - - .env.prod - depends_on: - - db - db: - image: postgres:16 - ports: - - "5432:5432" - environment: - - POSTGRES_USER=dbuser - - POSTGRES_PASSWORD=pass123 - - POSTGRES_DB=fog_broker - volumes: - - postgres_data:/var/lib/postgresql/data/ - - ./db/db_script.sql:/docker-entrypoint-initdb.d/db_script.sql - -volumes: - postgres_data: diff --git a/cfsb-backend/edge_data_ipat.json b/cfsb-backend/edge_data_ipat.json new file mode 100644 index 0000000..d2feae8 --- /dev/null +++ b/cfsb-backend/edge_data_ipat.json @@ -0,0 +1,80 @@ +{ + "_id": "b4ce322c-698a-43b9-a889-bf0da2a4dcb9", + "os": "LINUX", + "name": "Test VM #0001", + "owner": "admin", + "ipAddress": "10.10.0.6", + "location": { + "name": "laptop", + "latitude": 12.345, + "longitude": 56.789 + }, + "username": "ubuntu", + "password": [ + "u", + "b", + "u", + "n", + "t", + "u" + ], + "publicKey": [], + "deviceInfo": { + "CPU_SOCKETS": "1", + "CPU_CORES": "10", + "CPU_PROCESSORS": "20", + "RAM_TOTAL_KB": "16218480", + "RAM_AVAILABLE_KB": "13366788", + "RAM_FREE_KB": "10943372", + "RAM_USED_KB": "5275108", + "RAM_UTILIZATION": "32.5253", + "DISK_TOTAL_KB": "1055762868", + "DISK_FREE_KB": "976527612", + "DISK_USED_KB": "79235256", + "DISK_UTILIZATION": "7.50502", + "OS_ARCHITECTURE": "x86_64", + "OS_KERNEL": "Linux", + "OS_KERNEL_RELEASE": "5.15.133.1-microsoft-standard-WSL2" + }, + "requestId": "eb6441fc-613a-482e-ba94-b16db57ecd36", + "creationDate": "2024-01-15T13:23:40.602Z", + "lastUpdateDate": "2024-01-15T14:32:43.485Z", + "status": "HEALTHY", + "nodeReference": "40ed1989-49ba-4496-a5c5-3d8ca1a18972", + "messages": [], + "statusUpdate": { + "ipAddress": "10.10.0.6", + "clientId": "VM-LINUX-TEST-VM-0001-Test VM #0001-DEFAULT-10.10.0.6-_", + "state": "REGISTERED", + "stateLastUpdate": "2024-01-15T13:23:47.463Z", + "reference": "40ed1989-49ba-4496-a5c5-3d8ca1a18972", + "errors": [] + }, + "metrics": { + "ipAddress": "10.10.0.6", + "clientId": "VM-LINUX-TEST-VM-0001-Test VM", + "timestamp": "2024-01-15T14:32:33.467Z", + "metrics": { + "count-total-events-failures": 0, + "count-total-events-text": 0, + "tx": 0, + "count-total-events-other": 0, + "count-event-forwards-success": 0, + "count-event-forwards-failure": 0, + "rx": 0, + "count-total-events": 0, + "cpu": 0.6, + "uptime": 10742, + "count-event-local-publish-failure": 0, + "count-total-events-object": 0, + "disk": 2.48262, + "count-event-local-publish-success": 0, + "updatetime": 1705318391, + "currdatetime": 1705329133, + "ram": 23.7719 + }, + "latestEvents": [] + }, + "retries": 0, + "_class": "eu.nebulous.resource.discovery.monitor.model.Device" +} diff --git a/cfsb-backend/routes.py b/cfsb-backend/routes.py index d55d3ba..ea24e58 100644 --- a/cfsb-backend/routes.py +++ b/cfsb-backend/routes.py @@ -7,9 +7,11 @@ from data_types import get_attr_data_type import db.db_functions as db_functions import os import time +import get_data as file import activemq -# from activemq import connector_handler import traceback +import logging +# logging.disable(logging.CRITICAL) main_routes = Blueprint('main', __name__) @@ -19,6 +21,74 @@ NoData_Variables = ['attr-security', 'attr-performance-capacity', 'attr-performa Cont_Variables = ['attr-performance', 'attr-financial', 'attr-performance-capacity-memory', 'attr-performance-capacity-memory-speed'] +dummy_node_data = { + "id": "8a7481d98e702b64018e702cbe070000", + "nodeCandidateType": "EDGE", + "jobIdForByon": "", + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": "", + "cloudType": "EDGE", + "api": "", + "credential": "", + "cloudConfiguration": { + "nodeGroup": "", + "properties": {} + }, + "owner": "EDGE", + "state": "", + "diagnostic": "" + }, + "location": { + "id": "edge-location-KmVf4xDJKL7acBGc", + "name": "", + "providerId": "", + "locationScope": "", + "isAssignable": "", + "geoLocation": { + "city": "Warsaw", + "country": "Poland", + "latitude": 52.237049, + "longitude": 21.017532 + }, + "parent": "", + "state": "", + "owner": "" + }, + "image": { + "id": "edge-image-KmVf4xDJKL7acBGc", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": "", + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.00 + }, + "location": "", + "state": "", + "owner": "" + }, + "hardware": { + "id": "edge-hardware-KmVf4xDJKL7acBGc", + "name": "", + "providerId": "", + "cores": 1, + "ram": 1, + "disk": 1.0, + "fpga": 0, + "location": "", + "state": "", + "owner": "" + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": "", + "environment": "" +} + + #Used in HomePage.vue to save app_id and user_id # @main_routes.route('/save_ids', methods=['POST']) # def save_ids(): @@ -34,7 +104,6 @@ Cont_Variables = ['attr-performance', 'attr-financial', 'attr-performance-capaci #Used in CriteriaSelection.vue @main_routes.route('/get_hierarchical_category_list') def get_hierarchical_category_list(): - # TODO order by title in every level items_list = file.get_level_1_items() # Assume this function returns the list correctly if items_list is not None: # Return the list as a JSON response @@ -48,101 +117,196 @@ def get_hierarchical_category_list(): def process_selected_criteria(): try: data = request.json - # Selected Criteria by the User from the List selected_criteria = data.get('selectedItems', []) - # Extract app_id, user_id - application_id = data.get('app_id') # Take it from local storage from frontend - # application_id = 'd535cf554ea66fbebfc415ac837a5828' #dummy application_id_optimizer - user_id = data.get('user_id') # Take it from local storage from frontend - print("user_id:", user_id) - print("application_id:", application_id) - ## Prepare message to be send to SAL - message_for_SAL = [ # User side so ask SAL for every available node - { - "type": "NodeTypeRequirement", - "nodeTypes": ["IAAS", "PAAS", "FAAS", "BYON", "EDGE", "SIMULATION"] - # "jobIdForEDGE": "FCRnewLight0" - } + # application_id = data.get('app_id') + # user_id = data.get('user_id') + # print("user_id:", user_id) + # print("application_id:", application_id) + + message_for_SAL = [{ + "type": "NodeTypeRequirement", + "nodeTypes": ["IAAS", "PAAS", "FAAS", "BYON", "EDGE", "SIMULATION"]} + # ,{ + # "type": "AttributeRequirement", + # "requirementClass": "hardware", + # "requirementAttribute": "cores", + # "requirementOperator": "GEQ", + # "value": "64" + # }, + # { + # "type": "AttributeRequirement", + # "requirementClass": "hardware", + # "requirementAttribute": "ram", + # "requirementOperator": "GEQ", + # "value": "33000" + # } ] - # Convert the body data to a JSON string - body_json_string = json.dumps(message_for_SAL) + body_json_string_for_SAL = json.dumps(message_for_SAL) - RequestToSal = { # Dictionary - "metaData": {"user": "admin"}, # key [String "metaData"] value [dictionary] - "body": body_json_string # key [String "body"] value [JSON String] + RequestToSal = { + "metaData": {"user": "admin"}, + "body": body_json_string_for_SAL } - print("RequestToSal:", RequestToSal) + # print("RequestToSal:", RequestToSal) - # print("Is RequestToSal a valid dictionary:", isinstance(RequestToSal, dict)) - # print("Is the 'body' string in RequestToSal a valid JSON string:", is_json(RequestToSal["body"])) + sal_reply = activemq.call_publisher(RequestToSal) + nodes_data = json.loads(sal_reply) if isinstance(sal_reply, str) else sal_reply + # print("nodes_data", nodes_data) - ## Request the node candidates from SAL - # sal_reply = activemq.context.publishers['SAL-GET'].send_sync(RequestToSal) + extracted_data, node_ids, node_names = extract_SAL_node_candidate_data_Front(nodes_data) + # print("extracted_data:", extracted_data) + field_mapping = create_criteria_mapping() + # print("field_mapping", field_mapping) - ## Process SAL's Reply - # extracted_data, number_of_nodes, node_ids, node_names = extract_SAL_node_candidate_data(sal_reply) - # extracted_data, number_of_nodes, node_names = extract_node_candidate_data('dummy_data_node_candidates.json') - extracted_data, number_of_nodes, node_ids, node_names = extract_node_candidate_data('SAL_Response_11EdgeDevs.json') - print("extracted_data:", extracted_data) + default_list_criteria_mapping = { + # "Cost": "price", + "Operating cost": "price", + "Memory Price": "memoryPrice", + "Number of CPU Cores": "cores", + "Memory Size": "ram", + "Storage Capacity": "disk" + } - # Use the create_criteria_mapping() to get the criteria mappings - field_mapping = create_criteria_mapping(selected_criteria, extracted_data) - grid_data = {name: [] for name in node_names} + grid_data = {} - # Prepare the data to be sent to DataGrid.vue - # Blank by default for the Selected Criteria not found in mapping for node_data in extracted_data: - node_name = node_data.get('name') # Using name to match - node_id = node_data.get('id') # Extract the node ID - grid_data[node_name] = {"id": node_id, "criteria": []} + node_id = node_data.get('id') + # print("Before create_node_name") + node_name = create_node_name(node_data) if node_data else "Unknown" + # print("After create_node_name") - if node_name in grid_data: # Check if node_name exists in grid_data keys - for item in selected_criteria: - criterion_data = {} - criterion_data["data_type"] = get_attr_data_type(item) - item_data_dict = file.get_subject_data(file.SMI_prefix + item) - criterion_data["title"] = item_data_dict["title"] - field_name = field_mapping.get(criterion_data["title"], item) + if node_id and node_id not in grid_data: + grid_data[node_id] = {"name": node_name, "criteria": []} - # Check if the field_name is a direct key or nested inside 'hardware' - if field_name in node_data: - value = node_data[field_name] - elif 'hardware' in node_data and field_name in node_data['hardware']: - value = node_data['hardware'][field_name] + hardware_info = node_data.get('hardware', {}) # contains the values for criteria coming from SAL + + for criterion_key in selected_criteria: + # print("criterion_key:", criterion_key) + criterion_info = file.get_subject_data(file.SMI_prefix + criterion_key) # It contains the titles of the criteria + # print("criterion_info:", criterion_info) + + # Resolve title and then map title to field name + criterion_data_type = get_attr_data_type(criterion_key) # criterion_data_type: {'type': 1, 'values': ['Low', 'Medium', 'High']} + # print("criterion_data_type:", criterion_data_type) + criterion_title = criterion_info["title"] + + # Fetch the values of the selected default criteria + if criterion_title in default_list_criteria_mapping: + SAL_criterion_name = field_mapping.get(criterion_title) # Map the criterion title with the criterion name in SAL's reply + value = hardware_info.get(SAL_criterion_name, "N/A") # Get the criterion values + else: + # Handle other criteria (this part may need adjustment based on your actual data structure) + # value = "N/A" # Placeholder for the logic to determine non-default criteria values + # Generate random or default values for rest criteria + type_value = criterion_data_type['type'] + # print("type_value:", type_value) + + if type_value == 1: + value = random.choice(["High", "Medium", "Low"]) + elif type_value == 5: + value = random.choice(["True", "False"]) else: - # Generate random or default values for unmapped criteria or missing data - item_data_type_value = criterion_data["data_type"].get('type') - if item_data_type_value == 1: - value = random.choice(["High", "Medium", "Low"]) - elif item_data_type_value == 5: - value = random.choice(["True", "False"]) - else: - value = round(random.uniform(1, 100), 2) + value = round(random.uniform(1, 100), 2) - criterion_data["value"] = value if value != 0 else 0.00001 - # grid_data[node_id].append(criterion_data) - # grid_data[node_name].append(criterion_data) # Use node_name as key - grid_data[node_name]["criteria"].append(criterion_data) + criterion_data = { + "title": criterion_title, + "value": value, + "data_type": criterion_data_type # criterion_data_type: {'type': 1, 'values': ['Low', 'Medium', 'High']} + } + grid_data[node_id]["criteria"].append(criterion_data) - # Conversion to list format remains unchanged - # grid_data_with_names = [{'name': name, 'criteria': data} for name, data in grid_data.items()] - grid_data_with_names = [{'name': name, 'id': data["id"], 'criteria': data["criteria"]} for name, data in grid_data.items()] - print("grid_data_with_names:", grid_data_with_names) + grid_data_with_names = [{ + 'name': data["name"], + 'id': node_id, + 'criteria': data["criteria"] + } for node_id, data in grid_data.items()] + # print("grid_data_with_names:", grid_data_with_names) - # Send the comprehensive grid_data_with_names to the frontend return jsonify({ 'success': True, 'gridData': grid_data_with_names, 'NodeNames': node_names }) + except Exception as e: print(f"Error processing selected items: {e}") - traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 -# Used in WR.vue +# Works by reading a JSON file with dummy data +# def process_selected_criteria(): +# try: +# data = request.json +# # Selected Criteria by the User from the List +# selected_criteria = data.get('selectedItems', []) +# # Extract app_id, user_id +# application_id = data.get('app_id') # Take it from local storage from frontend +# # application_id = 'd535cf554ea66fbebfc415ac837a5828' #dummy application_id_optimizer +# user_id = data.get('user_id') # Take it from local storage from frontend +# print("user_id:", user_id) +# print("application_id:", application_id) +# +# ## Process SAL's Reply +# # extracted_data, number_of_nodes, node_names = extract_node_candidate_data('dummy_data_node_candidates.json') +# extracted_data, node_ids, node_names = extract_node_candidate_data('SAL_Response_11EdgeDevs.json') +# print("extracted_data:", extracted_data) +# +# # Use the create_criteria_mapping() to get the criteria mappings +# field_mapping = create_criteria_mapping(selected_criteria, extracted_data) +# grid_data = {name: [] for name in node_names} +# +# # Prepare the data to be sent to DataGrid.vue +# for node_data in extracted_data: +# node_name = node_data.get('name') # Using name to match +# node_id = node_data.get('id') # Extract the node ID +# grid_data[node_name] = {"id": node_id, "criteria": []} +# +# if node_name in grid_data: # Check if node_name exists in grid_data keys +# for item in selected_criteria: +# criterion_data = {} +# criterion_data["data_type"] = get_attr_data_type(item) +# item_data_dict = file.get_subject_data(file.SMI_prefix + item) +# criterion_data["title"] = item_data_dict["title"] +# field_name = field_mapping.get(criterion_data["title"], item) +# +# # Check if the field_name is a direct key or nested inside 'hardware' +# if field_name in node_data: +# value = node_data[field_name] +# elif 'hardware' in node_data and field_name in node_data['hardware']: +# value = node_data['hardware'][field_name] +# else: +# # Generate random or default values for unmapped criteria or missing data +# item_data_type_value = criterion_data["data_type"].get('type') +# if item_data_type_value == 1: +# value = random.choice(["High", "Medium", "Low"]) +# elif item_data_type_value == 5: +# value = random.choice(["True", "False"]) +# else: +# value = round(random.uniform(1, 100), 2) +# +# criterion_data["value"] = value if value != 0 else 0.00001 +# # grid_data[node_id].append(criterion_data) +# grid_data[node_name]["criteria"].append(criterion_data) +# +# # Conversion to list format remains unchanged +# # grid_data_with_names = [{'name': name, 'criteria': data} for name, data in grid_data.items()] +# grid_data_with_names = [{'name': name, 'id': data["id"], 'criteria': data["criteria"]} for name, data in grid_data.items()] +# print("grid_data_with_names:", grid_data_with_names) +# +# # Send the comprehensive grid_data_with_names to the frontend +# return jsonify({ +# 'success': True, +# 'gridData': grid_data_with_names, +# 'NodeNames': node_names +# }) +# except Exception as e: +# print(f"Error processing selected items: {e}") +# traceback.print_exc() +# return jsonify({'success': False, 'error': str(e)}), 500 + + + @main_routes.route('/process-evaluation-data', methods=['POST']) def process_evaluation_data(): try: @@ -150,14 +314,15 @@ def process_evaluation_data(): if data is None: raise ValueError("Received data is not in JSON format or 'Content-Type' header is not set to 'application/json'") - print("JSON data:", data) + # print("JSON in process_evaluation_data:", data) # Transform grid data to table and get node names directly from the function data_table, relative_wr_data, immediate_wr_data, node_names, node_ids = transform_grid_data_to_table(data) - # print("data_table:", data_table) + # print("data_table FRONT:", data_table) # print("relative_wr_data:", relative_wr_data) # print("immediate_wr_data:", immediate_wr_data) - # print("node_names:", node_names) + # print("# node_names:", len(node_names)) + # print("# node_ids:", len(node_ids)) # Run Optimization - Perform evaluation results = perform_evaluation(data_table, relative_wr_data, immediate_wr_data, node_names, node_ids) diff --git a/cfsb-backend/test.py b/cfsb-backend/test.py new file mode 100644 index 0000000..e2f8d98 --- /dev/null +++ b/cfsb-backend/test.py @@ -0,0 +1,140 @@ +# ActiveMQ communication logic via EXN library +import sys +import threading +import json +import time +sys.path.insert(0,'../exn') +import logging +from dotenv import load_dotenv +load_dotenv() +from proton import Message +from exn import core +from exn.connector import EXN +from exn.core.consumer import Consumer +from exn.core.synced_publisher import SyncedPublisher +from exn.core.publisher import Publisher +from exn.core.context import Context +from exn.core.handler import Handler +from exn.handler.connector_handler import ConnectorHandler +from User_Functions import * + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logging.getLogger('exn.connector').setLevel(logging.DEBUG) + + +class SyncedHandler(Handler): + def on_message(self, key, address, body, message: Message, context=None): + logging.info(f"[SyncedHandler] Received {key} => {address}: {body}") + logging.info("on_message in SyncedHandler is executed") + logging.info(f"[body] {body}") + + # Triggered by OPTIMIZER, Get app id, correlation id and filters + # if address == "topic://eu.nebulouscloud.cfsb.get_node_candidates": + if key == "OPT-triggering": + logging.info("Entered in OPT-triggering Key") + + # Save the correlation_id (We do not have it from the app_side) + # Optimizer_correlation_id = '88334290cad34ad9b21eb468a9f8ff11' # dummy correlation_id + Optimizer_correlation_id = message.correlation_id + logging.info(f"Optimizer_correlation_id {message.correlation_id}") + application_id = message.subject # can be taken also from message.annotations.application + + try: + opt_message_data = body + print("Message from Optimizer:", opt_message_data) + + # Extract 'body' from opt_message_data + # opt_body_data = opt_message_data.get('body', {}) + opt_body_data =[ + { + "type": "NodeTypeRequirement", + "nodeTypes": ["EDGE"], + "jobIdForEDGE": "FCRnewLight0" + } + ] + logging.info(opt_body_data) + print("Extracted body from Optim Message:", opt_body_data) + + ## Prepare message to be send to SAL + RequestToSal = { + "metaData": {"user": "admin"}, + "body": opt_body_data + } + print("RequestToSal:", RequestToSal) + + # Convert the Python structure to a JSON string + # RequestToSal = json.dumps(RequestToSal) + + # Request the node candidates from SAL + sal_reply = context.publishers['SAL-GET'].send_sync(RequestToSal, application_id, + properties={'correlation_id': Optimizer_correlation_id}, raw=False) + # sal_reply = context.publishers['SAL-GET'].send_sync(RequestToSal, application_id) + if sal_reply: + logging.info(f"Received reply from SAL: {sal_reply}") + print("SAL reply:", sal_reply) + else: + print("No reply from SAL!") + + ## Prepare message to be sent to OPTIMIZER + CFSBResponse = read_dummy_response_data_toOpt('CFSB_Body_Response.json') + + # SAL_and_Scores_Body = Give me a short example + # Encapsulate the data within the "body" structure + # CFSBResponse = { + # "metaData": {"user": "admin"}, + # "body": SAL_and_Scores_Body + # } + # print("CFSBResponse:", CFSBResponse) + + # Send message to Optimizer + context.get_publisher('SendToOPT').send(CFSBResponse, application_id) + # context.publishers['SendToOPT'].send(CFSBResponse, application_id, properties={ + # 'correlation_id': Optimizer_correlation_id}, raw=True) + + except json.JSONDecodeError as e: + logging.error(f"Failed to parse message body from Optimizer as JSON: {e}") + +class Bootstrap(ConnectorHandler): + context = None + def ready(self, context: Context): + self.context = context + + +def start_exn_connector_in_background(): + def run_connector(): + # eu.nebulouscloud.exn.sal.nodecandidate.* + addressSAL_GET = 'eu.nebulouscloud.exn.sal.nodecandidate.get' + + addressSAL_GET_REPLY = 'eu.nebulouscloud.exn.sal.nodecandidate.get.reply' + addressOPTtriggering = 'eu.nebulouscloud.cfsb.get_node_candidates' + addressSendToOPT = 'eu.nebulouscloud.cfsb.get_node_candidates.reply' + + connector = EXN('ui', url="localhost", port=5672, username="admin", password="admin", + handler=Bootstrap(), + publishers=[ + SyncedPublisher('SAL-GET', addressSAL_GET, True, True), + core.publisher.Publisher('SendToOPT', addressSendToOPT, True, True) + ], + consumers=[ + # Consumer('SAL-GET-REPLY', addressSAL_GET, handler=SyncedHandler(), topic=True, fqdn=True), + Consumer('OPT-triggering', addressOPTtriggering, handler=SyncedHandler(), topic=True, fqdn=True) + ]) + connector.start() + + # Start the EXN connector in a separate thread + thread = threading.Thread(target=run_connector) + thread.daemon = True # Daemon threads will shut down immediately when the program exits + thread.start() + + +# Used to read dummy JSON and send to Optimizer +def read_dummy_response_data_toOpt(file_path): + with open(file_path, 'r') as file: + data = json.load(file) + # Encapsulating the data within the "body" structure + encapsulated_data = { + "metaData": {"user": "admin"}, + "body": data + } + return encapsulated_data + diff --git a/cfsb-backend/updated_SALs_JSON.json b/cfsb-backend/updated_SALs_JSON.json new file mode 100644 index 0000000..666abd6 --- /dev/null +++ b/cfsb-backend/updated_SALs_JSON.json @@ -0,0 +1,1256 @@ +[ + { + "name": "test0", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.0" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "8", + "numberOfCores": 10, + "memory": 4, + "disk": 28.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Warsaw", + "country": "Poland", + "latitude": 68.6427, + "longitude": 81.24341 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d8d770000", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-bzzM75gJBIF9ciHt", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Warsaw", + "country": "Poland", + "latitude": 68.6427, + "longitude": 81.24341 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-bzzM75gJBIF9ciHt", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-bzzM75gJBIF9ciHt", + "name": null, + "providerId": null, + "cores": 10, + "ram": 4, + "disk": 28.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d8d7d0001", + "Score": 0.99999, + "Rank": 1 + }, + { + "name": "AWS-Raspberry-Madrid", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.1" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "15", + "numberOfCores": 6, + "memory": 7, + "disk": 11.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": -31.018383, + "longitude": -26.717098 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d8ea20002", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-Ehjjsq21EsRtesOu", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": -31.018383, + "longitude": -26.717098 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-Ehjjsq21EsRtesOu", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-Ehjjsq21EsRtesOu", + "name": null, + "providerId": null, + "cores": 6, + "ram": 7, + "disk": 11.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d8ea60003", + "Score": 0.99999, + "Rank": 1 + }, + { + "name": "test2", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.2" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "10", + "numberOfCores": 1, + "memory": 6, + "disk": 28.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -72.342804, + "longitude": -17.023972 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d8fc40004", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-sRI0g25q5kbxZYzK", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -72.342804, + "longitude": -17.023972 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-sRI0g25q5kbxZYzK", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-sRI0g25q5kbxZYzK", + "name": null, + "providerId": null, + "cores": 1, + "ram": 6, + "disk": 28.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d8fc70005", + "Score": 0.6666566666666667, + "Rank": 9 + }, + { + "name": "test3", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.3" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "13", + "numberOfCores": 13, + "memory": 6, + "disk": 16.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -18.325798, + "longitude": -2.8190002 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d90e40006", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-WeVLDpisE2wPtRxB", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -18.325798, + "longitude": -2.8190002 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-WeVLDpisE2wPtRxB", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-WeVLDpisE2wPtRxB", + "name": null, + "providerId": null, + "cores": 13, + "ram": 6, + "disk": 16.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d90e70007", + "Score": 1.0, + "Rank": 1 + }, + { + "name": "test4", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.4" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "15", + "numberOfCores": 1, + "memory": 8, + "disk": 51.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": -45.349804, + "longitude": 15.921776 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d92040008", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-40xmq8oI0kvATuAY", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": -45.349804, + "longitude": 15.921776 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-40xmq8oI0kvATuAY", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-40xmq8oI0kvATuAY", + "name": null, + "providerId": null, + "cores": 1, + "ram": 8, + "disk": 51.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d92090009", + "Score": 0.99999, + "Rank": 1 + }, + { + "name": "test5", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.5" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "12", + "numberOfCores": 2, + "memory": 11, + "disk": 79.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Warsaw", + "country": "Poland", + "latitude": 75.87384, + "longitude": -29.064651 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d9320000a", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-SX9MUIDWB80wCnHi", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Warsaw", + "country": "Poland", + "latitude": 75.87384, + "longitude": -29.064651 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-SX9MUIDWB80wCnHi", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-SX9MUIDWB80wCnHi", + "name": null, + "providerId": null, + "cores": 2, + "ram": 11, + "disk": 79.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d9326000b", + "Score": 0.99999, + "Rank": 1 + }, + { + "name": "test6", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.6" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "6", + "numberOfCores": 6, + "memory": 1, + "disk": 32.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -89.065475, + "longitude": 53.526062 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d9441000c", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-2NId1nrSANd43V6J", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -89.065475, + "longitude": 53.526062 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-2NId1nrSANd43V6J", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-2NId1nrSANd43V6J", + "name": null, + "providerId": null, + "cores": 6, + "ram": 1, + "disk": 32.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d9445000d", + "Score": 0.33333333333333337, + "Rank": 10 + }, + { + "name": "test7", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.7" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "11", + "numberOfCores": 3, + "memory": 4, + "disk": 93.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": 78.6129, + "longitude": -52.693478 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d957c000e", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-69VJ8eBysHcIB2XU", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": 78.6129, + "longitude": -52.693478 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-69VJ8eBysHcIB2XU", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-69VJ8eBysHcIB2XU", + "name": null, + "providerId": null, + "cores": 3, + "ram": 4, + "disk": 93.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d957f000f", + "Score": 0.99999, + "Rank": 1 + }, + { + "name": "test8", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.8" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "9", + "numberOfCores": 11, + "memory": 14, + "disk": 98.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -44.904438, + "longitude": 50.77269 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d96a20010", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-bf3bxPhgBqe20gOB", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": -44.904438, + "longitude": 50.77269 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-bf3bxPhgBqe20gOB", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-bf3bxPhgBqe20gOB", + "name": null, + "providerId": null, + "cores": 11, + "ram": 14, + "disk": 98.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d96a50011", + "Score": 0.33333333333333337, + "Rank": 10 + }, + { + "name": "test9", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.9" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "16", + "numberOfCores": 4, + "memory": 13, + "disk": 8.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": -24.562317, + "longitude": 26.578308 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d97c20012", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-3Hi1Qjr3nwDhHVKL", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Athens", + "country": "Greece", + "latitude": -24.562317, + "longitude": 26.578308 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-3Hi1Qjr3nwDhHVKL", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-3Hi1Qjr3nwDhHVKL", + "name": null, + "providerId": null, + "cores": 4, + "ram": 13, + "disk": 8.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d97c70013", + "Score": 0.99998, + "Rank": 1 + }, + { + "name": "test10", + "loginCredential": { + "username": "ubuntu", + "password": "ProActive123", + "privateKey": "" + }, + "ipAddresses": [ + { + "IpAddressType": "PUBLIC_IP", + "IpVersion": "V4", + "value": "10.10.10.10" + }, + { + "IpAddressType": "PRIVATE_IP", + "IpVersion": "V4", + "value": "18.203.158.193" + } + ], + "nodeProperties": { + "providerId": "20", + "numberOfCores": 8, + "memory": 1, + "disk": 13.0, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": 26.856018, + "longitude": 0.13977814 + } + }, + "reason": null, + "diagnostic": null, + "userId": null, + "allocated": null, + "jobId": "FCRnewLight0", + "systemArch": "ARMv8", + "scriptURL": "https://www.google.com", + "jarURL": "https://www.activeeon.com/public_content/7cde3381417ff3784639dc41fa7e7cd0544a5234-morphemic-7bulls/node_13.1.0-SNAPSHOT_armv8.jar", + "nodeCandidate": { + "id": "8a7482868df473cc018df47d98e00014", + "nodeCandidateType": "EDGE", + "jobIdForByon": null, + "jobIdForEdge": "FCRnewLight0", + "price": 0.0, + "cloud": { + "id": "edge", + "endpoint": null, + "cloudType": "EDGE", + "api": null, + "credential": null, + "cloudConfiguration": { + "nodeGroup": null, + "properties": {} + }, + "owner": "EDGE", + "state": null, + "diagnostic": null + }, + "location": { + "id": "edge-location-rY42bNpWUlA7RHm7", + "name": null, + "providerId": null, + "locationScope": null, + "isAssignable": null, + "geoLocation": { + "city": "Nice", + "country": "France", + "latitude": 26.856018, + "longitude": 0.13977814 + }, + "parent": null, + "state": null, + "owner": null + }, + "image": { + "id": "edge-image-rY42bNpWUlA7RHm7", + "name": "edge-image-name-UBUNTU-UNKNOWN", + "providerId": null, + "operatingSystem": { + "operatingSystemFamily": "UBUNTU", + "operatingSystemArchitecture": "UNKNOWN", + "operatingSystemVersion": 1804.0 + }, + "location": null, + "state": null, + "owner": null + }, + "hardware": { + "id": "edge-hardware-rY42bNpWUlA7RHm7", + "name": null, + "providerId": null, + "cores": 8, + "ram": 1, + "disk": 13.0, + "fpga": 0, + "location": null, + "state": null, + "owner": null + }, + "pricePerInvocation": 0.0, + "memoryPrice": 0.0, + "nodeId": null, + "environment": null + }, + "id": "8a7482868df473cc018df47d98e30015", + "Score": 1.0, + "Rank": 1 + } +] \ No newline at end of file diff --git a/cfsb-frontend/.env b/cfsb-frontend/.env new file mode 100644 index 0000000..9b3d341 --- /dev/null +++ b/cfsb-frontend/.env @@ -0,0 +1,2 @@ +VUE_APP_BACKEND_URL=http://127.0.0.1:5000 +VITE_BACKEND_URL=http://127.0.0.1:5000 \ No newline at end of file diff --git a/cfsb-frontend/.env.development b/cfsb-frontend/.env.development new file mode 100644 index 0000000..e4e39ec --- /dev/null +++ b/cfsb-frontend/.env.development @@ -0,0 +1,2 @@ +VUE_APP_BACKEND_URL=http://127.0.0.1:8001 +VITE_BACKEND_URL=http://127.0.0.1:8001 \ No newline at end of file diff --git a/cfsb-frontend/.env.production b/cfsb-frontend/.env.production new file mode 100644 index 0000000..e4e39ec --- /dev/null +++ b/cfsb-frontend/.env.production @@ -0,0 +1,2 @@ +VUE_APP_BACKEND_URL=http://127.0.0.1:8001 +VITE_BACKEND_URL=http://127.0.0.1:8001 \ No newline at end of file diff --git a/cfsb-frontend/Dockerfile b/cfsb-frontend/Dockerfile new file mode 100644 index 0000000..a5b5e96 --- /dev/null +++ b/cfsb-frontend/Dockerfile @@ -0,0 +1,22 @@ +# Step 1: Build Stage +FROM node:16 as build-stage + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm install + +COPY . . + +RUN npm run build + +# Step 2: Nginx Stage +FROM docker.io/nginx:alpine + +COPY --from=build-stage /app/dist /usr/share/nginx/html +COPY .env.production /usr/share/nginx/html/.env + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/cfsb-frontend/package-lock.json b/cfsb-frontend/package-lock.json index 2947fa4..26d93cd 100644 --- a/cfsb-frontend/package-lock.json +++ b/cfsb-frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "chart.js": "^4.4.1", + "package.json": "^2.0.1", "vue-router": "^4.0.13" }, "devDependencies": { @@ -679,6 +680,25 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.11.tgz", "integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==" }, + "node_modules/abs": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/abs/-/abs-1.3.14.tgz", + "integrity": "sha512-PrS26IzwKLWwuURpiKl8wRmJ2KdR/azaVrLEBWG/TALwT20Y7qjtYp1qcMLHA4206hBHY5phv3w4pjf9NPv4Vw==", + "dependencies": { + "ul": "^5.0.0" + } + }, + "node_modules/capture-stack-trace": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz", + "integrity": "sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chart.js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", @@ -690,11 +710,67 @@ "pnpm": ">=7" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==", + "dependencies": { + "capture-stack-trace": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deffy": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/deffy/-/deffy-2.2.4.tgz", + "integrity": "sha512-pLc9lsbsWjr6RxmJ2OLyvm+9l4j1yK69h+TML/gUit/t3vTijpkNGh8LioaJYTGO7F25m6HZndADcUOo2PsiUg==", + "dependencies": { + "typpy": "^2.0.0" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/err": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/err/-/err-1.1.1.tgz", + "integrity": "sha512-N97Ybd2jJHVQ+Ft3Q5+C2gM3kgygkdeQmEqbN2z15UTVyyEsIwLA1VK39O1DHEJhXbwIFcJLqm6iARNhFANcQA==", + "dependencies": { + "typpy": "^2.2.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.19.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.9.tgz", @@ -737,6 +813,15 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/exec-limiter": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/exec-limiter/-/exec-limiter-3.2.13.tgz", + "integrity": "sha512-86Ri699bwiHZVBzTzNj8gspqAhCPchg70zPVWIh3qzUOA1pUMcb272Em3LPk8AE0mS95B9yMJhtqF8vFJAn0dA==", + "dependencies": { + "limit-it": "^3.0.0", + "typpy": "^2.1.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -751,6 +836,208 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.name": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/function.name/-/function.name-1.0.13.tgz", + "integrity": "sha512-mVrqdoy5npWZyoXl4DxCeuVF6delDcQjVS9aPdvLYlBxtMTZDR2B5GVEQEoM1jJyspCqg3C0v4ABkLE7tp9xFA==", + "dependencies": { + "noop6": "^1.0.1" + } + }, + "node_modules/git-package-json": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/git-package-json/-/git-package-json-1.4.10.tgz", + "integrity": "sha512-DRAcvbzd2SxGK7w8OgYfvKqhFliT5keX0lmSmVdgScgf1kkl5tbbo7Pam6uYoCa1liOiipKxQZG8quCtGWl/fA==", + "dependencies": { + "deffy": "^2.2.1", + "err": "^1.1.1", + "gry": "^5.0.0", + "normalize-package-data": "^2.3.5", + "oargv": "^3.4.1", + "one-by-one": "^3.1.0", + "r-json": "^1.2.1", + "r-package-json": "^1.0.0", + "tmp": "0.0.28" + } + }, + "node_modules/git-source": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/git-source/-/git-source-1.1.10.tgz", + "integrity": "sha512-XZZ7ZgnLL35oLgM/xjnLYgtlKlxJG0FohC1kWDvGkU7s1VKGXK0pFF/g1itQEwQ3D+uTQzBnzPi8XbqOv7Wc1Q==", + "dependencies": { + "git-url-parse": "^5.0.1" + } + }, + "node_modules/git-up": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", + "integrity": "sha512-SRVN3rOLACva8imc7BFrB6ts5iISWKH1/h/1Z+JZYoUI7UVQM7gQqk4M2yxUENbq2jUUT09NEND5xwP1i7Ktlw==", + "dependencies": { + "is-ssh": "^1.0.0", + "parse-url": "^1.0.0" + } + }, + "node_modules/git-url-parse": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", + "integrity": "sha512-4uSiOgrryNEMBX+gTWogenYRUh2j1D+95STTSEF2RCTgLkfJikl8c7BGr0Bn274hwuxTsbS2/FQ5pVS9FoXegQ==", + "dependencies": { + "git-up": "^1.0.0" + } + }, + "node_modules/got": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-5.6.0.tgz", + "integrity": "sha512-MnypzkaW8dldA8AbJFjMs7y14+ykd2V8JCLKSvX1Gmzx1alH3Y+3LArywHDoAF2wS3pnZp4gacoYtvqBeF6drQ==", + "dependencies": { + "create-error-class": "^3.0.1", + "duplexer2": "^0.1.4", + "is-plain-obj": "^1.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "node-status-codes": "^1.0.0", + "object-assign": "^4.0.1", + "parse-json": "^2.1.0", + "pinkie-promise": "^2.0.0", + "read-all-stream": "^3.0.0", + "readable-stream": "^2.0.5", + "timed-out": "^2.0.0", + "unzip-response": "^1.0.0", + "url-parse-lax": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gry": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/gry/-/gry-5.0.8.tgz", + "integrity": "sha512-meq9ZjYVpLzZh3ojhTg7IMad9grGsx6rUUKHLqPnhLXzJkRQvEL2U3tQpS5/WentYTtHtxkT3Ew/mb10D6F6/g==", + "dependencies": { + "abs": "^1.2.1", + "exec-limiter": "^3.0.0", + "one-by-one": "^3.0.0", + "ul": "^5.0.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/iterate-object": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/iterate-object/-/iterate-object-1.3.4.tgz", + "integrity": "sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw==" + }, + "node_modules/limit-it": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/limit-it/-/limit-it-3.2.10.tgz", + "integrity": "sha512-T0NK99pHnkimldr1WUqvbGV1oWDku/xC9J/OqzJFsV1jeOS6Bwl8W7vkeQIBqwiON9dTALws+rX/XPMQqWerDQ==", + "dependencies": { + "typpy": "^2.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -762,6 +1049,14 @@ "node": ">=12" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -779,11 +1074,159 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-status-codes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", + "integrity": "sha512-1cBMgRxdMWE8KeWCqk2RIOrvUb0XCwYfEsY5/y2NlXyq4Y/RumnOZvTj4Nbr77+Vb2C+kyBoRTdkNOS8L3d/aQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/noop6": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/noop6/-/noop6-1.0.9.tgz", + "integrity": "sha512-DB3Hwyd89dPr5HqEPg3YHjzvwh/mCqizC1zZ8vyofqc+TQRyPDnT4wgXXbLGF4z9YAzwwTLi8pNLhGqcbSjgkA==" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/oargv": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/oargv/-/oargv-3.4.10.tgz", + "integrity": "sha512-SXaMANv9sr7S/dP0vj0+Ybipa47UE1ntTWQ2rpPRhC6Bsvfl+Jg03Xif7jfL0sWKOYWK8oPjcZ5eJ82t8AP/8g==", + "dependencies": { + "iterate-object": "^1.1.0", + "ul": "^5.0.0" + } + }, + "node_modules/obj-def": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/obj-def/-/obj-def-1.0.9.tgz", + "integrity": "sha512-bQ4ya3VYD6FAA1+s6mEhaURRHSmw4+sKaXE6UyXZ1XDYc5D+c7look25dFdydmLd18epUegh398gdDkMUZI9xg==", + "dependencies": { + "deffy": "^2.2.2" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/one-by-one": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/one-by-one/-/one-by-one-3.2.8.tgz", + "integrity": "sha512-HR/pSzZdm46Xqj58K+Bu64kMbSTw8/u77AwWvV+rprO/OsuR++pPlkUJn+SmwqBGRgHKwSKQ974V3uls7crIeQ==", + "dependencies": { + "obj-def": "^1.0.0", + "sliced": "^1.0.1" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", + "integrity": "sha512-PRg65iXMTt/uK8Rfh5zvzkUbfAPitF17YaCY+IbHsYgksiLvtzWWTUildHth3mVaZ7871OJ7gtP4LBRBlmAdXg==", + "dependencies": { + "got": "^5.0.0", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/package-json-path/-/package-json-path-1.0.9.tgz", + "integrity": "sha512-uNu7f6Ef7tQHZRnkyVnCtzdSYVN9uBtge/sG7wzcUaawFWkPYUq67iXxRGrQSg/q0tzxIB8jSyIYUKjG2Jn//A==", + "dependencies": { + "abs": "^1.2.1" + } + }, + "node_modules/package.json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", + "integrity": "sha512-pSxZ6XR5yEawRN2ekxx9IKgPN5uNAYco7MCPxtBEWMKO3UKWa1X2CtQMzMgloeGj2g2o6cue3Sb5iPkByIJqlw==", + "deprecated": "Use pkg.json instead.", + "dependencies": { + "git-package-json": "^1.4.0", + "git-source": "^1.1.0", + "package-json": "^2.3.1" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-url": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-1.3.11.tgz", + "integrity": "sha512-1wj9nkgH/5EboDxLwaTMGJh3oH3f+Gue+aGdh631oCqoSBpokzmMmOldvOeBPtB8GJBYJbaF93KPzlkU+Y1ksg==", + "dependencies": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0" + } + }, + "node_modules/parse-url/node_modules/protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", @@ -811,6 +1254,117 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" + }, + "node_modules/r-json": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/r-json/-/r-json-1.3.0.tgz", + "integrity": "sha512-xesd+RHCpymPCYd9DvDvUr1w1IieSChkqYF1EpuAYrvCfLXji9NP36DvyYZJZZB5soVDvZ0WUtBoZaU1g5Yt9A==", + "dependencies": { + "w-json": "1.3.10" + } + }, + "node_modules/r-package-json": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/r-package-json/-/r-package-json-1.0.9.tgz", + "integrity": "sha512-G4Vpf1KImWmmPFGdtWQTU0L9zk0SjqEC4qs/jE7AQ+Ylmr5kizMzGeC4wnHp5+ijPqNN+2ZPpvyjVNdN1CDVcg==", + "dependencies": { + "package-json-path": "^1.0.0", + "r-json": "^1.2.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w==", + "dependencies": { + "pinkie-promise": "^2.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/rollup": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.0.tgz", @@ -840,6 +1394,24 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -848,6 +1420,130 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/timed-out": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", + "integrity": "sha512-pqqJOi1rF5zNs/ps4vmbE4SFCrM4iR7LW+GHAsHqO/EumqbIWceioevYLM5xZRgQSH6gFgL9J/uB7EcJhQ9niQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tmp": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz", + "integrity": "sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==", + "dependencies": { + "os-tmpdir": "~1.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/typpy": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/typpy/-/typpy-2.3.13.tgz", + "integrity": "sha512-vOxIcQz9sxHi+rT09SJ5aDgVgrPppQjwnnayTrMye1ODaU8gIZTDM19t9TxmEElbMihx2Nq/0/b/MtyKfayRqA==", + "dependencies": { + "function.name": "^1.0.3" + } + }, + "node_modules/ul": { + "version": "5.2.15", + "resolved": "https://registry.npmjs.org/ul/-/ul-5.2.15.tgz", + "integrity": "sha512-svLEUy8xSCip5IWnsRa0UOg+2zP0Wsj4qlbjTmX6GJSmvKMHADBuHOm1dpNkWqWPIGuVSqzUkV3Cris5JrlTRQ==", + "dependencies": { + "deffy": "^2.2.2", + "typpy": "^2.3.4" + } + }, + "node_modules/unzip-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", + "integrity": "sha512-pwCcjjhEcpW45JZIySExBHYv5Y9EeL2OIGEfrSKp2dMUFGFv4CpvZkwJbVge8OvGH2BNNtJBx67DuKuJhf+N5Q==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "dependencies": { + "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vite": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", @@ -936,6 +1632,11 @@ "peerDependencies": { "vue": "^3.2.0" } + }, + "node_modules/w-json": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/w-json/-/w-json-1.3.10.tgz", + "integrity": "sha512-XadVyw0xE+oZ5FGApXsdswv96rOhStzKqL53uSe5UaTadABGkWIg1+DTx8kiZ/VqTZTBneoL0l65RcPe4W3ecw==" } }, "dependencies": { @@ -1310,6 +2011,19 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.11.tgz", "integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==" }, + "abs": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/abs/-/abs-1.3.14.tgz", + "integrity": "sha512-PrS26IzwKLWwuURpiKl8wRmJ2KdR/azaVrLEBWG/TALwT20Y7qjtYp1qcMLHA4206hBHY5phv3w4pjf9NPv4Vw==", + "requires": { + "ul": "^5.0.0" + } + }, + "capture-stack-trace": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz", + "integrity": "sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==" + }, "chart.js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", @@ -1318,11 +2032,61 @@ "@kurkle/color": "^0.3.0" } }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==", + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deffy": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/deffy/-/deffy-2.2.4.tgz", + "integrity": "sha512-pLc9lsbsWjr6RxmJ2OLyvm+9l4j1yK69h+TML/gUit/t3vTijpkNGh8LioaJYTGO7F25m6HZndADcUOo2PsiUg==", + "requires": { + "typpy": "^2.0.0" + } + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "requires": { + "readable-stream": "^2.0.2" + } + }, + "err": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/err/-/err-1.1.1.tgz", + "integrity": "sha512-N97Ybd2jJHVQ+Ft3Q5+C2gM3kgygkdeQmEqbN2z15UTVyyEsIwLA1VK39O1DHEJhXbwIFcJLqm6iARNhFANcQA==", + "requires": { + "typpy": "^2.2.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "esbuild": { "version": "0.19.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.9.tgz", @@ -1358,6 +2122,15 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "exec-limiter": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/exec-limiter/-/exec-limiter-3.2.13.tgz", + "integrity": "sha512-86Ri699bwiHZVBzTzNj8gspqAhCPchg70zPVWIh3qzUOA1pUMcb272Em3LPk8AE0mS95B9yMJhtqF8vFJAn0dA==", + "requires": { + "limit-it": "^3.0.0", + "typpy": "^2.1.0" + } + }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1365,6 +2138,181 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.name": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/function.name/-/function.name-1.0.13.tgz", + "integrity": "sha512-mVrqdoy5npWZyoXl4DxCeuVF6delDcQjVS9aPdvLYlBxtMTZDR2B5GVEQEoM1jJyspCqg3C0v4ABkLE7tp9xFA==", + "requires": { + "noop6": "^1.0.1" + } + }, + "git-package-json": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/git-package-json/-/git-package-json-1.4.10.tgz", + "integrity": "sha512-DRAcvbzd2SxGK7w8OgYfvKqhFliT5keX0lmSmVdgScgf1kkl5tbbo7Pam6uYoCa1liOiipKxQZG8quCtGWl/fA==", + "requires": { + "deffy": "^2.2.1", + "err": "^1.1.1", + "gry": "^5.0.0", + "normalize-package-data": "^2.3.5", + "oargv": "^3.4.1", + "one-by-one": "^3.1.0", + "r-json": "^1.2.1", + "r-package-json": "^1.0.0", + "tmp": "0.0.28" + } + }, + "git-source": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/git-source/-/git-source-1.1.10.tgz", + "integrity": "sha512-XZZ7ZgnLL35oLgM/xjnLYgtlKlxJG0FohC1kWDvGkU7s1VKGXK0pFF/g1itQEwQ3D+uTQzBnzPi8XbqOv7Wc1Q==", + "requires": { + "git-url-parse": "^5.0.1" + } + }, + "git-up": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", + "integrity": "sha512-SRVN3rOLACva8imc7BFrB6ts5iISWKH1/h/1Z+JZYoUI7UVQM7gQqk4M2yxUENbq2jUUT09NEND5xwP1i7Ktlw==", + "requires": { + "is-ssh": "^1.0.0", + "parse-url": "^1.0.0" + } + }, + "git-url-parse": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", + "integrity": "sha512-4uSiOgrryNEMBX+gTWogenYRUh2j1D+95STTSEF2RCTgLkfJikl8c7BGr0Bn274hwuxTsbS2/FQ5pVS9FoXegQ==", + "requires": { + "git-up": "^1.0.0" + } + }, + "got": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-5.6.0.tgz", + "integrity": "sha512-MnypzkaW8dldA8AbJFjMs7y14+ykd2V8JCLKSvX1Gmzx1alH3Y+3LArywHDoAF2wS3pnZp4gacoYtvqBeF6drQ==", + "requires": { + "create-error-class": "^3.0.1", + "duplexer2": "^0.1.4", + "is-plain-obj": "^1.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "node-status-codes": "^1.0.0", + "object-assign": "^4.0.1", + "parse-json": "^2.1.0", + "pinkie-promise": "^2.0.0", + "read-all-stream": "^3.0.0", + "readable-stream": "^2.0.5", + "timed-out": "^2.0.0", + "unzip-response": "^1.0.0", + "url-parse-lax": "^1.0.0" + } + }, + "gry": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/gry/-/gry-5.0.8.tgz", + "integrity": "sha512-meq9ZjYVpLzZh3ojhTg7IMad9grGsx6rUUKHLqPnhLXzJkRQvEL2U3tQpS5/WentYTtHtxkT3Ew/mb10D6F6/g==", + "requires": { + "abs": "^1.2.1", + "exec-limiter": "^3.0.0", + "one-by-one": "^3.0.0", + "ul": "^5.0.0" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "requires": { + "hasown": "^2.0.0" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==" + }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" + }, + "is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "requires": { + "protocols": "^2.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "iterate-object": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/iterate-object/-/iterate-object-1.3.4.tgz", + "integrity": "sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw==" + }, + "limit-it": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/limit-it/-/limit-it-3.2.10.tgz", + "integrity": "sha512-T0NK99pHnkimldr1WUqvbGV1oWDku/xC9J/OqzJFsV1jeOS6Bwl8W7vkeQIBqwiON9dTALws+rX/XPMQqWerDQ==", + "requires": { + "typpy": "^2.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, "magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -1373,16 +2321,149 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, "nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, + "node-status-codes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", + "integrity": "sha512-1cBMgRxdMWE8KeWCqk2RIOrvUb0XCwYfEsY5/y2NlXyq4Y/RumnOZvTj4Nbr77+Vb2C+kyBoRTdkNOS8L3d/aQ==" + }, + "noop6": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/noop6/-/noop6-1.0.9.tgz", + "integrity": "sha512-DB3Hwyd89dPr5HqEPg3YHjzvwh/mCqizC1zZ8vyofqc+TQRyPDnT4wgXXbLGF4z9YAzwwTLi8pNLhGqcbSjgkA==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "oargv": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/oargv/-/oargv-3.4.10.tgz", + "integrity": "sha512-SXaMANv9sr7S/dP0vj0+Ybipa47UE1ntTWQ2rpPRhC6Bsvfl+Jg03Xif7jfL0sWKOYWK8oPjcZ5eJ82t8AP/8g==", + "requires": { + "iterate-object": "^1.1.0", + "ul": "^5.0.0" + } + }, + "obj-def": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/obj-def/-/obj-def-1.0.9.tgz", + "integrity": "sha512-bQ4ya3VYD6FAA1+s6mEhaURRHSmw4+sKaXE6UyXZ1XDYc5D+c7look25dFdydmLd18epUegh398gdDkMUZI9xg==", + "requires": { + "deffy": "^2.2.2" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "one-by-one": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/one-by-one/-/one-by-one-3.2.8.tgz", + "integrity": "sha512-HR/pSzZdm46Xqj58K+Bu64kMbSTw8/u77AwWvV+rprO/OsuR++pPlkUJn+SmwqBGRgHKwSKQ974V3uls7crIeQ==", + "requires": { + "obj-def": "^1.0.0", + "sliced": "^1.0.1" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" + }, + "package-json": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", + "integrity": "sha512-PRg65iXMTt/uK8Rfh5zvzkUbfAPitF17YaCY+IbHsYgksiLvtzWWTUildHth3mVaZ7871OJ7gtP4LBRBlmAdXg==", + "requires": { + "got": "^5.0.0", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, + "package-json-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/package-json-path/-/package-json-path-1.0.9.tgz", + "integrity": "sha512-uNu7f6Ef7tQHZRnkyVnCtzdSYVN9uBtge/sG7wzcUaawFWkPYUq67iXxRGrQSg/q0tzxIB8jSyIYUKjG2Jn//A==", + "requires": { + "abs": "^1.2.1" + } + }, + "package.json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", + "integrity": "sha512-pSxZ6XR5yEawRN2ekxx9IKgPN5uNAYco7MCPxtBEWMKO3UKWa1X2CtQMzMgloeGj2g2o6cue3Sb5iPkByIJqlw==", + "requires": { + "git-package-json": "^1.4.0", + "git-source": "^1.1.0", + "package-json": "^2.3.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-url": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-1.3.11.tgz", + "integrity": "sha512-1wj9nkgH/5EboDxLwaTMGJh3oH3f+Gue+aGdh631oCqoSBpokzmMmOldvOeBPtB8GJBYJbaF93KPzlkU+Y1ksg==", + "requires": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0" + }, + "dependencies": { + "protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==" + } + } + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "requires": { + "pinkie": "^2.0.0" + } + }, "postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", @@ -1393,6 +2474,99 @@ "source-map-js": "^1.0.2" } }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" + }, + "r-json": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/r-json/-/r-json-1.3.0.tgz", + "integrity": "sha512-xesd+RHCpymPCYd9DvDvUr1w1IieSChkqYF1EpuAYrvCfLXji9NP36DvyYZJZZB5soVDvZ0WUtBoZaU1g5Yt9A==", + "requires": { + "w-json": "1.3.10" + } + }, + "r-package-json": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/r-package-json/-/r-package-json-1.0.9.tgz", + "integrity": "sha512-G4Vpf1KImWmmPFGdtWQTU0L9zk0SjqEC4qs/jE7AQ+Ylmr5kizMzGeC4wnHp5+ijPqNN+2ZPpvyjVNdN1CDVcg==", + "requires": { + "package-json-path": "^1.0.0", + "r-json": "^1.2.1" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w==", + "requires": { + "pinkie-promise": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "requires": { + "rc": "^1.0.1" + } + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "rollup": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.0.tgz", @@ -1415,11 +2589,129 @@ "fsevents": "~2.3.2" } }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" + }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "timed-out": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", + "integrity": "sha512-pqqJOi1rF5zNs/ps4vmbE4SFCrM4iR7LW+GHAsHqO/EumqbIWceioevYLM5xZRgQSH6gFgL9J/uB7EcJhQ9niQ==" + }, + "tmp": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz", + "integrity": "sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==", + "requires": { + "os-tmpdir": "~1.0.1" + } + }, + "typpy": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/typpy/-/typpy-2.3.13.tgz", + "integrity": "sha512-vOxIcQz9sxHi+rT09SJ5aDgVgrPppQjwnnayTrMye1ODaU8gIZTDM19t9TxmEElbMihx2Nq/0/b/MtyKfayRqA==", + "requires": { + "function.name": "^1.0.3" + } + }, + "ul": { + "version": "5.2.15", + "resolved": "https://registry.npmjs.org/ul/-/ul-5.2.15.tgz", + "integrity": "sha512-svLEUy8xSCip5IWnsRa0UOg+2zP0Wsj4qlbjTmX6GJSmvKMHADBuHOm1dpNkWqWPIGuVSqzUkV3Cris5JrlTRQ==", + "requires": { + "deffy": "^2.2.2", + "typpy": "^2.3.4" + } + }, + "unzip-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", + "integrity": "sha512-pwCcjjhEcpW45JZIySExBHYv5Y9EeL2OIGEfrSKp2dMUFGFv4CpvZkwJbVge8OvGH2BNNtJBx67DuKuJhf+N5Q==" + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "requires": { + "prepend-http": "^1.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "vite": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", @@ -1451,6 +2743,11 @@ "requires": { "@vue/devtools-api": "^6.0.0" } + }, + "w-json": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/w-json/-/w-json-1.3.10.tgz", + "integrity": "sha512-XadVyw0xE+oZ5FGApXsdswv96rOhStzKqL53uSe5UaTadABGkWIg1+DTx8kiZ/VqTZTBneoL0l65RcPe4W3ecw==" } } } diff --git a/cfsb-frontend/package.json b/cfsb-frontend/package.json index 81ce9e5..ac271c5 100644 --- a/cfsb-frontend/package.json +++ b/cfsb-frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "chart.js": "^4.4.1", + "package.json": "^2.0.1", "vue-router": "^4.0.13" }, "devDependencies": { diff --git a/cfsb-frontend/public/favicon.ico b/cfsb-frontend/public/favicon.ico index df36fcf..ea39f3c 100644 Binary files a/cfsb-frontend/public/favicon.ico and b/cfsb-frontend/public/favicon.ico differ diff --git a/cfsb-frontend/src/App.vue b/cfsb-frontend/src/App.vue index 09131f0..6a3b431 100644 --- a/cfsb-frontend/src/App.vue +++ b/cfsb-frontend/src/App.vue @@ -28,17 +28,55 @@ <!-- Main content where routed components will be displayed --> <!-- <router-view></router-view> <button v-if="showCriteriaSelectionButton" @click="goToCriteriaSelection">Go to Criteria Selection</button> --> + + <div class="modal fade" id="userLoginModal" aria-hidden="false"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title">User login</h1> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + + <form @submit.prevent="submitUserLoginForm"> + <div class="mb-3"> + <label for="app_id" class="form-label">Insert Application ID</label> + <input type="text" class="form-control" id="app_id" v-model="app_id" placeholder="Application ID" required> + </div> + <div class="mb-3"> + <label for="username" class="form-label">Your username</label> + <input type="text" class="form-control" id="username" v-model="username" placeholder="Username" required> + </div> + + <div class="mb-3"> + <label for="password" class="form-label">Your password</label> + <input type="password" class="form-control" id="password" v-model="password" placeholder="Password" required> + </div> + + <button type="submit" class="btn btn-success">Login</button> + </form> + + <div v-if="!login" class="alert alert-danger">Error</div> + + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" ref="modalCloseBtn">Close</button> + </div> + </div> + </div> + </div> + <footer class="footer text-center p-2"> - <span class="text-white">© NebulOus</span> + <span class="text-white">© NebulOus - Cloud Fog Service Broker</span> </footer> </div> </template> <style> :root { - --main-color: #7030A0; + --main-color: #1b253b; --secondary-color: #e0cffc; - --color-indigo-700: #3d0a91; + --color-indigo-700: #172135; --light-gray-color: #f8f9fa; --medium-gray-color: #6c757d; } @@ -89,22 +127,107 @@ .footer { background-color: var(--main-color); + margin-top: 15px; } </style> <script> +export const backendURL = import.meta.env.VITE_BACKEND_URL; +const apiURL = backendURL; export default { name: 'App', + data() { + return { + username: "", + password: "", + uuid: "", + app_id: "", + login: true + } + }, methods: { goToCriteriaSelection() { this.$router.push('/criteria-selection'); - } + }, + checkUserLogin() { + let uuid = localStorage.getItem('fog_broker_user_uuid'); + if (uuid) { + console.log("user is set"); + } else { + console.log("user not set"); + let myModal = new bootstrap.Modal(document.getElementById('userLoginModal')); + myModal.show(); + } + }, + async submitUserLoginForm() { + console.log('username = :', this.username); + let user_data = { + 'username': this.username, + 'password': this.password, + 'app_id': this.app_id, + } + let result = await this.fetchUser(user_data) + this.username = ""; + this.password = ""; + }, + async fetchUser(user_data) { + try { + const response = await fetch(apiURL+'/login', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(user_data) + }); + const data = await response.json(); + console.log(data); + if (data.length===1) { + this.uuid = data[0][2]; + console.log(data[0][2]); + localStorage.setItem('fog_broker_user_uuid', data[0][2]); + localStorage.setItem('fog_broker_app_id', user_data.app_id); + let elem = this.$refs.modalCloseBtn + elem.click() + this.login = true; + } else { + this.login = false; + } + } catch (error) { + console.error('Error fetching user:', error); + } + }, + getURLparams() { + let app_in_url = false + let user_in_url = false + let app_id_from_js = new URL(location.href).searchParams.get('app_id'); + let user_id_from_js = new URL(location.href).searchParams.get('user_id'); + + if (app_id_from_js) { + console.log('app_id from URL:', app_id_from_js); + this.app_id = app_id_from_js; + app_in_url = true; + localStorage.setItem('fog_broker_app_id', this.app_id); + } + if (user_id_from_js) { + console.log('user_id from URL:', user_id_from_js); + this.uuid = user_id_from_js; + user_in_url = true + localStorage.setItem('fog_broker_user_uuid', this.uuid); + } + if (app_in_url && user_in_url){ + return true + } else { + return false + } + }, }, computed: { showCriteriaSelectionButton() { return this.$route.path === '/'; /* other conditions */ } }, + mounted() { + this.getURLparams(); + this.checkUserLogin(); + } }; </script> diff --git a/cfsb-frontend/src/assets/base.css b/cfsb-frontend/src/assets/base.css index 8816868..64e06b2 100644 --- a/cfsb-frontend/src/assets/base.css +++ b/cfsb-frontend/src/assets/base.css @@ -1,8 +1,8 @@ /* 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-white: #1B253BFF; + --vt-c-white-soft: #1b253b; + --vt-c-white-mute: #1b253b; --vt-c-black: #181818; --vt-c-black-soft: #222222; diff --git a/cfsb-frontend/src/components/CriteriaSelection.vue b/cfsb-frontend/src/components/CriteriaSelection.vue index 4627baf..6e2e1b2 100644 --- a/cfsb-frontend/src/components/CriteriaSelection.vue +++ b/cfsb-frontend/src/components/CriteriaSelection.vue @@ -1,28 +1,33 @@ <template> - <div class="row" style="padding-bottom: 2rem"> - </div> + <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"> + <!-- Use a flex container for the header --> + <div class="card-header d-flex justify-content-between align-items-center"> <h2>Selection of Criteria</h2> + <!-- Clickable icon and text for expanding/collapsing --> + <span class="expand-collapse-link" @click="toggleExpandAll"> + <i v-bind:class="expandIconClass"></i>{{ expandButtonText }}</span> </div> <div class="card-body"> - <HierarchicalCategoryList - :items="hierarchicalCategoryList" - @selected-items="updateSelectedItems" - ></HierarchicalCategoryList> + <p class="description"> + Please select at least two criteria to proceed. + </p> + <!-- HierarchicalCategoryList is included here with the necessary bindings --> + <HierarchicalCategoryList ref="hierarchicalList" :items="hierarchicalCategoryList" @selected-items="updateSelectedItems"/> </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> +export const backendURL = import.meta.env.VITE_BACKEND_URL; +const apiURL = backendURL; import HierarchicalCategoryList from "@/components/HierarchicalCategoryList.vue"; export default { @@ -33,32 +38,62 @@ export default { return { hierarchicalCategoryList: [], selectedItems: [], + allCategoriesExpanded: false }; }, mounted() { console.log('CriteriaSelection.vue mounted'); this.fetchHierarchicalCategoryList(); }, + computed: { + expandButtonText() { + return this.allCategoriesExpanded ? 'Collapse All' : 'Expand All'; + }, + expandIconClass() { + return this.allCategoriesExpanded ? 'bi-arrow-bar-up' : 'bi-arrow-bar-down'; + } + }, methods: { async fetchHierarchicalCategoryList() { try { - const response = await fetch('http://127.0.0.1:5000/get_hierarchical_category_list'); + const response = await fetch(apiURL+'/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' }); + toggleExpandAll() { + this.allCategoriesExpanded = !this.allCategoriesExpanded; + if (this.$refs.hierarchicalList) { + this.$refs.hierarchicalList.setChildrenVisibility(this.hierarchicalCategoryList, this.allCategoriesExpanded); + } }, updateSelectedItems(newSelectedItems) { //console.log('Updating selected items in CriteriaSelection.vue:', newSelectedItems); this.selectedItems = newSelectedItems; - }, + } }, + }; </script> +<style> +.card-header { + display: flex; + justify-content: space-between; + align-items: center; +} +.expand-collapse-link { + cursor: pointer; + color: var(--color-indigo-700); user-select: none; + display: flex; + align-items: center; /* Aligns the icon and text vertically */ +} + +.expand-collapse-link i { + margin-right: 0.5rem; /* Add some space between the icon and text */ +} + +</style> \ No newline at end of file diff --git a/cfsb-frontend/src/components/DataGrid.vue b/cfsb-frontend/src/components/DataGrid.vue index 3c37a8d..4ba21f1 100644 --- a/cfsb-frontend/src/components/DataGrid.vue +++ b/cfsb-frontend/src/components/DataGrid.vue @@ -1,25 +1,68 @@ <template> <div> <div class="p-4"> - <h2>Edge / Fog Nodes Data</h2> + <h2>Nodes Data</h2> </div> +<!-- <table v-if="gridData.length" class="grid-cell-class">--> +<!-- <thead>--> +<!-- <tr>--> +<!-- <th>Node</th>--> +<!-- <!– Assuming all entries have the same criteria, using the first one to generate headers –>--> +<!-- <th v-for="(criterion, index) in gridData[0].criteria" :key="index">--> +<!-- {{ criterion.title }}--> +<!-- </th>--> +<!-- </tr>--> +<!-- </thead>--> +<!-- <tbody>--> +<!-- <tr v-for="(entry, entryIndex) in gridData" :key="entry.name">--> +<!-- <td>{{ entry.name }}</td>--> +<!-- <td v-for="(dataValue, dataIndex) in entry.data_values" :key="`${entry.name}-${dataIndex}`">--> +<!-- <!– Numeric data type –>--> +<!-- <template v-if="dataValue.data_type.type === 2">--> +<!-- <input type="number" v-model="dataValue.value" @blur="validateNumeric(entry.data_values, dataIndex)" step="0.5"/>--> +<!-- </template>--> - <table v-if="Object.keys(gridData).length" class="grid-cell-class"> +<!-- <!– Ordinal data type –>--> +<!-- <template v-else-if="dataValue.data_type.type === 1">--> +<!-- <select v-model="dataValue.value">--> +<!-- <option v-for="option in dataValue.data_type.values" :value="option" :key="option">{{ option }}</option>--> +<!-- </select>--> +<!-- </template>--> + +<!-- <!– Boolean data type –>--> +<!-- <template v-else-if="dataValue.data_type.type === 5">--> +<!-- <select v-model="dataValue.value">--> +<!-- <option v-for="option in ['True', 'False']" :value="option" :key="option">{{ option }}</option>--> +<!-- </select>--> +<!-- </template>--> + +<!-- <!– Fallback or other data types –>--> +<!-- <template v-else>--> +<!-- <input type="text" v-model="dataValue.value" />--> +<!-- </template>--> +<!-- </td>--> +<!-- </tr>--> +<!-- </tbody>--> +<!-- </table>--> + <table v-if="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> + <th>Node</th> + <th v-for="(criterion, index) in gridData[0].criteria" :key="index">{{ criterion.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> + <tr v-for="(entry, entryIndex) in gridData" :key="entry.name"> + <td>{{ entry.name }}</td> + <td v-for="(criterion, criterionIndex) in entry.criteria" :key="`${entry.name}-${criterionIndex}`"> + <input v-if="criterion.data_type.type === 2" type="number" v-model="criterion.value" /> + <select v-else-if="criterion.data_type.type === 1" v-model="criterion.value"> + <option v-for="option in criterion.data_type.values" :value="option" :key="option">{{ option }}</option> </select> - <input v-else type="text" v-model="values.data_values[index - 1]" /> + <select v-else-if="criterion.data_type.type === 5" v-model="criterion.value"> + <option v-for="option in ['True', 'False']" :value="option">{{ option }}</option> + </select> + <input v-else type="text" v-model="criterion.value" /> </td> </tr> </tbody> @@ -28,58 +71,62 @@ No data to display. </div> <div class="pt-4"></div> - <!-- <button @click="SaveDataforEvaluation" class="bg-color-primary">Save and Run Evaluation</button> --> + <button @click="goBackToCriteriaSelection" class="bg-color-primary">Back to Criteria Selection</button> <button @click="SaveDataforWR" class="bg-color-primary">Save and Add Weight Restrictions</button> </div> - </template> <script> -import { useRouter } from 'vue-router'; - +export const backendURL = import.meta.env.VITE_BACKEND_URL; +const apiURL = backendURL; 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 + NodeNames: [], + gridData: [], // Updated to be an array to match the structure provided by the backend }; }, mounted() { - const selectedItems = this.$route.params.selectedItems || []; - if (selectedItems.length > 0) { - this.fetchGridData(selectedItems); + let selectedItemsWithTypes = this.getSelectedItemsFromStorage(); + if (!selectedItemsWithTypes.length) { + selectedItemsWithTypes = this.$route.params.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; + if (selectedItemsWithTypes.length > 0) { + this.fetchGridData(selectedItemsWithTypes.map(item => item.name)); } }, methods: { - // Receives the Grid Data 1st time + getSelectedItemsFromStorage() { + const storedItems = localStorage.getItem('selectedCriteria'); + return storedItems ? JSON.parse(storedItems) : []; + }, async fetchGridData(selectedItems) { try { - const response = await fetch('http://127.0.0.1:5000/process_selected_items', { + // Retrieve app_id and user_id from local storage directly within this method + const app_id = localStorage.getItem('fog_broker_app_id'); + const user_id = localStorage.getItem('fog_broker_user_uuid'); + const response = await fetch(apiURL+'/process_selected_criteria', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({selectedItems}), + // body: JSON.stringify({selectedItems}), + body: JSON.stringify({ + selectedItems, + app_id, // Include app_id from local storage + user_id // Include user_id from local storage + }) }); + 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 + const { gridData, NodeNames } = await response.json(); + // Initialize data_values for each entry in gridData + this.gridData = gridData.map(entry => ({ + ...entry, + data_values: entry.criteria.map(criterion => ({ + value: criterion.value, + data_type: criterion.data_type + })) + })); + this.NodeNames = NodeNames || []; } else { throw new Error('Failed to fetch grid data'); } @@ -87,81 +134,136 @@ export default { 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)); + validateNumeric(entry, criterionIndex) { + // Directly modify and validate numeric value within the entry's criteria + const numericValue = parseFloat(entry.criteria[criterionIndex].value); + if (isNaN(numericValue) || numericValue <= 0) { + alert('Please enter a number greater than zero.'); + entry.criteria[criterionIndex].value = ''; // Reset invalid value + return false; // Halt further processing + } else { + entry.criteria[criterionIndex].value = numericValue; // Update with valid numeric value + } }, 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 - } + for (const entry of this.gridData) { + for (const criterion of entry.criteria) { + // Convert value to string to handle trimming and empty checks + const valueAsString = String(criterion.value).trim(); + + switch (criterion.data_type.type) { + case 2: // Numeric + const numericValue = parseFloat(valueAsString); + if (isNaN(numericValue) || numericValue <= 0) { + alert('Please enter a valid number for all numeric fields.'); + return false; // Prevent further processing + } + break; + + case 1: // Ordinal + if (!criterion.data_type.values.includes(criterion.value)) { + alert(`Please select a valid option for ${criterion.title}.`); + return false; // Prevent further processing + } + break; + + case 5: // Boolean + if (!["True", "False"].includes(valueAsString)) { + alert(`Please select a valid boolean value for ${criterion.title}.`); + return false; // Prevent further processing + } + break; + + default: + // Check for empty values for any other data types + if (valueAsString === '') { + alert(`Please ensure all fields are filled for ${criterion.title}.`); + return false; // Prevent further processing + } } } } - return true; // All data is valid + return true; // All validations passed }, - 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); - } - } + goBackToCriteriaSelection() { + this.$router.push({ name: 'CriteriaSelection' }); }, - async SaveDataforEvaluation() { + async SaveDataforWR() { + if (!this.validateGridData()) { + return; + } + // Log the current state of gridData + console.log("Before saving, gridData is:", JSON.parse(JSON.stringify(this.gridData))); + + try { + const formattedGridData = this.gridData.map(node => ({ + name: node.name, + id: node.id, + criteria: node.criteria.map(criterion => ({ + title: criterion.title, + value: criterion.value, + data_type: criterion.data_type.type + })) + })); + + const DataforWR = JSON.stringify(formattedGridData); + localStorage.setItem('gridData', DataforWR); // Save gridData to localStorage + // console.log("Save DataforWR DataGrid.VUE to localstorage:", JSON.stringify(JSON.parse(DataforWR), null, 2)); + + // Save the NodeNames to localStorage + const NodeNames = JSON.stringify(this.NodeNames); + localStorage.setItem('NodeNames', NodeNames); + + // Navigate to WR component with prepared data and NodeNames + this.$router.push({ + name: 'WR', + params: { + data: DataforWR, + NodeNames: NodeNames // Include NodeNames in the route parameters + } + }); + } catch (error) { + console.error('Error:', error); + } } + + } }; </script> + <style> /* Basic table styling */ table { -width: 100%; -border-collapse: collapse; + width: 100%; + border-collapse: collapse; } /* Header styling */ th { -background-color: #813F8F; /* Primary color */ -color: #FFFFFF; /* White text */ -padding: 10px; -text-align: center; + background-color: #232d45; /* 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; + background-color: #E7E7E7; /* Light grey */ + color: #172135; /* Secondary color */ + padding: 8px; + font-weight: bold; } /* Alternate row colors for better readability */ tr:nth-child(even) { -background-color: #E4DCD5; /* Light tan */ + background-color: #155e75; /* Light tan */ } /* Hover effect on rows */ tr:hover { -background-color: #6FBFFF; /* Light blue */ + background-color: #6FBFFF; /* Light blue */ } /* Additional styles for editable input fields in the table */ @@ -200,10 +302,11 @@ button:hover { select { width: 100%; padding: 8px; - border: 1px solid #ccc; + border: 1px solid #1b253b; } .grid-cell-class { text-align: center; } -</style> + +</style> \ No newline at end of file diff --git a/cfsb-frontend/src/components/HierarchicalCategoryList.vue b/cfsb-frontend/src/components/HierarchicalCategoryList.vue index 69460b5..54bd77f 100644 --- a/cfsb-frontend/src/components/HierarchicalCategoryList.vue +++ b/cfsb-frontend/src/components/HierarchicalCategoryList.vue @@ -3,35 +3,37 @@ <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> + <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 v-model="item.checked" type="checkbox" @change="handleCheckboxChange(item)" /> --> <input type="checkbox" :checked="item.checked" @change="() => handleCheckboxChange(item)" /> - <span @click="toggleCategory(item)">{{ item.title }}</span> + <span @click="toggleCategory(item)" v-bind:title="item.description"> {{ item.title }} ({{ getTypeName(item.type) }}) </span> </label> <ul v-show="item.showChildren" class="list-group"> <!-- Recursive call without Submit button --> - <HierarchicalCategoryList :isChild="true" :items="item.children" @selected-items="updateSelectedItems" /> + <!-- <HierarchicalCategoryList :isChild="true" :items="item.children" @selected-items="updateSelectedItems" /> --> + <HierarchicalCategoryList :isChild="true" :items="item.children" /> </ul> </li> </ul> + <button @click="goBackToHome" class="bg-color-primary">Back</button> <!-- Submit button outside the recursive structure --> - <button type="submit" class="bg-color-primary">Submit</button> + <button type="submit" class="bg-color-primary">Next</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 v-model="item.checked" type="checkbox" @change="handleCheckboxChange(item)" />--> <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" /> + <span @click="toggleCategory(item)" v-bind:title="item.description"> {{ item.title }} ({{ getTypeName(item.type) }}) </span> + </label> + <ul v-show="item.showChildren"> + <!-- Recursive call without Submit button + <HierarchicalCategoryList :isChild="true" :items="item.children" @selected-items="updateSelectedItems" /> --> + <HierarchicalCategoryList :isChild="true" :items="item.children" /> </ul> </li> </div> @@ -46,11 +48,12 @@ export default { type: Boolean, default: false, }, + updateItemType: Function, + }, data() { return { localSelectedItems: [], - selectedItemsFromBack: [], }; }, computed: { @@ -95,50 +98,91 @@ export default { } return null; }, + setChildrenVisibility(items, visible) { + items.forEach(item => { + item.showChildren = visible; + if (item.children && item.children.length > 0) { + this.setChildrenVisibility(item.children, visible); + } + }); + }, updateSelectedItems() { - // Update the selected items list - this.localSelectedItems = this.collectSelectedItems(this.items); - // Emit the updated list - this.$emit('selected-items', this.localSelectedItems); + // Update the selected items list with additional type information + const selectedItemsWithType = this.items + .filter(item => item.checked) + .map(item => ({ name: item.name, type: item.type })); + }, + getTypeName(type) { + switch (type) { + case 2: return 'Numeric'; + case 1: return 'Ordinal'; + case 5: return 'Boolean'; + case 7: return 'Ordinal'; + default: return 'Numeric'; + } }, collectSelectedItems(items) { let selectedItems = []; for (const item of items) { if (item.checked) { - selectedItems.push(item.name); + console.log(`Selected item: ${item.name}, Type: ${item.type}, Title: ${item.title}`); + selectedItems.push({ name: item.name, type: item.type, title: item.title }); + //console.log('Selected items in collectSelectedItems:', selectedItems); // Log selected items } - if (item.children) { - selectedItems = selectedItems.concat(this.collectSelectedItems(item.children)); + if (item.children && item.children.length > 0) { + const childSelectedItems = this.collectSelectedItems(item.children); + selectedItems = selectedItems.concat(childSelectedItems); } } return selectedItems; }, - submitSelection() { + async submitSelection() { const selectedItems = this.collectSelectedItems(this.items); + //console.log('Selected items in Submit:', selectedItems); // Log selected items - if (selectedItems.length < 2) { - alert('Please select at least two items before submitting.'); + let nonBooleanCriteriaCount = 0; + let selectedItemsWithType = selectedItems.map(item => ({ + name: item.name, + type: item.type, + title: item.title + })); + + for (const item of selectedItemsWithType) { + console.log(`Item: ${item.name}, Type: ${item.type}`); // Add this line for debugging + if (item.type !== 5) { // Or item.type !== 'Boolean' depending on the actual format + nonBooleanCriteriaCount++; + } + } + + //console.log('Non-boolean criteria count:', nonBooleanCriteriaCount); // Log non-boolean criteria count + console.log('selectedItemsWithType:', selectedItemsWithType); + + if (selectedItemsWithType.length < 2) { + //console.log('Blocking submission due to insufficient criteria selection.'); + alert('Please select at least two criteria to proceed.'); return; } - // Emitting the selected items - useful if there's a parent component listening to this event - this.$emit('selected-items', selectedItems); + if (nonBooleanCriteriaCount < 2) { + //console.log('Blocking submission due to insufficient non-boolean criteria selection.'); + alert('Please select at least two non-boolean criteria.'); + return; + } - // Programmatic navigation to the DataGrid page, passing the selected items as route parameters - this.$router.push({ name: 'DataGrid', params: { selectedItems: selectedItems } }); + // Save the selected items with types to Local Storage + localStorage.setItem('selectedCriteria', JSON.stringify(selectedItemsWithType)); + + // Emitting the selected items with types to the DataGrid.vue + // this.$emit('selected-items', selectedItemsWithType); + + // Navigate to DataGrid.vue, passing only the item names as route parameters + const itemNames = selectedItemsWithType.map(item => item.name); + this.$router.push({ name: 'DataGrid', params: { selectedItems: itemNames } }); }, - 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; - }, - }, + goBackToHome() { + this.$router.push({ name: 'HomePage' }); + } + } }; </script> @@ -193,6 +237,8 @@ button { border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; + margin-bottom: 10px; + margin-top: 10px; } button:hover { @@ -217,4 +263,5 @@ ul { background-color: var(--light-gray-color); color: var(--main-color); } + </style> diff --git a/cfsb-frontend/src/components/HomePage.vue b/cfsb-frontend/src/components/HomePage.vue index 164a14f..7ea1e4b 100644 --- a/cfsb-frontend/src/components/HomePage.vue +++ b/cfsb-frontend/src/components/HomePage.vue @@ -1,18 +1,45 @@ <script> export default { - name: "HomePage" + name: "HomePage", + // methods: { + // async saveIds(appId, userId) { + // fetch(' http://127.0.0.1:5000/save_ids', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // app_id: appId, + // user_id: userId, + // }), + // }) + // .then(response => response.json()) + // .then(data => { + // console.log('Success:', data); + // // Save to local storage + // localStorage.setItem('app_id', appId); + // localStorage.setItem('user_id', userId); + // }) + // .catch((error) => { + // console.error('Error:', error); + // }); + // } + // }, + // mounted() { + // // Example usage + // // this.saveIds('d535cf554ea66fbebfc415ac837a5828', 'e3ff4006-be5f-4e00-bbe1-e49a88b2541a'); + // }, } </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> + <h1 class="display-2">Welcome to <span style="color: var(--main-color);">Cloud Fog Service Broker</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> @@ -30,7 +57,7 @@ export default { <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"> @@ -43,7 +70,7 @@ export default { <div class="row p-4 text-center"> <div class="col col-12"> - <h2 class="display-4">How does it work</h2> + <h2 class="display-4">Architecture</h2> </div> <div class="col col-12"> <img src="/images/Broker.png" class="img-fluid border-radius-md" alt="..."> @@ -54,9 +81,16 @@ export default { </template> <style scoped> - -.bg-row { - background-color: var(--secondary-color); +button-primary:hover{ + border: 2px #172135; +} +.bg-row { + background-color: #e9ebed; +} +.row{text-align: justify; +} +.img-fluid { + max-width: 75%; + height: auto; } - </style> \ No newline at end of file diff --git a/cfsb-frontend/src/components/Results.vue b/cfsb-frontend/src/components/Results.vue index 6bd46f5..cdad7ec 100644 --- a/cfsb-frontend/src/components/Results.vue +++ b/cfsb-frontend/src/components/Results.vue @@ -1,13 +1,16 @@ <template> <div class="results-container"> <h2>Evaluation Results</h2> + <p class="description"> + The scores have been rounded to the nearest two decimal places. + </p> <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>Node</th> <th>Score (%)</th> <th>Ranking</th> </tr> @@ -20,6 +23,8 @@ </tr> </tbody> </table> + <!-- Separator Line --> + <div class="separator-line"></div> <!-- Chart Container --> <div class="charts-container"> <div class="chart-wrapper"> @@ -29,179 +34,270 @@ <canvas id="ranksChart"></canvas> </div> </div> + </div> + <!-- Separator Line --> + <div class="separator-line"></div> + <div class="button-container"> + <button @click="goBackToWR">Add/Modify Weight Restrictions</button> + <button @click="saveProjectResults">Save Project</button> + </div> </div> - <div class="button-container"> - <button @click="goBackToWR">Add/Modify Weight Restrictions</button> - <button @click="saveProjectResults">Save Project</button> - </div> - </div> -</template> + </template> -<script> -import Chart from 'chart.js/auto'; + <script> + export const backendURL = import.meta.env.VITE_BACKEND_URL; + const apiURL = backendURL; + 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' }); + export default { + data() { + return { + results: [], + loading: true, + deaScoresChart: null, + ranksChart: null, + gridData: null, + relativeWRData: null, + immediateWRData: null + }; }, - saveProjectResults() { - // For now, this method is a placeholder - console.log('Save Project Results button clicked'); + mounted() { + const resultsString = localStorage.getItem('evaluationResults'); + + try { + const data = JSON.parse(resultsString); + if (data && data.results) { + this.results = data.results; + this.createCharts(); + this.loading = false; + } else { + console.error('Error fetching results: Data is not in the expected format.'); + this.loading = false; + // Handle the error by navigating to a different page or displaying an error message + } + } catch (error) { + console.error('Error parsing JSON:', error); + // Handle parsing error by navigating to a different page or displaying an error message + this.loading = false; + } }, - 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; + methods: { + goBackToWR() { + // Make sure 'WR' matches the name of the route in your router configuration + this.$router.push({ name: 'WR' }); + }, + async saveProjectResults() { + if (confirm("Save Project?")) { + console.log('Save Project Results button clicked'); + let array_data = [] + // Application Id + let app_id = localStorage.getItem('fog_broker_app_id'); + let appData = [ + {app_id: app_id} + ]; + array_data.push(appData); + + // Node Names + let NodeNamesFromStorage = localStorage.getItem('NodeNames'); + let NodeNames = JSON.parse(NodeNamesFromStorage); + array_data.push(NodeNames); + + // Selected Criteria + let selectedCriteriaFromStorage = localStorage.getItem('selectedCriteria'); + let selectedCriteria = JSON.parse(selectedCriteriaFromStorage); + array_data.push(selectedCriteria); + + // DataGrid Data + let gridDataFromStorage = localStorage.getItem('gridData'); + let GridData = JSON.parse(gridDataFromStorage); + array_data.push(GridData); + // relativeWRData + let relativeWRDataFromStorage = localStorage.getItem('relativeWRData'); + let relativeWRData = JSON.parse(relativeWRDataFromStorage); + array_data.push(relativeWRData); + //immediateWRData + let immediateWRDataFromStorage = localStorage.getItem('immediateWRData'); + let immediateWRData = JSON.parse(immediateWRDataFromStorage); + array_data.push(immediateWRData); + // evaluation Results + let evaluationResultsFromStorage = localStorage.getItem('evaluationResults'); + let evaluationResults = JSON.parse(evaluationResultsFromStorage); + array_data.push(evaluationResults.results); // Save only th results not the LPStatus + + let result = await this.saveProjectData(array_data); + console.log(result); + + // Clear local storage + localStorage.removeItem('evaluationResults'); + localStorage.removeItem('selectedCriteria'); + localStorage.removeItem('NodeNames'); + localStorage.removeItem('gridData'); + localStorage.removeItem('relativeWRData'); + localStorage.removeItem('immediateWRData'); + // localStorage.removeItem('fog_broker_user_uuid'); May keep them so the user can evaluate again + // localStorage.removeItem('fog_broker_app_id'); + + // Redirect to the Home page + this.$router.push({ name: 'HomePage' }); + } + else { + console.log('Project not saved.'); + } + }, + async saveProjectData(data) { + try { + const response = await fetch(apiURL+'/app/save', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) }); - }, - 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); + console.log("response" + response); + let response_data = await response.json(); + console.log(response_data); + } catch (error) { + console.error('Error saving project:', error); + } + }, + createCharts() { + if (!this.results || this.results.length === 0) { + console.error('No results data available to create charts.'); + return; + } + console.log(this.results); - 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 - } + 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 + }] }, - 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 + 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(); } - }); - }, - 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 - } + // 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 + }] }, - 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 + options: { + responsive: true, + //maintainAspectRatio: false, // Set to false to allow full width and controlled height + indexAxis: 'y', // This makes 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}%`; + }); + }, + formatPercentage(value) { + const percentage = (value * 100).toFixed(2); + return percentage === '100.00' ? '100%' : `${percentage}%`; + } } + }; + </script> + + <style> + .results-container { + padding: 20px; } -}; -</script> -<style> -.results-container { - padding: 20px; -} + .loading { + text-align: center; + } -.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 */ + } -.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; + } -.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; + } -td { - text-align: center; -} + .separator-line { + height: 4px; /* Thickness of the line */ + background-color: #172135; /* Deep purple color */ + margin: 10px 0; /* Spacing above and below the line */ + } -</style> + </style> diff --git a/cfsb-frontend/src/components/WR.vue b/cfsb-frontend/src/components/WR.vue index a8c445a..4877eb4 100644 --- a/cfsb-frontend/src/components/WR.vue +++ b/cfsb-frontend/src/components/WR.vue @@ -1,97 +1,177 @@ <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> + <!-- Relative constraints section --> + <div class="relative-constraints"> + <h2>Relative Constraints</h2> + <p class="description"> + Set relative constraints between the criteria. For example, "Weight of Criterion A >= 2* Weight of Criterion B". + </p> + <div v-for="(condition, index) in relativeConditions" :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> + <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.operator">--> +<!-- <option value="" disabled>Select Operator</option>--> +<!-- <option v-for="(value, key) in operatorMapping" :key="key" :value="value">{{ key }}</option>--> +<!-- </select>--> - <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> + <input type="number" v-model.number="condition.value" :min="0" step="0.5" placeholder="Value" /> - <button @click="removeCondition(index)">-</button> + <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">+ Add Relative Constraint</button> </div> + <!-- Separator Line --> + <div class="separator-line"></div> + <div class="immediate-constraints"> + <h2>Immediate Constraints</h2> + <p class="description"> + Set immediate constraints on individual criteria. For example, "Weight of Criterion A >= 0.25". + </p> + <div v-for="(immediateCondition, index) in immediateConditions" :key="`immediate-${index}`" class="condition-row"> + <select v-model="immediateCondition.criterion"> + <option value="" disabled>Select Criterion</option> + <option v-for="col in criteria_titles" :key="`immediate-${col}`" :value="col">{{ col }}</option> + </select> - <button @click="addCondition">+</button> - <!-- <button @click.prevent="sendWRData">Run Evaluation</button> --> - <button @click="sendWRData">Run Evaluation</button> + <select v-model="immediateCondition.operator"> + <option value="" disabled>Select Operator</option> + <option v-for="op in operators" :key="op" :value="op">{{ op }}</option> + </select> + +<!-- <select v-model="immediateCondition.operator">--> +<!-- <option value="" disabled>Select Operator</option>--> +<!-- <option v-for="(value, key) in operatorMapping" :key="key" :value="value">{{ key }}</option>--> +<!-- </select>--> + + <input type="number" v-model.number="immediateCondition.value" :min="0" step="0.1" placeholder="Value" /> + + <button @click="removeImmediateCondition(index)">-</button> + </div> + + <button @click="addImmediateCondition">+ Add Immediate Constraint</button> + </div> + <!-- Separator Line --> + <div class="separator-line"></div> + <div class="pt-4"></div> + <div class="button-container"> + <button @click="goBackToCriteriaSelection" class="bg-color-primary">Back to Criteria Selection</button> + <button @click="sendWRData" class="bg-color-primary">Run Evaluation</button> + </div> </div> </template> <script> -import { useRouter } from 'vue-router'; +export const backendURL = import.meta.env.VITE_BACKEND_URL; +const apiURL = backendURL; export default { data() { return { receivedGridData: null, - conditions: [{ column1: '', operator: '', value: 0, column2: '' }], + relativeConditions: [{ column1: '', operator: '', value: 0, column2: '' }], criteria_titles: [], // This is populated with the column titles operators: ['>=', '=', '<='], + immediateConditions: [{ criterion: '', operator: '', value: 0 }], + operatorMapping: { + '>=': 1, + '=': 0, + '<=': -1 + }, + errorMessage: '', // Add this line }; }, mounted() { + // Prioritize data from route parameters if (this.$route.params.data) { // Parse the JSON string back into an object this.receivedGridData = JSON.parse(this.$route.params.data); + } else { + // Fallback to localStorage if route params are not available + const gridDataFromStorage = localStorage.getItem('gridData'); + if (gridDataFromStorage) { + this.receivedGridData = JSON.parse(gridDataFromStorage); + } + } + + // Continue with other localStorage checks + const wrDataFromStorage = localStorage.getItem('wrData'); + const immediateWRDataFromStorage = localStorage.getItem('immediateWRData'); + + if (wrDataFromStorage) { + this.wrData = JSON.parse(wrDataFromStorage); + } + + if (immediateWRDataFromStorage) { + this.immediateConditions = JSON.parse(immediateWRDataFromStorage); + } else { + this.immediateConditions = [{ criterion: '', operator: '', value: 0 }]; + } + + // Retrieve selectedCriteria from local storage + const selectedCriteriaJson = localStorage.getItem('selectedCriteria'); + if (selectedCriteriaJson) { + try { + const selectedCriteria = JSON.parse(selectedCriteriaJson); + // Use selectedCriteria to populate criteria_titles and filter out boolean criteria (type 5) + this.criteria_titles = selectedCriteria + .filter(info => info.type !== 5) + .map(info => info.title); + } catch (e) { + console.error('Error parsing selected criteria information:', e); + this.$router.push({ name: 'CriteriaSelection' }); + } + } else { + console.error('Error: Selected criteria information not found in local storage.'); + this.$router.push({ name: 'CriteriaSelection' }); } - 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}); + this.relativeConditions.push({column1: '', column2: '', operator: '', value: 0}); }, removeCondition(index) { - this.conditions.splice(index, 1); + this.relativeConditions.splice(index, 1); }, validateForm() { - for (const condition of this.conditions) { + for (const condition of this.relativeConditions) { + if (!condition.column1 || !condition.column2) { + alert('Please select criteria for each relative constraint.'); + return false; + } if (!condition.operator) { - alert('Please select an operator for each condition.'); + alert('Please select an operator for each relative constraint.'); return false; } if (condition.value === null || condition.value === '') { - alert('Please enter a numeric value for each.'); + alert('Please enter a numeric value for each relative constraint.'); 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!'); + if (condition.value <= 0) { + alert('The priority in each relative constraint must be greater than zero.'); return false; } } + + const uniquePairs = new Set( + this.relativeConditions.map(c => [c.column1, c.column2].sort().join('-')) + ); + + if (uniquePairs.size !== this.relativeConditions.length) { + alert('Each pair of criteria can only be used once in a restriction!'); + return false; + } + return true; }, updateDropdowns(index) { @@ -100,15 +180,73 @@ export default { 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); + return this.criteria_titles.filter(col => col !== this.relativeConditions[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); + return this.criteria_titles.filter(col => col !== this.relativeConditions[index].column1); } }, + // Add a method to validate the Immediate Constraints + validateImmediateConstraints() { + let criterionConstraints = {}; + + // Iterate over immediate conditions and organize them by criterion + for (const condition of this.immediateConditions) { + if (!condition.criterion || !condition.operator) { + continue; // Skip empty conditions + } + + // Ensure value is greater than 0 + if (condition.value === null || condition.value === '' || condition.value <= 0) { + alert(`The importance of criterion "${condition.criterion}" should be greater than 0.`); + return false; + } + + // Initialize the constraints list for the criterion if not already done + if (!criterionConstraints[condition.criterion]) { + criterionConstraints[condition.criterion] = {}; + } + + // Check for duplicate operators + if (criterionConstraints[condition.criterion][condition.operator]) { + alert(`You cannot use the same operator more than once for the criterion "${condition.criterion}".`); + return false; + } + + // Add the condition to the list for the criterion + criterionConstraints[condition.criterion][condition.operator] = condition.value; + } + + // Iterate over the constraints for each criterion and apply validation rules + for (const [criterion, operators] of Object.entries(criterionConstraints)) { + // Only one constraint allowed when using '=' operator + if (operators['='] !== undefined && Object.keys(operators).length > 1) { + alert(`Only one constraint allowed for '${criterion}' when using '=' operator.`); + return false; + } + + // Validate logical consistency between '>=' and '<=' values + if (operators['>='] !== undefined && operators['<='] !== undefined) { + const greaterThanOrEqualValue = parseFloat(operators['>=']); + const lessThanOrEqualValue = parseFloat(operators['<=']); + + if (isNaN(greaterThanOrEqualValue) || isNaN(lessThanOrEqualValue)) { + alert(`Invalid numeric values for the criterion "${criterion}".`); + return false; + } + + if (greaterThanOrEqualValue > lessThanOrEqualValue) { + alert(`For the criterion "${criterion}", the value for '>=' must be less than or equal to the value for '<='.`); + return false; + } + } + } + + return true; + }, validateNonInvertedConditions() { let isValid = true; - let conditionPairs = this.conditions.map(c => [c.column1, c.column2].sort().join('-')); + let conditionPairs = this.relativeConditions.map(c => [c.column1, c.column2].sort().join('-')); // Create a Set for unique pairs const uniquePairs = new Set(conditionPairs); @@ -120,94 +258,122 @@ export default { return isValid; }, + addImmediateCondition() { + this.immediateConditions.push({ criterion: '', operator: '', value: 0 }); + }, + removeImmediateCondition(index) { + this.immediateConditions.splice(index, 1); + }, async sendWRData() { - // Check if any condition is set - const isAnyConditionSet = this.conditions.some(condition => condition.column1 && condition.column2 && condition.operator); + // Check if any relative or immediate condition is set + const isAnyRelativeConditionSet = this.relativeConditions.some(condition => condition.column1 && condition.column2 && condition.operator); + const isAnyImmediateConditionSet = this.immediateConditions.some(condition => condition.criterion && condition.operator); - // If no conditions are set, prompt the user - if (!isAnyConditionSet) { + // Filter out incomplete or empty relative constraints + const validRelativeConditions = this.relativeConditions.filter(condition => condition.column1 && condition.column2 && condition.operator); + // Filter out incomplete or empty immediate constraints + const validImmediateConditions = this.immediateConditions.filter(condition => condition.criterion && condition.operator); + + // Prompt the user if no conditions are set + if (!isAnyRelativeConditionSet && !isAnyImmediateConditionSet) { 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; + return; // User chose 'No', do nothing } - // User chose 'Yes', proceed with sending data + // Clear data if user chose 'Yes' + this.relativeConditions = []; + this.immediateConditions = []; } 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!'); + // Validate conditions + if ((isAnyRelativeConditionSet && (!this.validateForm() || !this.validateNonInvertedConditions())) || + (isAnyImmediateConditionSet && (!this.validateImmediateConstraints()))) { return; // Stop if validation fails } } - const operatorMapping = { - '<=': -1, - '=': 0, - '>=': 1 - }; + // Process Relative constraints + const relativeWRData = validRelativeConditions.map(condition => ({ + LHSCriterion: condition.column1, + Operator: this.operatorMapping[condition.operator], + Intense: condition.value, + RHSCriterion: condition.column2 + })); - const processedWRData = this.conditions.map(condition => { - return { - LHSCriterion: condition.column1, - Operator: operatorMapping[condition.operator], - Intense: condition.value, - RHSCriterion: condition.column2 - }; - }); + // Process Immediate constraints + const immediateWRData = validImmediateConditions.map(condition => ({ + Criterion: condition.criterion, + Operator: this.operatorMapping[condition.operator], + Value: condition.value + })); + // Retrieve node names from local storage + let nodeNamesArray = []; + const NodeNamesString = localStorage.getItem('NodeNames'); + if (NodeNamesString) { + nodeNamesArray = JSON.parse(NodeNamesString); + } + + // Prepare payload with filtered conditions const payload = { - gridData: this.receivedGridData, // Data received from DataGrid.vue - wrData: processedWRData + gridData: this.receivedGridData, + relativeWRData: relativeWRData, + immediateWRData: immediateWRData, + nodeNames: nodeNamesArray }; + console.log('Payload being sent to backend from WR.vue:', payload); + // Ask the backend to perform evaluation try { - const response = await fetch('http://127.0.0.1:5000/process-evaluation-data', { + const response = await fetch(apiURL+'/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); + if (!response.ok) { + // If the HTTP response is not OK, throw an error + throw new Error('Network response was not ok'); + } - // 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); - } */ + const data = await response.json(); + console.log('Response from backend process-evaluation-data():', data); + console.log('Response data.results.LPstatus:', data.results.LPstatus); + + // First, check the general status of the response to confirm the request was processed successfully + // Check the LP problem's feasibility status + if (data.status === 'success') { + if (data.results.LPstatus === 'feasible') { + localStorage.setItem('evaluationResults', JSON.stringify(data.results)); + localStorage.setItem('relativeWRData', JSON.stringify(relativeWRData)); + localStorage.setItem('immediateWRData', JSON.stringify(immediateWRData)); + + // Navigate to Results.vue + this.$router.push({ name: 'Results', params: { evaluationResults: data.results.results } }); + } else if (data.results.LPstatus === 'infeasible') { + // Set the error message for infeasible LP solution + this.errorMessage = data.results.message; // Accessing the message directly + alert(this.errorMessage); // Show the message to the user via alert + } + } else { + // Handle other unexpected 'status' + this.errorMessage = 'An unexpected error occurred.'; + } } catch (error) { - console.error('Error sending data to backend:', error); - alert('Failed to send data to backend.'); + console.error('Error:', error); + this.errorMessage = error.message || '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); - }); + goBackToCriteriaSelection() { + this.$router.push({ name: 'CriteriaSelection' }); } } }; </script> <style scoped> +input{height: 40px;} + .wr-container { display: flex; flex-direction: column; @@ -218,6 +384,7 @@ export default { display: flex; align-items: center; gap: 10px; + margin-bottom: 15px; } button { @@ -233,7 +400,7 @@ button { button:hover { background-color: var(--secondary-color); /* Lighter shade of purple on hover */ color: var(--main-color); - border:2px; - border-color:var(--main-color); + border: 2px; + border-color: var(--main-color); } </style> \ No newline at end of file diff --git a/cfsb-frontend/src/components/WR_AllCriteria.vue b/cfsb-frontend/src/components/WR_AllCriteria.vue new file mode 100644 index 0000000..e89c573 --- /dev/null +++ b/cfsb-frontend/src/components/WR_AllCriteria.vue @@ -0,0 +1,357 @@ +<template> + <div class="wr-container"> + <!-- Relative constraints section --> + <div class="relative-constraints"> + <h3>Relative Constraints</h3> + <p class="description"> + Set relative constraints between the criteria. For example, "Weight of Criterion A >= 2* Weight of Criterion B". + </p> + <div v-for="(condition, index) in relativeConditions" :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" step="0.5" 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">+ Add Relative Constraint</button> + </div> + <div class="immediate-constraints"> + <h3>Immediate Constraints</h3> + <p class="description"> + Set immediate constraints on individual criteria. For example, "Weight of Criterion A >= 0.25". + </p> + <div v-for="(immediateCondition, index) in immediateConditions" :key="`immediate-${index}`" class="condition-row"> + <select v-model="immediateCondition.criterion"> + <option value="" disabled>Select Criterion</option> + <option v-for="col in criteria_titles" :key="`immediate-${col}`" :value="col">{{ col }}</option> + </select> + + <select v-model="immediateCondition.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="immediateCondition.value" :min="0" step="0.1" placeholder="Value" /> + + <button @click="removeImmediateCondition(index)">-</button> + </div> + + <button @click="addImmediateCondition">+ Add Immediate Constraint</button> + </div> + <button @click="sendWRData">Run Evaluation</button> + </div> +</template> + +<script> +export const backendURL = import.meta.env.VITE_BACKEND_URL; +const apiURL = backendURL; +import {useRouter} from 'vue-router'; + +export default { + data() { + return { + receivedGridData: null, + relativeConditions: [{column1: '', operator: '', value: 0, column2: ''}], + criteria_titles: [], // This is populated with the column titles + operators: ['>=', '=', '<='], + immediateConditions: [{criterion: '', operator: '', value: 0}], + operatorMapping: { + '<=': -1, + '=': 0, + '>=': 1 + }, + }; + }, + mounted() { + if (this.$route.params.data) { + // Parse the JSON string back into an object + this.receivedGridData = JSON.parse(this.$route.params.data); + } + + const gridDataFromStorage = localStorage.getItem('gridData'); + const wrDataFromStorage = localStorage.getItem('wrData'); + const immediateWRDataFromStorage = localStorage.getItem('immediateWRData'); + + if (gridDataFromStorage) { + this.receivedGridData = JSON.parse(gridDataFromStorage); + } + + if (wrDataFromStorage) { + this.wrData = JSON.parse(wrDataFromStorage); + } + + if (immediateWRDataFromStorage) { + this.immediateConditions = JSON.parse(immediateWRDataFromStorage); + } else { + // Reset immediateConditions if there is no stored data + this.immediateConditions = [{criterion: '', operator: '', value: 0}]; + } + + this.fetchCriteriaTitles(); + + }, + methods: { + fetchCriteriaTitles() { + fetch(apiURL+'/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.relativeConditions.push({column1: '', column2: '', operator: '', value: 0}); + }, + removeCondition(index) { + this.relativeConditions.splice(index, 1); + }, + validateForm() { + for (const condition of this.relativeConditions) { + if (!condition.column1 || !condition.column2) { + alert('Please select criteria for each relative constraint.'); + return false; + } + if (!condition.operator) { + alert('Please select an operator for each relative constraint.'); + return false; + } + if (condition.value === null || condition.value === '') { + alert('Please enter a numeric value for each relative constraint.'); + return false; + } + if (condition.value <= 0) { + alert('The priority in each relative constraint must be greater than zero.'); + return false; + } + } + + const uniquePairs = new Set( + this.relativeConditions.map(c => [c.column1, c.column2].sort().join('-')) + ); + + if (uniquePairs.size !== this.relativeConditions.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.relativeConditions[index].column2); + } else { + // For the second dropdown, filter out the column selected in the first dropdown + return this.criteria_titles.filter(col => col !== this.relativeConditions[index].column1); + } + }, + // Add a method to validate the Immediate Constraints + validateImmediateConstraints() { + let criterionConstraints = {}; + + // Iterate over immediate conditions and organize them by criterion + for (const condition of this.immediateConditions) { + if (!condition.criterion || !condition.operator) { + continue; // Skip empty conditions + } + + // Ensure value is greater than 0 + if (condition.value === null || condition.value === '' || condition.value <= 0) { + alert(`The importance of criterion "${condition.criterion}" should be greater than 0.`); + return false; + } + + // Initialize the constraints list for the criterion if not already done + if (!criterionConstraints[condition.criterion]) { + criterionConstraints[condition.criterion] = {}; + } + + // Check for duplicate operators + if (criterionConstraints[condition.criterion][condition.operator]) { + alert(`You cannot use the same operator more than once for the criterion "${condition.criterion}".`); + return false; + } + + // Add the condition to the list for the criterion + criterionConstraints[condition.criterion][condition.operator] = condition.value; + } + + // Iterate over the constraints for each criterion and apply validation rules + for (const [criterion, operators] of Object.entries(criterionConstraints)) { + // Only one constraint allowed when using '=' operator + if (operators['='] !== undefined && Object.keys(operators).length > 1) { + alert(`Only one constraint allowed for '${criterion}' when using '=' operator.`); + return false; + } + + // Validate logical consistency between '>=' and '<=' values + if (operators['>='] !== undefined && operators['<='] !== undefined) { + const greaterThanOrEqualValue = parseFloat(operators['>=']); + const lessThanOrEqualValue = parseFloat(operators['<=']); + + if (isNaN(greaterThanOrEqualValue) || isNaN(lessThanOrEqualValue)) { + alert(`Invalid numeric values for the criterion "${criterion}".`); + return false; + } + + if (greaterThanOrEqualValue > lessThanOrEqualValue) { + alert(`For the criterion "${criterion}", the value for '>=' must be less than or equal to the value for '<='.`); + return false; + } + } + } + + return true; + }, + validateNonInvertedConditions() { + let isValid = true; + let conditionPairs = this.relativeConditions.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; + }, + addImmediateCondition() { + this.immediateConditions.push({criterion: '', operator: '', value: 0}); + }, + removeImmediateCondition(index) { + this.immediateConditions.splice(index, 1); + }, + async sendWRData() { + const operatorMapping = {'<=': -1, '=': 0, '>=': 1}; + // Check if any relative or immediate condition is set + const isAnyRelativeConditionSet = this.relativeConditions.some(condition => condition.column1 && condition.column2 && condition.operator); + const isAnyImmediateConditionSet = this.immediateConditions.some(condition => condition.criterion && condition.operator); + + // Filter out incomplete or empty relative constraints + const validRelativeConditions = this.relativeConditions.filter(condition => condition.column1 && condition.column2 && condition.operator); + // Filter out incomplete or empty immediate constraints + const validImmediateConditions = this.immediateConditions.filter(condition => condition.criterion && condition.operator); + + // Prompt the user if no conditions are set + if (!isAnyRelativeConditionSet && !isAnyImmediateConditionSet) { + const proceedWithoutWR = confirm("Would you like to proceed without imposing Weight Restrictions?"); + if (!proceedWithoutWR) { + return; // User chose 'No', do nothing + } + // Clear data if user chose 'Yes' + this.relativeConditions = []; + this.immediateConditions = []; + } else { + // Validate conditions + if ((isAnyRelativeConditionSet && (!this.validateForm() || !this.validateNonInvertedConditions())) || + (isAnyImmediateConditionSet && (!this.validateImmediateConstraints()))) { + return; // Stop if validation fails + } + } + + // Process Relative constraints + const RelativeWRData = validRelativeConditions.map(condition => ({ + LHSCriterion: condition.column1, + Operator: this.operatorMapping[condition.operator], + Intense: condition.value, + RHSCriterion: condition.column2 + })); + + // Process Immediate constraints + const immediateWRData = validImmediateConditions.map(condition => ({ + Criterion: condition.criterion, + Operator: this.operatorMapping[condition.operator], + Value: condition.value + })); + + // Prepare payload with filtered conditions + const payload = { + gridData: this.receivedGridData, + wrData: RelativeWRData, + immediateWRData: immediateWRData + }; + + + try { + const response = await fetch(apiURL+'/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') { + localStorage.setItem('gridData', JSON.stringify(this.receivedGridData)); + localStorage.setItem('wrData', JSON.stringify(RelativeWRData)); + + this.$router.push({name: 'Results'}); + } else { + 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.'); + } + } + } +}; +</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> \ No newline at end of file diff --git a/cfsb-frontend/src/index.js b/cfsb-frontend/src/index.js index d1f697b..ac48409 100644 --- a/cfsb-frontend/src/index.js +++ b/cfsb-frontend/src/index.js @@ -1,7 +1,6 @@ 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 diff --git a/cfsb-frontend/src/router.js b/cfsb-frontend/src/router.js index 7e91f98..ca25264 100644 --- a/cfsb-frontend/src/router.js +++ b/cfsb-frontend/src/router.js @@ -1,4 +1,3 @@ -// router.js import { createRouter, createWebHistory } from 'vue-router'; import App from './App.vue'; import HierarchicalCategoryList from '@/components/HierarchicalCategoryList.vue'; diff --git a/charts/nebulous-cloud-fog-service-broker/templates/config-map-postgresql.yaml b/charts/nebulous-cloud-fog-service-broker/templates/config-map-postgresql.yaml new file mode 100644 index 0000000..def7fe3 --- /dev/null +++ b/charts/nebulous-cloud-fog-service-broker/templates/config-map-postgresql.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: db-init-script +data: + db_script.sql: | + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + uuid VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL + ); + + CREATE TABLE IF NOT EXISTS apps ( + id SERIAL PRIMARY KEY, + user_uuid VARCHAR(255) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + app_id VARCHAR(255) NOT NULL + ); + + INSERT INTO users (username, password, uuid) VALUES ('greg', '12345', 'e3ff4006-be5f-4e00-bbe1-e49a88b2541a'); + INSERT INTO apps (user_uuid, title, description, app_id) VALUES ('e3ff4006-be5f-4e00-bbe1-e49a88b2541a', 'Demo App', 'Demo App description', '2f7cc63df4b1da7532756f44345758da'); diff --git a/charts/nebulous-cloud-fog-service-broker/templates/deployment-postgres.yaml b/charts/nebulous-cloud-fog-service-broker/templates/deployment-postgres.yaml new file mode 100644 index 0000000..87ee8d6 --- /dev/null +++ b/charts/nebulous-cloud-fog-service-broker/templates/deployment-postgres.yaml @@ -0,0 +1,61 @@ +{{ if .Values.postgresql.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nebulous-cloud-fog-service-broker.fullname" . }}-postgresql + labels: + {{- include "nebulous-cloud-fog-service-broker.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "nebulous-cloud-fog-service-broker.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "nebulous-cloud-fog-service-broker.selectorLabels" . | nindent 8 }} + spec: + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: {{ .Values.postgresql.volumeMounts.data.claimName }} + - name: init-script + configMap: + name: {{ .Values.postgresql.volumeMounts.initScript.configMapName }} + containers: + - name: postgresql + image: "{{ .Values.postgresql.image }}" + ports: + - name: postgresql + containerPort: {{ .Values.postgresql.port }} + protocol: TCP + env: + - name: POSTGRES_USER + value: "{{ .Values.postgresql.user }}" + - name: POSTGRES_PASSWORD + value: "{{ .Values.postgresql.password }}" + - name: POSTGRES_DB + value: "{{ .Values.postgresql.dbName }}" + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data/ + - name: init-script + mountPath: /docker-entrypoint-initdb.d + + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nebulous-cloud-fog-service-broker.fullname" . }}-postgresql + labels: + {{- include "nebulous-cloud-fog-service-broker.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: {{ .Values.postgresql.port }} + targetPort: postgresql + protocol: TCP + name: postgresql + selector: + {{- include "nebulous-cloud-fog-service-broker.selectorLabels" . | nindent 4 }} +{{ end }} diff --git a/charts/nebulous-cloud-fog-service-broker/templates/deployment.yaml b/charts/nebulous-cloud-fog-service-broker/templates/deployment.yaml index 9dc8921..d623733 100644 --- a/charts/nebulous-cloud-fog-service-broker/templates/deployment.yaml +++ b/charts/nebulous-cloud-fog-service-broker/templates/deployment.yaml @@ -35,18 +35,29 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http - containerPort: 8080 + containerPort: 8001 protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http resources: {{- toYaml .Values.resources | nindent 12 }} + env: + - name: NEBULOUS_BROKER_URL + value: "{{ .Values.customEnv.NEBULOUS_BROKER_URL }}" + - name: NEBULOUS_BROKER_PORT + value: "{{ .Values.customEnv.NEBULOUS_BROKER_PORT }}" + - name: NEBULOUS_BROKER_USERNAME + value: "{{ .Values.customEnv.NEBULOUS_BROKER_USERNAME }}" + - name: NEBULOUS_BROKER_PASSWORD + value: "{{ .Values.customEnv.NEBULOUS_BROKER_PASSWORD }}" + - name: POSTGRES_DB_HOST + value: "{{ .Values.customEnv.POSTGRES_DB_HOST }}" + - name: POSTGRES_DB_NAME + value: "{{ .Values.customEnv.POSTGRES_DB_NAME }}" + - name: POSTGRES_DB_PORT + value: "{{ .Values.customEnv.POSTGRES_DB_PORT }}" + - name: POSTGRES_DB_USER + value: "{{ .Values.customEnv.POSTGRES_DB_USER }}" + - name: POSTGRES_DB_PASS + value: "{{ .Values.customEnv.POSTGRES_DB_PASS }}" {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/nebulous-cloud-fog-service-broker/templates/pvc-postgresql.yaml b/charts/nebulous-cloud-fog-service-broker/templates/pvc-postgresql.yaml new file mode 100644 index 0000000..3895aad --- /dev/null +++ b/charts/nebulous-cloud-fog-service-broker/templates/pvc-postgresql.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgresql-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/charts/nebulous-cloud-fog-service-broker/values.yaml b/charts/nebulous-cloud-fog-service-broker/values.yaml index 53a6a83..1ef68a7 100644 --- a/charts/nebulous-cloud-fog-service-broker/values.yaml +++ b/charts/nebulous-cloud-fog-service-broker/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: - repository: "quay.io/nebulous/cloud-fog-service-broker-java-spring-boot-demo" + repository: "quay.io/nebulous/cloud-fog-service-broker-backend" pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" @@ -80,3 +80,27 @@ nodeSelector: {} tolerations: [] affinity: {} + +customEnv: + NEBULOUS_BROKER_URL: "nebulous-activemq" + NEBULOUS_BROKER_PORT: "61616" + NEBULOUS_BROKER_USERNAME: "admin" + NEBULOUS_BROKER_PASSWORD: "admin" + POSTGRES_DB_HOST: "localhost" + POSTGRES_DB_NAME: "fog_broker" + POSTGRES_DB_PORT: "5432" + POSTGRES_DB_USER: "dbuser" + POSTGRES_DB_PASS: "pass123" + +postgresql: + enabled: true + image: "docker.io/postgres:16" + user: "dbuser" + password: "pass123" + dbName: "fog_broker" + port: 5432 + volumeMounts: + data: + claimName: "postgresql-pvc" + initScript: + configMapName: "db-init-script" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..670956e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.0' +services: + backend: + build: + context: ./cfsb-backend + dockerfile: Dockerfile + ports: + - "8001:8001" + env_file: + - ./cfsb-backend/.env.prod + depends_on: + - db + networks: + cfsb-network: + db: + image: postgres:16 + ports: + - "5432:5432" + environment: + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=pass123 + - POSTGRES_DB=fog_broker + volumes: + - postgres_data:/var/lib/postgresql/data/ + - ./cfsb-backend/db/db_script.sql:/docker-entrypoint-initdb.d/db_script.sql + networks: + cfsb-network: + frontend: + build: + context: ./cfsb-frontend + dockerfile: Dockerfile + ports: + - "8080:80" + networks: + cfsb-network: + +networks: + cfsb-network: + +volumes: + postgres_data: diff --git a/java-spring-boot-demo/.gitignore b/java-spring-boot-demo/.gitignore deleted file mode 100644 index 549e00a..0000000 --- a/java-spring-boot-demo/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -HELP.md -target/ -!.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ diff --git a/java-spring-boot-demo/Dockerfile b/java-spring-boot-demo/Dockerfile deleted file mode 100644 index 427e30e..0000000 --- a/java-spring-boot-demo/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# -# Build stage -# -FROM docker.io/library/maven:3.9.2-eclipse-temurin-17 AS build -COPY src /home/app/src -COPY pom.xml /home/app -RUN mvn -f /home/app/pom.xml clean package - -# -# Package stage -# -FROM docker.io/library/eclipse-temurin:17-jre -COPY --from=build /home/app/target/demo-0.0.1-SNAPSHOT.jar /usr/local/lib/demo.jar -EXPOSE 8080 -ENTRYPOINT ["java","-jar","/usr/local/lib/demo.jar"] diff --git a/java-spring-boot-demo/pom.xml b/java-spring-boot-demo/pom.xml deleted file mode 100644 index 76e0f0e..0000000 --- a/java-spring-boot-demo/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> - <parent> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-parent</artifactId> - <version>3.1.0</version> - <relativePath/> <!-- lookup parent from repository --> - </parent> - <groupId>com.example</groupId> - <artifactId>demo</artifactId> - <version>0.0.1-SNAPSHOT</version> - <name>demo</name> - <description>Demo project for Spring Boot</description> - <properties> - <java.version>17</java.version> - </properties> - <dependencies> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter</artifactId> - </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-web</artifactId> - </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> - </dependency> - </dependencies> - <build> - <plugins> - <plugin> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-maven-plugin</artifactId> - </plugin> - </plugins> - </build> -</project> diff --git a/java-spring-boot-demo/src/main/java/com/example/demo/DemoApplication.java b/java-spring-boot-demo/src/main/java/com/example/demo/DemoApplication.java deleted file mode 100644 index 094d95b..0000000 --- a/java-spring-boot-demo/src/main/java/com/example/demo/DemoApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.demo; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class DemoApplication { - - public static void main(String[] args) { - SpringApplication.run(DemoApplication.class, args); - } - -} diff --git a/java-spring-boot-demo/src/main/java/com/example/demo/DemoController.java b/java-spring-boot-demo/src/main/java/com/example/demo/DemoController.java deleted file mode 100644 index 61a5075..0000000 --- a/java-spring-boot-demo/src/main/java/com/example/demo/DemoController.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.demo; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class DemoController { - - @RequestMapping("/") - public Object root() { - return null; - } - -} diff --git a/java-spring-boot-demo/src/main/resources/application.properties b/java-spring-boot-demo/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/java-spring-boot-demo/src/test/java/com/example/demo/DemoApplicationTests.java b/java-spring-boot-demo/src/test/java/com/example/demo/DemoApplicationTests.java deleted file mode 100644 index eaa9969..0000000 --- a/java-spring-boot-demo/src/test/java/com/example/demo/DemoApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.demo; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class DemoApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml index 293c1c4..b73ea4a 100644 --- a/zuul.d/jobs.yaml +++ b/zuul.d/jobs.yaml @@ -6,17 +6,24 @@ soft: false provides: - nebulous-cloud-fog-service-broker-container-images - description: Build the container images. + description: Build the container images for both the backend and frontend. files: &image_files - - ^java-spring-boot-demo/ + - ^cfsb-backend/ + - ^cfsb-frontend/ vars: &image_vars promote_container_image_job: nebulous-cloud-fog-service-broker-upload-container-images container_images: - - context: java-spring-boot-demo + - context: cfsb-backend registry: quay.io - repository: quay.io/nebulous/cloud-fog-service-broker-java-spring-boot-demo + repository: quay.io/nebulous/cloud-fog-service-broker-backend namespace: nebulous - repo_shortname: cloud-fog-service-broker-java-spring-boot-demo + repo_shortname: cloud-fog-service-broker + repo_description: "" + - context: cfsb-frontend + registry: quay.io + repository: quay.io/nebulous/cloud-fog-service-broker-frontend + namespace: nebulous + repo_shortname: cloud-fog-service-broker repo_description: "" - job: @@ -27,14 +34,14 @@ soft: false provides: - nebulous-cloud-fog-service-broker-container-images - description: Build and upload the container images. + description: Build and upload both the backend and frontend container images. files: *image_files vars: *image_vars - job: name: nebulous-cloud-fog-service-broker-promote-container-images parent: nebulous-promote-container-images - description: Promote previously uploaded container images. + description: Promote previously uploaded backend and frontend container images. files: *image_files vars: *image_vars @@ -44,7 +51,8 @@ description: Run Hadolint on Dockerfile(s). vars: dockerfiles: - - java-spring-boot-demo/Dockerfile + - cfsb-backend/Dockerfile + - cfsb-frontend/Dockerfile - job: name: nebulous-cloud-fog-service-broker-helm-lint