diff --git a/README.md b/README.md index 30cd235..e1325d4 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,8 @@ sudo docker-compose run --rm app poetry run alembic revision --autogenerate -m " sudo docker-compose run --rm app poetry run alembic upgrade head # and you are good to go ``` + + +# to solve the error of ':5432 fetal error: auth failed' +netstat -aon | findstr :5432 +taskkill /PID 15732 /F \ No newline at end of file diff --git a/app/api/api.py b/app/api/api.py index c2860b6..66407f3 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -1,4 +1,4 @@ -from . import teams, metrics,microservice ,microservice_info ,metric_info +from . import teams, metrics,microservice ,microservice_info ,metric_info, scorecard from fastapi import APIRouter apiRouter = APIRouter() @@ -11,4 +11,6 @@ apiRouter.include_router(microservice_info.router , prefix="/serviceinfo",tags=["microserviceinfo"]) -apiRouter.include_router(metric_info.router , prefix="/metricinfo",tags=["metricinfo"]) \ No newline at end of file +apiRouter.include_router(metric_info.router , prefix="/metricinfo",tags=["metricinfo"]) + +apiRouter.include_router(scorecard.router, prefix="/scorecard", tags=["scorecards"]) \ No newline at end of file diff --git a/app/api/handling_exception.py b/app/api/handling_exception.py index 10331be..8e95b4f 100644 --- a/app/api/handling_exception.py +++ b/app/api/handling_exception.py @@ -1,8 +1,9 @@ -from fastapi import Request, HTTPException, FastAPI +from fastapi import Request, HTTPException from fastapi.responses import JSONResponse from starlette import status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError +from app.schemas.apiException import CustomException def my_exception_handler(request: Request, exc: HTTPException): @@ -28,5 +29,8 @@ def validation_exception_handler(request: Request, exc: RequestValidationError): ) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - content=jsonable_encoder({"message": "error in the datatypes", "details" : modified_details}), + content = jsonable_encoder(CustomException( + message="error in the datatypes", + details=modified_details + )) ) \ No newline at end of file diff --git a/app/api/metrics.py b/app/api/metrics.py index 93de8e0..ba6c461 100644 --- a/app/api/metrics.py +++ b/app/api/metrics.py @@ -1,20 +1,16 @@ -from fastapi.exceptions import RequestValidationError -from app.schemas import ServiceMetricCreate, MetricCreate -from fastapi import APIRouter, Depends, Request, exception_handlers, status, Response, HTTPException, FastAPI -from fastapi.responses import JSONResponse, PlainTextResponse +from fastapi import APIRouter, Depends from fastapi.encoders import jsonable_encoder from pydantic import BaseModel, Field -from app.crud import CRUDMetric, CRUDServiceMetric, CRUDMicroserviceScoreCard, CRUDMicroservice -from app.core.security import JWTBearer, decodeJWT -from app import schemas, models, crud, dependencies -from typing import Any, Callable +from app import schemas, crud, dependencies +from typing import Any import json -from fastapi.routing import APIRoute from .exceptions import HTTPResponseCustomized from app.utils.base import format_code +from app.schemas.apiResponse import CustomResponse +from .responses import ResponseCustomized -metric_type = ["integer", "boolean"] +metric_type = ["integer", "boolean", "string", "float"] class Value(BaseModel): @@ -26,7 +22,7 @@ class Value(BaseModel): # ADD NEW METRIC HERE -@router.post("/", response_model=schemas.Metric) +@router.post("/", response_model=schemas.Metric, response_class=ResponseCustomized) def createMetric(metric: schemas.MetricCreate, metricCrud: crud.CRUDMetric = Depends(dependencies.getMetricsCrud)) -> schemas.Metric: metricObj = metric # change ' ' with '-' @@ -51,13 +47,12 @@ def createMetric(metric: schemas.MetricCreate, metricCrud: crud.CRUDMetric = Dep # Handling the datatype of the field type to be integer or boolean if (metricObj.type not in metric_type): raise HTTPResponseCustomized( - status_code=422, detail="type must be integer or boolean") + status_code=422, detail="type must be valid") metricCrud.create(metricObj) - raise HTTPResponseCustomized( - status_code=200, detail="Success in creating metric") + return ResponseCustomized("Success in creating metric") # Get Any Metric Here By ID with parsing the list to be a real list not just stringified -@router.get("/{metricID}", response_model=schemas.Metric) +@router.get("/{metricID}", response_model=schemas.Metric, response_class=ResponseCustomized) def getMetric(metricID: int, metricCrud: crud.CRUDMetric = Depends(dependencies.getMetricsCrud)) -> Any: metric = metricCrud.get(metricID) try: @@ -66,29 +61,27 @@ def getMetric(metricID: int, metricCrud: crud.CRUDMetric = Depends(dependencies. metric.area = [] metric.area = json.loads(metric.area) metricOBJ = jsonable_encoder(metric) - raise HTTPResponseCustomized(status_code=200, detail=metricOBJ) + return ResponseCustomized(metricOBJ) # Delete any metric using its own ID -@router.delete("/{metricID}") +@router.delete("/{metricID}", response_model=CustomResponse, response_class=ResponseCustomized) def deleteMetric(metricID: int, metricCrud: crud.CRUDMetric = Depends(dependencies.getMetricsCrud)) -> Any: # this line is used to check if the metric is found to be deleted as # if it is not found this will auto raise an error "Not Found" metric = metricCrud.get(metricID) metricCrud.delete(metricID) - raise HTTPResponseCustomized( - status_code=200, detail="deleted successfully") + return ResponseCustomized("Metric is deleted successfully") - -@router.put("/{metricID}") +@router.put("/{metricID}", response_model=CustomResponse, response_class=ResponseCustomized) def editMetric(metricID: int, metricInput: schemas.MetricUpdate, metricCrud: crud.CRUDMetric = Depends(dependencies.getMetricsCrud)) -> Any: metric = metricCrud.get(metricID) metricObj = metricInput if (metricInput.name): metricObj.code = format_code(metricInput.name) if (metricCrud.getByCode(metricObj.code) and metricObj.id == metric.id): - raise HTTPResponseCustomized( - status_code=422, detail="name already exists") - if (metricInput.area != None): + raise HTTPResponseCustomized(status_code=422, detail="name already exists") + # Stringfying the list of areas to be saved as string + if (metricObj.area != None): if any(item == "" for item in metricObj.area): raise HTTPResponseCustomized( status_code=422, detail="area can not have an empty string or more") @@ -96,28 +89,26 @@ def editMetric(metricID: int, metricInput: schemas.MetricUpdate, metricCrud: cru # I use set to remove all duplicates and return it back to list cuz set is not json serializable metricObj.area = list(set(metricInput.area)) metricObj.area = json.dumps(metricInput.area) - # Well it will throw the error but i can not test it as i dont know how the error will come as XD - # need help for testing it. except (TypeError, ValueError) as e: raise ValueError(f"Invalid area datatype: {e}") + elif(metric.area != None): + metricObj.area = metric.area else: metric.area = [] metricObj.area = json.dumps(metric.area) - if (metricObj.type and metricObj.type not in metric_type): + if (metricObj.type not in metric_type and metricObj.type): raise HTTPResponseCustomized( - status_code=422, detail="type must be integer or boolean") + status_code=422, detail="type must be valid") metricCrud.update(metricID, metricObj) - raise HTTPResponseCustomized(status_code=200, detail="Success in Editing") + return ResponseCustomized(f"Success, The metric {metricID} has been edited successfully.") # get all the metrics - - -@router.get("/", response_model=schemas.List[schemas.Metric]) +@router.get("/", response_model=schemas.List[schemas.Metric], response_class=ResponseCustomized) def getAllMetrics(metricCrud: crud.CRUDMetric = Depends(dependencies.getMetricsCrud)) -> Any: metrics = metricCrud.list() metricsOBJ = [] + print (metrics) for metric in metrics: metric.area = json.loads(metric.area) metricsOBJ.append(metric) - raise HTTPResponseCustomized( - status_code=200, detail=jsonable_encoder(metricsOBJ)) + return ResponseCustomized(jsonable_encoder(metricsOBJ)) diff --git a/app/api/microservice.py b/app/api/microservice.py index c4941c0..3b6b568 100644 --- a/app/api/microservice.py +++ b/app/api/microservice.py @@ -1,13 +1,13 @@ from fastapi import APIRouter, Depends, status -from app.schemas import MicroserviceInDBBase, MicroserviceCreate, MicroserviceTeamScorecardBase, MicroserviceCreateApi, MicroserviceScoreCardCreate, MicroserviceUpdate,ServiceMetricReading -from app.crud import CRUDMicroservice, CRUDMicroserviceTeamScorecard, CRUDTeam, CRUDScoreCard, CRUDMicroserviceScoreCard ,CRUDServiceMetric -from typing import List , Optional -from datetime import datetime +from app.schemas import MicroserviceInDBBase, MicroserviceCreate, MicroserviceTeamScorecardBase, MicroserviceCreateApi, MicroserviceScoreCardCreate, MicroserviceUpdate, ServiceMetricReading, ServiceMetricCreate +from app.crud import CRUDMicroservice, CRUDMicroserviceTeamScorecard, CRUDTeam, CRUDScoreCard, CRUDMicroserviceScoreCard, CRUDMetric, CRUDServiceMetric +from typing import List, Optional +from datetime import datetime, timezone from app import dependencies from pydantic import BaseModel, Field -from sqlalchemy.orm import Session from .exceptions import HTTPResponseCustomized from app.utils.base import format_code +from app.utils import utity_datatype class Value(BaseModel): @@ -46,6 +46,7 @@ async def get_one_service(service_id: int, microServices: CRUDMicroservice = Dep name=service.name, description=service.description, code=service.code, + teamId= service.teamId, team_name=teamobj.name if teamobj else None, ) @@ -109,7 +110,7 @@ def create_microservice(newmicroservice: MicroserviceCreateApi, pass if newmicroservice.scorecardids: - scorecard_objs = scorecard.getByScoreCradIds( + scorecard_objs = scorecard.getByScoreCardIds( newmicroservice.scorecardids) if len(scorecard_objs) != len(newmicroservice.scorecardids): missing_ids = set(newmicroservice.scorecardids) - \ @@ -123,6 +124,7 @@ def create_microservice(newmicroservice: MicroserviceCreateApi, code=formatted_code)) if newmicroservice.scorecardids is not None: for scorecard_obj in scorecard_objs: + try: servicescorecard.create(MicroserviceScoreCardCreate( microserviceId=created_microservice.id, @@ -135,7 +137,9 @@ def create_microservice(newmicroservice: MicroserviceCreateApi, return created_microservice # update operation -@router.put("/{servise_id}", response_model=None) + + +@router.put("/{microservice_id}", response_model=None) def update_microservice(microservice_id: int, updatemicroservice: MicroserviceCreateApi, microservice: CRUDMicroservice = Depends( dependencies.getMicroservicesCrud), @@ -144,10 +148,16 @@ def update_microservice(microservice_id: int, updatemicroservice: MicroserviceCr servicescorecard: CRUDMicroserviceScoreCard = Depends( dependencies.getMicroserviceScoreCardsCrud), scorecard: CRUDScoreCard = Depends(dependencies.getScoreCardsCrud)): + + existing_microservice = microservice.get(microservice_id) + if not existing_microservice: + raise HTTPResponseCustomized(status_code=404, detail="Microservice not found") if not updatemicroservice.name: - raise HTTPResponseCustomized( - status_code=400, detail="Name cannot be empty") + updatemicroservice.name = existing_microservice.name + + description = updatemicroservice.description or existing_microservice.description + teamId = updatemicroservice.teamId or existing_microservice.teamId if len(updatemicroservice.name) < 3: raise HTTPResponseCustomized( @@ -161,18 +171,9 @@ def update_microservice(microservice_id: int, updatemicroservice: MicroserviceCr raise HTTPResponseCustomized( status_code=400, detail="Description cannot exceed 500 characters") - try: - teamobj = teamservice.get(updatemicroservice.teamId) - if teamobj is None: - raise HTTPResponseCustomized( - status_code=404, detail="Not Found") - except Exception as x: - error_message = 'Team Id was not found' - raise HTTPResponseCustomized(status_code=404, detail=error_message) - scorecard_objs = [] if updatemicroservice.scorecardids: - scorecard_objs = scorecard.getByScoreCradIds( + scorecard_objs = scorecard.getByScoreCardIds( updatemicroservice.scorecardids) if len(scorecard_objs) != len(updatemicroservice.scorecardids): missing_ids = set(updatemicroservice.scorecardids) - \ @@ -180,11 +181,16 @@ def update_microservice(microservice_id: int, updatemicroservice: MicroserviceCr raise HTTPResponseCustomized( status_code=404, detail=f"ScoreCard(s) with ID(s): {missing_ids} were not found") + else: + existing_scorecards = servicescorecard.getByServiceId(microservice_id) + updatemicroservice.scorecardids = [sc.scoreCardId for sc in existing_scorecards] + + formatted_code = format_code(updatemicroservice.name) updated_microservice = microservice.update(microservice_id, MicroserviceUpdate( name=updatemicroservice.name, - description=updatemicroservice.description, - teamId=updatemicroservice.teamId, + description=description, + teamId=teamId, code=formatted_code )) servicescorecard.deleteByServiceId(microservice_id) @@ -195,7 +201,7 @@ def update_microservice(microservice_id: int, updatemicroservice: MicroserviceCr return updated_microservice -@router.delete("/{service_id}") +@router.delete("/{microservice_id}") def delete_microservice( microservice_id: int, microservice: CRUDMicroservice = Depends( @@ -216,15 +222,65 @@ def delete_microservice( status_code=404, detail="Can't delete Microservice ScoreCard") return {"message": "Microservice and associated scorecards successfully deleted"} - - -@router.get("/{service_id}/metric_reading", response_model=list[ServiceMetricReading]) -def get_metrics(service_id: int, from_date: Optional[datetime] = None, - to_date: Optional[datetime] = None, service_metric_crud: CRUDServiceMetric = Depends(dependencies.getServiceMetricsCrud)): - metrics = service_metric_crud.get_metric_values_by_service(service_id, from_date,to_date) - + +@router.post("/{service_id}/metric_reading", response_model=list[ServiceMetricReading]) +def get_metrics(service_id: int, from_date: Optional[datetime] = None, + to_date: Optional[datetime] = None, service_metric_crud: CRUDServiceMetric = Depends(dependencies.getServiceMetricsCrud)): + + metrics = service_metric_crud.get_metric_values_by_service( + service_id, from_date, to_date) + if not metrics: - raise HTTPResponseCustomized(status_code=404, detail="Metrics not found for this service and metric.") + raise HTTPResponseCustomized( + status_code=404, detail="Metrics not found for this service and metric.") + + return metrics + + +@router.post("/{service_id}/{metric_id}/reading", response_model=None) +def create_metric_reading( + service_id: int, + metric_id: int, + newmservicemetric: ServiceMetricCreate, + microservice: CRUDMicroservice = Depends( + dependencies.getMicroservicesCrud), + servicemetric: CRUDServiceMetric = Depends( + dependencies.getServiceMetricsCrud), + metric: CRUDMetric = Depends(dependencies.getMetricsCrud), +): + + microservice_obj = microservice.get(service_id) + if not microservice_obj: + raise HTTPResponseCustomized( + status_code=404, detail="Service not found") + + metric_obj = metric.get(metric_id) + if not metric_obj: + raise HTTPResponseCustomized( + status_code=404, detail="Metric not found") + + try: + metric_value = utity_datatype.parse_stringified_value(newmservicemetric.value, metric_obj.type) + except ValueError as e: + raise HTTPResponseCustomized( + status_code=400, + detail=f"Invalid value for metric '{metric_obj.name}': {str(e)}. Expected type: {metric_obj.type}" + ) + + date = newmservicemetric.timestamp or datetime.now() + date_utc = date.astimezone(timezone.utc) + if date_utc > datetime.now(timezone.utc): + raise HTTPResponseCustomized( + status_code=400, detail="Timestamp cannot be in the future") + + service_metric = servicemetric.create( + ServiceMetricCreate( + serviceId= service_id, + metricId= metric_id, + value=metric_value, + timestamp=date, + ) + ) - return metrics \ No newline at end of file + return service_metric diff --git a/app/api/microservice_info.py b/app/api/microservice_info.py index 34936a1..945732c 100644 --- a/app/api/microservice_info.py +++ b/app/api/microservice_info.py @@ -4,7 +4,7 @@ from typing import List from app import dependencies from pydantic import BaseModel, Field -from .exceptions import HTTPResponseCustomized +from app.api.exceptions import HTTPResponseCustomized class Value(BaseModel): @@ -17,8 +17,7 @@ class Value(BaseModel): @router.get("/{service_id}", response_model=MicroserviceInfoBase) async def getmicroservice_info(service_id: int, microServiceinfo: CRUDMicroserviceInfo = Depends(dependencies.getMicroserviceInfoCrud)): - service = microServiceinfo.getServiceInfo( - service_id) + service = microServiceinfo.getServiceInfo(service_id) if service is None: raise HTTPResponseCustomized(status_code=404, detail="Service not found") return service \ No newline at end of file diff --git a/app/api/responses.py b/app/api/responses.py new file mode 100644 index 0000000..027b4e8 --- /dev/null +++ b/app/api/responses.py @@ -0,0 +1,22 @@ +from fastapi.responses import JSONResponse +from fastapi.encoders import jsonable_encoder +import json +from app.schemas.apiResponse import CustomResponse + + +class ResponseCustomized(JSONResponse): + def render(self, data) -> JSONResponse: + if isinstance(data,str): + content = CustomResponse( + message = data + ) + else: + content = data + content = jsonable_encoder(content, custom_encoder={bool: lambda o: o}) + return json.dumps( + content, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") \ No newline at end of file diff --git a/app/api/scorecard.py b/app/api/scorecard.py new file mode 100644 index 0000000..57fb782 --- /dev/null +++ b/app/api/scorecard.py @@ -0,0 +1,226 @@ +from app import dependencies +from fastapi import APIRouter, Depends +from app import schemas, crud +from typing import Any, List +from app.utils.base import format_code, check_metric, stringify_value +from .exceptions import HTTPResponseCustomized +from .responses import ResponseCustomized +from app.schemas.apiResponse import CustomResponse +from app.schemas.scorecardServiceMetric import ScorecardServiceMetricCreate +from app.crud.scorecardServiceMetric import CRUDScoreCardServiceMetric + + +router = APIRouter() +@router.post("/", response_model=CustomResponse, response_class=ResponseCustomized) +def createScoreCard(scoreCardInput: ScorecardServiceMetricCreate, + scoreCardCrud: crud.CRUDScoreCard = Depends( + dependencies.getScoreCardsCrud), + serviceScorecardCrud: crud.CRUDMicroserviceScoreCard = Depends( + dependencies.getMicroserviceScoreCardsCrud), + scorecardMetricCrud: crud.CRUDScoreCardMetric = Depends( + dependencies.getScoreCardMetricsCrud), + serviceCrud: crud.CRUDMicroservice = Depends( + dependencies.getMicroservicesCrud), + metricCrud: crud.CRUDMetric = Depends( + dependencies.getMetricsCrud), + scorecard: crud.CRUDScoreCard = Depends( + dependencies.getScoreCardsCrud) + ): + # Create the scorecard. + scoreCardInput.code = format_code(scoreCardInput.name) + scorecard = schemas.ScoreCardCreate( + name=scoreCardInput.name, + code=scoreCardInput.code, + description=scoreCardInput.description + ) + # if the scorecardids are passed duplicated + servicesIDsSet = list(set(scoreCardInput.services)) + # i need to check if the serviceid is found or not + services = serviceCrud.getByServiceIds(servicesIDsSet) + serviceListToBeCreated = [] + if (len(services) != len(servicesIDsSet)): + raise HTTPResponseCustomized(status_code=400, detail="Service not found") + + metricIds = [] + for metric in scoreCardInput.metrics: + metricIds.append(metric.id) + metricIds = list(set(metricIds)) + metric_data = metricCrud.getByIds(metricIds) + if (len(metric_data) != len(metricIds)): + raise HTTPResponseCustomized(status_code=404, detail="Metric is not found") + + if (scoreCardCrud.getByScoreCardCode(scoreCardInput.code)): + raise HTTPResponseCustomized( + status_code=400, detail="Scorecard already found") + try: + obj = scoreCardCrud.create(scorecard) + scorecardID = obj.id + except Exception as e: + raise HTTPResponseCustomized(status_code=422, detail=f"Error: {e}") + + metric_type_map = {metric.id:metric.type for metric in metric_data} + for serviceid in servicesIDsSet: + serviceScorecard = schemas.MicroserviceScoreCardCreate( + scoreCardId=scorecardID, + microserviceId=serviceid + ) + serviceListToBeCreated.append(serviceScorecard) + + + if (scoreCardInput.metrics): + objects = [] + for metric in scoreCardInput.metrics: + metricCreate = schemas.MetricTypeScorecard( + id=metric.id, + weight=metric.weight, + desiredValue=metric.desiredValue, + criteria=metric.criteria, + type=metric_type_map[metric.id] # Add the type directly from retrieved metric + ) + objects.append(metricCreate) + try: + check_metric(objects) + except Exception as e: + scoreCardCrud.delete(scorecardID) + raise e # Return the error message + for metric in scoreCardInput.metrics: + metric.desiredValue = stringify_value(metric.desiredValue) + scorecardmetric = schemas.ScoreCardMetricsCreate( + scoreCardId=scorecardID, + metricId=metric.id, + criteria=metric.criteria, + weight=metric.weight, + desiredValue=metric.desiredValue + ) + scorecardMetricCrud.create(scorecardmetric) + + for servicescorecard in serviceListToBeCreated: + serviceScorecardCrud.create(servicescorecard) + + return ResponseCustomized("Scorecard created successfully") + + +@router.get("/", response_model=List[schemas.listScoreCard], response_class=ResponseCustomized) +def getAllScoreCard(scoreCardCrud: CRUDScoreCardServiceMetric = Depends(dependencies.getScorecardServiceMetric)): + scorecard = scoreCardCrud.getlist() + return ResponseCustomized(scorecard) + + +@router.get("/{scorecardID}", response_model=schemas.ScorecardServiceMetric, response_class=ResponseCustomized) +def getScoreCard(scorecardID: int, scoreCardCrud: CRUDScoreCardServiceMetric = Depends(dependencies.getScorecardServiceMetric)): + scorecard = scoreCardCrud.getwithscorecardIDmetricandservice(scorecardID) + return ResponseCustomized(scorecard) + + +# FINISHED +# Delete one ScoreCard with its own ID +@router.delete("/{scorecardID}", response_model=CustomResponse, response_class=ResponseCustomized) +def deleteScorecard(scorecardID: int, + scoreCardCrud: crud.CRUDScoreCard = Depends( + dependencies.getScoreCardsCrud), + scorecardService: crud.CRUDMicroserviceScoreCard = Depends( + dependencies.getMicroserviceScoreCardsCrud), + scorecardMetrics: crud.CRUDScoreCardMetric = Depends(dependencies.getScoreCardMetricsCrud)) -> Any: + scoreCardCrud.delete(scorecardID) + scorecardService.deleteByScorecardId(scorecardID) + scorecardMetrics.deleteByScorecardId(scorecardID) + return ResponseCustomized("Scorecard deleted successfully") + +@router.put("/{scorecardID}", response_model=schemas.ScorecardServiceMetricUpdate) +def updateScorecard(scorecardID: int, + update_scorecard: schemas.ScorecardServiceMetricUpdate, + scorecardCrud: crud.CRUDScoreCard = Depends(dependencies.getScoreCardsCrud), + scorecardServiceCrud: crud.CRUDMicroserviceScoreCard = Depends(dependencies.getMicroserviceScoreCardsCrud), + scorecardMetricsCrud: crud.CRUDScoreCardMetric = Depends(dependencies.getScoreCardMetricsCrud), + serviceCrud: crud.CRUDMicroservice = Depends(dependencies.getMicroservicesCrud), + metricCrud: crud.CRUDMetric = Depends(dependencies.getMetricsCrud) + ): + + saved_scorecard = scorecardCrud.get(scorecardID) + if (saved_scorecard is None): + raise HTTPResponseCustomized(status_code=404, detail="Scorecard not found") + + if(update_scorecard.name): + old_scorecard = scorecardCrud.getByScoreCardCode(format_code(update_scorecard.name)) + if (old_scorecard): + raise HTTPResponseCustomized(status_code=422, detail="Scorecard name is already found") + + updated_fields = schemas.ScoreCardCreate + if update_scorecard.name: + updated_fields.name = update_scorecard.name + updated_fields.code = format_code(update_scorecard.name) + else: + updated_fields.name = saved_scorecard.name + updated_fields.code = format_code(saved_scorecard.name) + + if update_scorecard.description: + updated_fields.description = update_scorecard.description + else: + updated_fields.description = saved_scorecard.description + + scorecard_update = schemas.ScoreCardUpdate(**vars(updated_fields)) + scorecardCrud.update(scorecardID,scorecard_update) + + # METRICS UPDATE + if update_scorecard.metrics: + metricIDsSet = [] + for metric in update_scorecard.metrics: + metricIDsSet.append(metric.id) + metricIDsSet = list(set(metricIDsSet)) + metric_data = metricCrud.getByIds(metricIDsSet) + if (len(metric_data) != len(metricIDsSet)): + raise HTTPResponseCustomized(status_code=404, detail="Metric is not found") + + metric_type_map = {metric.id:metric.type for metric in metric_data} + + objects = [] + for metric in update_scorecard.metrics: + metric = schemas.MetricTypeScorecard( + id=metric.id, + weight=metric.weight, + desiredValue=metric.desiredValue, + criteria=metric.criteria, + type=metric_type_map[metric.id] # Add the type directly from retrieved metric + ) + objects.append(metric) + + check_metric(objects) + + # Delete old metrics related to this scorecard + scorecardMetricsCrud.deleteByScorecardId(scorecardID) + # Create new ones + for metric in update_scorecard.metrics: + metric.desiredValue = stringify_value(metric.desiredValue) + metricUpdated = schemas.ScoreCardMetricsCreate( + scoreCardId=scorecardID, + metricId=metric.id, + criteria=metric.criteria, + weight=metric.weight, + desiredValue=metric.desiredValue + ) + scorecardMetricsCrud.create(metricUpdated) + + + #SERVICE UPDATE + if update_scorecard.services: + # if the scorecardids are passed duplicated + servicesIDsSet = list(set(update_scorecard.services)) + # i need to check if the serviceid is found or not + services = serviceCrud.getByServiceIds(servicesIDsSet) + if (len(services) != len(servicesIDsSet)): + raise HTTPResponseCustomized(status_code=400, detail="Service not found") + + + # Delete old services related to this scorecard + scorecardServiceCrud.deleteByScorecardId(scorecardID) + # Create new ones + for serviceID in update_scorecard.services: + serviceScorecard = schemas.MicroserviceScoreCardUpdate( + scoreCardId=scorecardID, + microserviceId=serviceID + ) + try: + scorecardServiceCrud.create(serviceScorecard) + except Exception as e: + raise HTTPResponseCustomized(status_code=422, detail=e) + return ResponseCustomized("Scorecard updated successfully") diff --git a/app/api/teams.py b/app/api/teams.py index e62a138..126aec1 100644 --- a/app/api/teams.py +++ b/app/api/teams.py @@ -1,5 +1,5 @@ from typing import Any, List -from app import crud, models, schemas +from app import crud, schemas from app import dependencies from fastapi import APIRouter, Depends diff --git a/app/core/config.py b/app/core/config.py index f3a0f5a..7a78e7a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -12,7 +12,7 @@ class Settings(BaseSettings): PROJECT_DESCRIPTION: Optional[str] = "" PROJECT_VERSION: Optional[str] = "1.0.0" - BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = ['http://localhost:3000'] + BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = ["http://localhost:3000"] @field_validator("BACKEND_CORS_ORIGINS") def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: diff --git a/app/crud/__init__.py b/app/crud/__init__.py index 547a349..195f7dc 100644 --- a/app/crud/__init__.py +++ b/app/crud/__init__.py @@ -8,4 +8,5 @@ from .team import CRUDTeam from .microserviceTeamScorecard import CRUDMicroserviceTeamScorecard from .microserviceInfo import CRUDMicroserviceInfo -from .metric_info import CRUDMetricInfo \ No newline at end of file +from .metric_info import CRUDMetricInfo +from .scorecardServiceMetric import CRUDScoreCardServiceMetric \ No newline at end of file diff --git a/app/crud/base.py b/app/crud/base.py index ca136d5..2c60eb4 100644 --- a/app/crud/base.py +++ b/app/crud/base.py @@ -20,7 +20,7 @@ def __init__(self, model: Type[ModelType], db_session: Session): def get(self, id: Any) -> Optional[ModelType]: obj: Optional[ModelType] = self.db_session.query(self.model).get(id) if obj is None: - raise HTTPResponseCustomized(status_code=404, detail="Not Found") + raise HTTPResponseCustomized(status_code=404, detail=f"{ModelType} Not Found") return obj def list(self, skip: int = 0, limit: int = 100) -> List[ModelType]: diff --git a/app/crud/metric.py b/app/crud/metric.py index 84a122a..2f809bf 100644 --- a/app/crud/metric.py +++ b/app/crud/metric.py @@ -1,6 +1,4 @@ - from sqlalchemy.orm import Session - from ..models import Metric from ..schemas import MetricCreate, MetricUpdate from .base import CRUDBase @@ -12,12 +10,29 @@ def __init__(self, db_session: Session): def getByCode(self, code: str): return self.db_session.query(Metric).filter(Metric.code == code).first() - + + def getIdByCode(self, code: str): + return self.db_session.query(Metric).filter(Metric.code == code).first().id() + + def getById(self, id:int): + #return self.db_session.query(Metric).filter(Metric.id.in_(id)).all() + return self.db_session.query(Metric).filter(Metric.id == id).first() + + def getByIds(self , ids:list[int]): + return self.db_session.query(Metric).filter(Metric.id.in_(ids)).all() + + def getByName(self, name: str): + return self.db_session.query(Metric).filter(Metric.name == name).first() + + def getnamebyid(self, metricID: int): + return self.db_session.query(Metric).filter(Metric.id == metricID).first().name + def get_all_by_ids(self, metric_ids: set): metrics = self.db_session.query(Metric).filter( Metric.id.in_(metric_ids)).all() return metrics - + def getByName(self, name: str): return self.db_session.query(Metric).filter(Metric.name == name).first() + diff --git a/app/crud/metric_info.py b/app/crud/metric_info.py index 1c896ce..f07c658 100644 --- a/app/crud/metric_info.py +++ b/app/crud/metric_info.py @@ -13,8 +13,7 @@ def __init__(self, db_session: Session): def get_latest_metric_readings(self, service_id: int, scorecard_id: int) -> list[MetricInfoBase]: scoremetricobjects = self.scorecardmetric.get_metrics(scorecard_id) - metriclist = self.servicemetric.get_last_metrics( - service_id, scorecard_id) + metriclist = self.servicemetric.get_last_metrics( scorecard_id ,service_id) metric_data = { metric.metricId: metric.weight for metric in scoremetricobjects} metric_ids = [metric.metricId for metric in metriclist] diff --git a/app/crud/microservice.py b/app/crud/microservice.py index b1efeda..613519c 100644 --- a/app/crud/microservice.py +++ b/app/crud/microservice.py @@ -1,10 +1,8 @@ -import sqlalchemy from sqlalchemy.orm import Session from fastapi import status -from fastapi.responses import JSONResponse -from ..models import Microservice ,Team ,Scorecard ,MicroserviceScoreCard -from ..schemas import MicroserviceCreate, MicroserviceUpdate ,MicroserviceInDBBase , TeamInDBBase ,ScoreCardInDBBase -from .base import CRUDBase +from ..models import Microservice ,Team ,MicroserviceScoreCard ,Scorecard +from ..schemas import MicroserviceCreate, MicroserviceUpdate ,MicroserviceInDBBase +from .base import CRUDBase from typing import List from sqlalchemy.sql import func from app.api.exceptions import HTTPResponseCustomized @@ -12,13 +10,21 @@ class CRUDMicroservice(CRUDBase[Microservice, MicroserviceCreate, MicroserviceUpdate]): def __init__(self, db_session: Session): super(CRUDMicroservice, self).__init__(Microservice, db_session) + def getByTeamId(self, teamId: str): return self.db_session.query(Microservice).filter(Microservice.teamId == teamId).all() def getByTeamIdAndCode(self, teamId: str, code: str): return self.db_session.query(Microservice).filter(Microservice.teamId == teamId, Microservice.code == code).first() + + def getByServiceId(self, serviceID: int) -> Microservice: + return self.db_session.query(Microservice).filter(Microservice.id == serviceID).first() + + def getByServiceIds(self, serviceID: list[int]) -> list[Microservice]: + return self.db_session.query(Microservice).filter(Microservice.id.in_(serviceID)).all() + def getAllServicesWithTeamName(self) -> list[MicroserviceInDBBase]: microservices = self.list() @@ -26,19 +32,29 @@ def getAllServicesWithTeamName(self) -> list[MicroserviceInDBBase]: for microservice in microservices: team = self.db_session.query(Team).filter(Team.id == microservice.teamId).first() + scorecard_names = ( + self.db_session.query(Scorecard.name) + .join(MicroserviceScoreCard, Scorecard.id == MicroserviceScoreCard.scoreCardId) + .filter(MicroserviceScoreCard.microserviceId == microservice.id) + .all() + ) + scorecard_names = [name for (name,) in scorecard_names] service = MicroserviceInDBBase( id=microservice.id, name=microservice.name, description=microservice.description, code=microservice.code, + teamId= microservice.teamId, team_name=team.name if team else None, + scorecard_names= scorecard_names + ) services.append(service) return services #get one with team - def getByServiceId(self , service_id:int): + def getByServiceIdWithTeam(self , service_id:int): result = ( self.db_session.query(Microservice.id, Microservice.name, Microservice.description, Microservice.code, Team.name.label("team_name")) .filter(Microservice.id == service_id) @@ -47,7 +63,11 @@ def getByServiceId(self , service_id:int): return result def get_by_code (self , code:str): - return self.db_session.query(Microservice).filter(Microservice.code == code).first() + service = self.db_session.query(Microservice).filter(Microservice.code == code).first() + return service + + def getByCodeReturnIDs (self , code:str): + return self.db_session.query(Microservice).filter(Microservice.code == code).first().id() def check_service_name_exists(self, name: str): diff --git a/app/crud/microserviceInfo.py b/app/crud/microserviceInfo.py index e404c76..c4c2732 100644 --- a/app/crud/microserviceInfo.py +++ b/app/crud/microserviceInfo.py @@ -3,7 +3,7 @@ from ..models import Microservice from ..schemas import MicroserviceInfoBase from .base import CRUDBase -from typing import List +from typing import List from datetime import datetime from sqlalchemy.sql import func from app.api.exceptions import HTTPResponseCustomized @@ -29,25 +29,28 @@ def getServiceInfo(self, service_id: int) -> MicroserviceInfoBase: scorecardIds = self.scoreCardService.getByServiceId(microservice.id) scorecard_ids = [sc_id.scoreCardId for sc_id in scorecardIds] - scorecards = self.scoreCard.getByScoreCradIds(scorecard_ids) - + scorecards = self.scoreCard.getByScoreCardIds(scorecard_ids) + service_scorecards = [] for sc in scorecards: - (calculated_scores, update_time) = self.serviceMetric.get_calculated_value(service_id, sc.id) - service_scorecards.append({ - 'id': sc.id, - 'name': sc.name, - 'code': sc.code, - 'update_time' : update_time, - 'score_value' : calculated_scores - }) - + (calculated_scores, update_time) = self.serviceMetric.get_calculated_value( + service_id, sc.id) + service_scorecards.append({ + 'id': sc.id, + 'name': sc.name, + 'code': sc.code, + 'description': sc.description, + 'update_time': update_time, + 'score_value': calculated_scores + }) + service = MicroserviceInfoBase( id=microservice.id, name=microservice.name, - description=microservice.description, code=microservice.code, + description=microservice.description, team_name=microservice.team.name, scorecards=service_scorecards -) + ) + print(service) return service diff --git a/app/crud/microserviceScoreCard.py b/app/crud/microserviceScoreCard.py index a67b669..adda74f 100644 --- a/app/crud/microserviceScoreCard.py +++ b/app/crud/microserviceScoreCard.py @@ -1,6 +1,6 @@ - +from typing import Any from sqlalchemy.orm import Session -from ..models import MicroserviceScoreCard +from ..models import MicroserviceScoreCard , Scorecard from ..schemas import MicroserviceScoreCardCreate, MicroserviceScoreCardUpdate from .base import CRUDBase from .microservice import CRUDMicroservice @@ -14,11 +14,27 @@ def __init__(self, db_session: Session): def getByTeamId(self, teamId: str): microservices = self.microserviceService.getByTeamId(teamId) return self.db_session.query(MicroserviceScoreCard).filter(MicroserviceScoreCard.microserviceId.in_([microservice.id for microservice in microservices])).all() - + def getByServiceId(self, serviceId: int) -> list[MicroserviceScoreCard]: return self.db_session.query(MicroserviceScoreCard).filter(MicroserviceScoreCard.microserviceId == serviceId).all() def deleteByServiceId(self,serviceid:int): self.db_session.query(MicroserviceScoreCard).filter(MicroserviceScoreCard.microserviceId == serviceid).delete() self.db_session.commit() + + def getservice(self, scorecardId: int) -> MicroserviceScoreCard: + return self.db_session.query(MicroserviceScoreCard).filter(MicroserviceScoreCard.scoreCardId == scorecardId).all() + def deleteByScorecardId(self, scorecardID:int): + self.db_session.query(MicroserviceScoreCard).filter(MicroserviceScoreCard.scoreCardId == scorecardID).delete() + self.db_session.commit() + + def get_scorecard_names_by_service_id(self, service_id: int) -> list[str]: + scorecard_names = ( + self.db_session.query(Scorecard.name) + .join(MicroserviceScoreCard, Scorecard.id == MicroserviceScoreCard.scorecardId) + .filter(MicroserviceScoreCard.serviceId == service_id) + .all() + ) + + return [name for (name,) in scorecard_names] \ No newline at end of file diff --git a/app/crud/microserviceTeamScorecard.py b/app/crud/microserviceTeamScorecard.py index bc0810c..68cb06b 100644 --- a/app/crud/microserviceTeamScorecard.py +++ b/app/crud/microserviceTeamScorecard.py @@ -1,8 +1,5 @@ -import sqlalchemy from sqlalchemy.orm import Session -from ..models import Microservice from ..schemas import MicroserviceTeamScorecardBase, team, scoreCard -from .base import CRUDBase from typing import List from sqlalchemy.sql import func from app.api.exceptions import HTTPResponseCustomized @@ -27,9 +24,9 @@ def getByServiceIdWithTeamAndScoreDetails(self, service_id: int) -> Microservice teamobject = self.teamService.get(microservice.teamId) scorecardIds = self.scoreCardService.getByServiceId(microservice.id) - scorecards = [] + scorecards = [] scorecard_ids = [sc_id.scoreCardId for sc_id in scorecardIds] - scorecards = self.scoreCard.getByScoreCradIds(scorecard_ids) + scorecards = self.scoreCard.getByScoreCardIds(scorecard_ids) service = MicroserviceTeamScorecardBase( id=microservice.id, @@ -37,8 +34,8 @@ def getByServiceIdWithTeamAndScoreDetails(self, service_id: int) -> Microservice description=microservice.description, code=microservice.code, team=team.TeamBase( - id=teamobject.id, name=teamobject.name) if team else None, + id=teamobject.id, name=teamobject.name), scorecards=[scoreCard.ScoreCardInDBBase( - id=sc.id, name=sc.name, description=sc.description) for sc in scorecards] + id=sc.id, name=sc.name, description=sc.description, code=sc.code) for sc in scorecards] ) return service diff --git a/app/crud/scoreCard.py b/app/crud/scoreCard.py index 3eafe7f..7b8ab16 100644 --- a/app/crud/scoreCard.py +++ b/app/crud/scoreCard.py @@ -1,19 +1,23 @@ - from sqlalchemy.orm import Session - from ..models import Scorecard from ..schemas.scoreCard import ScoreCardCreate, ScoreCardUpdate from .base import CRUDBase +from sqlalchemy import func class CRUDScoreCard(CRUDBase[Scorecard, ScoreCardCreate, ScoreCardUpdate]): def __init__(self, db_session: Session): super(CRUDScoreCard, self).__init__(Scorecard, db_session) - def getByScoreCradId(self, ScoreCardId: str): - return self.db_session.query(Scorecard).filter(Scorecard.id == ScoreCardId).all() + def getByScoreCardId(self, ScoreCardId: str) -> Scorecard: + return self.db_session.query(Scorecard).filter(Scorecard.id == ScoreCardId).first() - def getByScoreCradIds(self, ScoreCardIds: list[int]): - ids =[ScoreCardId for ScoreCardId in ScoreCardIds ] + def getByScoreCardIds(self, ScoreCardIds: list[int]): + ids =[ScoreCardId for ScoreCardId in ScoreCardIds] return self.db_session.query(Scorecard).filter(Scorecard.id.in_(ids)).all() - + + def getByScoreCardCode(self, code: str): + return self.db_session.query(Scorecard).filter(Scorecard.code == code).all() + + def getScorecardName(self, ScoreCardId: int): + return self.db_session.query(Scorecard.name).filter(Scorecard.id == ScoreCardId).first() \ No newline at end of file diff --git a/app/crud/scoreCardMetrics.py b/app/crud/scoreCardMetrics.py index 4f1ecfb..447ea73 100644 --- a/app/crud/scoreCardMetrics.py +++ b/app/crud/scoreCardMetrics.py @@ -1,6 +1,4 @@ - from sqlalchemy.orm import Session - from ..models import ScoreCardMetrics from ..schemas import ScoreCardMetricsCreate, ScoreCardMetricsUpdate from .base import CRUDBase @@ -30,3 +28,27 @@ def get_metrics(self, scorecard_id: int) -> list[ScoreCardMetrics]: ).all() return metrics + + + def getbyscorecardID(self, scorecardID: int) -> ScoreCardMetrics: + return self.db_session.query(ScoreCardMetrics).filter(ScoreCardMetrics.scoreCardId == scorecardID).all() + + def getbymetricIDandScorecardID(self, metricID: int, scorecardID: int): + return ( + self.db_session.query(ScoreCardMetrics) + .filter(ScoreCardMetrics.metricId == metricID, ScoreCardMetrics.scoreCardId == scorecardID) + .first() + ) + + def getByMetricIdsandScorecardId(self , metricIds: list[int], scorecardId: int): + return (self.db_session.query(ScoreCardMetrics) + .filter(ScoreCardMetrics.metricId.in_(metricIds), + ScoreCardMetrics.scoreCardId == scorecardId)).all() + + + def deleteByScorecardId(self, scorecardID:int): + self.db_session.query(ScoreCardMetrics).filter(ScoreCardMetrics.scoreCardId == scorecardID).delete() + self.db_session.commit() + + def getIdByScorecardID(self, scorecardID: int) -> list[int]: + return self.db_session.query(ScoreCardMetrics.id).filter(ScoreCardMetrics.scoreCardId == scorecardID).all() \ No newline at end of file diff --git a/app/crud/scorecardServiceMetric.py b/app/crud/scorecardServiceMetric.py new file mode 100644 index 0000000..5a83b85 --- /dev/null +++ b/app/crud/scorecardServiceMetric.py @@ -0,0 +1,103 @@ +from sqlalchemy.orm import Session +from app.schemas.scorecardServiceMetric import ScorecardServiceMetricCreate, ScorecardServiceMetric, ScorecardServiceMetricUpdate +from . import CRUDScoreCardMetric, CRUDMicroserviceScoreCard, CRUDMetric, CRUDScoreCard, CRUDMicroservice +from .base import CRUDBase +from app.utils import format_code +from app.utils.base import parse_stringified_value +from app.schemas.microservice import Microserviceforscorecard +from app.schemas.scoreCardMetrics import MetricListforScorecardGet + +class CRUDScoreCardServiceMetric(CRUDBase[ScorecardServiceMetric, ScorecardServiceMetricCreate, ScorecardServiceMetricUpdate]): + def __init__(self, db_session: Session): + self.scoreCard = CRUDScoreCard(db_session) + self.microServiceScoreCard = CRUDMicroserviceScoreCard(db_session) + self.scoreCardMetric = CRUDScoreCardMetric(db_session) + self.microService = CRUDMicroservice(db_session) + self.metric = CRUDMetric(db_session) + + def getwithscorecardIDmetricandservice(self, scorecardID: int): + # it handles if the scorecard is not found & raises error message + scorecard = self.scoreCard.get(scorecardID) + + # get ids of metrics and services used in this scorecard + metrics = self.scoreCardMetric.getbyscorecardID(scorecardID) + services = self.microServiceScoreCard.getservice(scorecardID) + metric_ids = [sc_id.metricId for sc_id in metrics] + service_ids = [sc_id.microserviceId for sc_id in services] + + metricOBJs = [] + serviceOBJs = [] + + metrics = self.metric.getByIds(metric_ids) + metrics_types_map = {metric.id : metric.type for metric in metrics} + metricList = self.scoreCardMetric.getByMetricIdsandScorecardId(metric_ids, scorecardID) + for metric in metricList: + metrictype = metrics_types_map[metric.metricId] + metric.desiredValue = parse_stringified_value(metric.desiredValue, metrictype) + metricOBJs.append({ + 'id': metric.metricId, + 'criteria': metric.criteria, + 'desiredValue': metric.desiredValue, + 'weight': metric.weight + }) + + serviceList = self.microService.getByServiceIds(service_ids) + serviceOBJs = [Microserviceforscorecard(**vars(service)) for service in serviceList] + + scorecard.code = format_code(scorecard.name) + scorecard = ScorecardServiceMetric( + id=scorecard.id, + name=scorecard.name, + code=scorecard.code, + description=scorecard.description, + metrics=metricOBJs, + services=serviceOBJs + ) + return scorecard.dict() + + def getlist(self): + # Got all the scorecard ids i want + scorecards = self.scoreCard.list() + scorecardIDs = [] + for scorecard in scorecards: + scorecardIDs.append(scorecard.id) + out = [] + for scorecardID in scorecardIDs: + # it handles if the scorecard is not found & raises error message + scorecard = self.scoreCard.get(scorecardID) + + # get ids of metrics and services used in this scorecard + metrics = self.scoreCardMetric.getbyscorecardID(scorecardID) + services = self.microServiceScoreCard.getservice(scorecardID) + metric_ids = [sc_id.metricId for sc_id in metrics] + service_ids = [sc_id.microserviceId for sc_id in services] + metricOBJs = [] + serviceOBJs = [] + + metrics = self.metric.getByIds(metric_ids) + metrics_types_map = {metric.id : metric.type for metric in metrics} + metricList = self.scoreCardMetric.getByMetricIdsandScorecardId(metric_ids, scorecardID) + for metric in metricList: + metrictype = metrics_types_map[metric.metricId] + metric.desiredValue = parse_stringified_value(metric.desiredValue, metrictype) + metricOBJs.append(MetricListforScorecardGet( + id= metric.metricId, + criteria= metric.criteria, + desiredValue= metric.desiredValue, + weight= metric.weight + )) + + serviceList = self.microService.getByServiceIds(service_ids) + serviceOBJs = [Microserviceforscorecard(**vars(service)) for service in serviceList] + + scorecard.code = format_code(scorecard.name) + scorecardOut = ScorecardServiceMetric( + id=scorecard.id, + name=scorecard.name, + code=scorecard.code, + description=scorecard.description, + metrics=metricOBJs, + services=serviceOBJs + ) + out.append(scorecardOut.dict()) + return out diff --git a/app/crud/serviceMetric.py b/app/crud/serviceMetric.py index bf49ac2..08bf8b6 100644 --- a/app/crud/serviceMetric.py +++ b/app/crud/serviceMetric.py @@ -7,7 +7,7 @@ from . import CRUDScoreCardMetric, CRUDMetric from app.utils.utility_functions import main_calculate_error, calculate_score from typing import Optional -from datetime import datetime +from datetime import datetime, timezone class CRUDServiceMetric(CRUDBase[ServiceMetric, ServiceMetricCreate, ServiceMetricUpdate]): @@ -21,32 +21,43 @@ def getByScorecardId(self, scorecardId: int) -> list[ServiceMetric]: def getByServiceId(self, serviceId: int) -> list[ServiceMetric]: return self.db_session.query(ServiceMetric).filter(ServiceMetric.serviceId == serviceId).all() + def get_metric_values_by_service(self, service_id: int, from_date: Optional[datetime], to_date: Optional[datetime]) -> list[ServiceMetricReading]: - query = self.db_session.query(ServiceMetric.metricId, ServiceMetric.value, ServiceMetric.timestamp).filter( + query = self.db_session.query(ServiceMetric.serviceId, ServiceMetric.metricId, ServiceMetric.value, ServiceMetric.timestamp).filter( ServiceMetric.serviceId == service_id) - if from_date and to_date: + if from_date: + from_date_utc = from_date.astimezone(timezone.utc) + else: + from_date_utc = None + + if to_date: + to_date_utc = to_date.astimezone(timezone.utc) + else: + to_date_utc = None + + if from_date_utc and to_date_utc: query = query.filter(ServiceMetric.timestamp >= - from_date, ServiceMetric.timestamp <= to_date) - elif from_date: - query = query.filter(ServiceMetric.timestamp >= from_date) - elif to_date: - query = query.filter(ServiceMetric.timestamp <= to_date) + from_date, ServiceMetric.timestamp <= to_date_utc) + elif from_date_utc: + query = query.filter(ServiceMetric.timestamp >= from_date_utc) + elif to_date_utc: + query = query.filter(ServiceMetric.timestamp <= to_date_utc) query = query.order_by(ServiceMetric.timestamp.desc()) metrics = query.all() return metrics def get_last_metrics(self, scorecard_id: int, service_id: int) -> list[ServiceMetric]: + metrics=self.scorecardMetrics.getMetricByScoreCradId(scorecard_id) subquery = self.db_session.query( ServiceMetric.metricId, ServiceMetric.value, ServiceMetric.timestamp ).filter( ServiceMetric.serviceId == service_id, - ServiceMetric.metricId.in_( - self.scorecardMetrics.getMetricByScoreCradId(scorecard_id)) + ServiceMetric.metricId.in_(metrics) ).order_by(ServiceMetric.metricId, ServiceMetric.timestamp.desc()).distinct(ServiceMetric.metricId).all() return subquery diff --git a/app/crud/team.py b/app/crud/team.py index 3236fb2..da1d01b 100644 --- a/app/crud/team.py +++ b/app/crud/team.py @@ -3,7 +3,6 @@ from ..schemas import TeamCreate, TeamUpdate from .base import CRUDBase - class CRUDTeam(CRUDBase[Team, TeamCreate, TeamUpdate]): def __init__(self, db_session: Session): super(CRUDTeam, self).__init__(Team, db_session) diff --git a/app/dependencies.py b/app/dependencies.py index a1a3888..bf72f42 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -43,3 +43,6 @@ def getMicroserviceInfoCrud(db_session: Session = Depends(get_db)): def getMetricInfoCrud(db_session: Session = Depends(get_db)): return crud.CRUDMetricInfo(db_session) + +def getScorecardServiceMetric(db_session: Session = Depends(get_db)): + return crud.CRUDScoreCardServiceMetric(db_session) \ No newline at end of file diff --git a/app/main.py b/app/main.py index f4d982a..c5996da 100644 --- a/app/main.py +++ b/app/main.py @@ -7,14 +7,15 @@ from fastapi.encoders import jsonable_encoder from app.api.exceptions import HTTPResponseCustomized from app.api.handling_exception import my_exception_handler, validation_exception_handler - +from app.api.responses import CustomResponse from app.api.api import apiRouter from app.core.config import settings from app.views.dashboard import router as dashboard_router +from app.api.responses import ResponseCustomized def create_app() -> FastAPI: - _app = FastAPI(title=settings.PROJECT_NAME, description=settings.PROJECT_DESCRIPTION, version=settings.PROJECT_VERSION) + _app = FastAPI(title=settings.PROJECT_NAME, description=settings.PROJECT_DESCRIPTION, version=settings.PROJECT_VERSION, default_response_class=ResponseCustomized) _app.add_middleware( CORSMiddleware, @@ -43,5 +44,4 @@ async def health() -> str: return _app - app = create_app() diff --git a/app/migrations/versions/bb6d2911e2c9_database.py b/app/migrations/versions/bb6d2911e2c9_database.py new file mode 100644 index 0000000..cfd5333 --- /dev/null +++ b/app/migrations/versions/bb6d2911e2c9_database.py @@ -0,0 +1,111 @@ +"""DataBase + +Revision ID: bb6d2911e2c9 +Revises: 9f86ca3b5c60 +Create Date: 2024-09-22 12:32:54.600508 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + + +# revision identifiers, used by Alembic. +revision = 'bb6d2911e2c9' +down_revision = None +branch_labels = None +depends_on = None + +metricTypes = sa.Enum('integer', 'boolean', 'string', 'float', name='type') +criteriaTypes = sa.Enum('greater', 'smaller', 'equal', 'greater or equal', 'smaller or equal', name='criteria') + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('metric', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('code', sa.String(), nullable=True), + sa.Column('area', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=False), + sa.Column('type', metricTypes, nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index(op.f('ix_metric_code'), 'metric', ['code'], unique=True) + op.create_index(op.f('ix_metric_id'), 'metric', ['id'], unique=False) + op.create_table('microservicescorecard', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('microserviceId', sa.Integer(), nullable=False), + sa.Column('scoreCardId', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_microservicescorecard_id'), 'microservicescorecard', ['id'], unique=False) + op.create_table('scorecard', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('code', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_scorecard_id'), 'scorecard', ['id'], unique=False) + op.create_table('scorecardmetrics', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('scoreCardId', sa.Integer(), nullable=False), + sa.Column('metricId', sa.Integer(), nullable=False), + sa.Column('criteria',criteriaTypes, nullable=False), + sa.Column('desiredValue', sa.String(), nullable=False), + sa.Column('weight', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_scorecardmetrics_id'), 'scorecardmetrics', ['id'], unique=False) + op.create_table('servicemetric', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('serviceId', sa.Integer(), nullable=False), + sa.Column('metricId', sa.Integer(), nullable=False), + sa.Column('value', sa.String(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_servicemetric_id'), 'servicemetric', ['id'], unique=False) + op.create_table('team', + sa.Column('id', UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('token', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_team_id'), 'team', ['id'], unique=False) + op.create_table('microservice', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('code', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('teamId', UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint(['teamId'], ['team.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_microservice_id'), 'microservice', ['id'], unique=False) + metricTypes.create(op.get_bind(), checkfirst=True) + criteriaTypes.create(op.get_bind(), checkfirst=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + metricTypes.drop(op.get_bind(), checkfirst=True) + criteriaTypes.drop(op.get_bind(), checkfirst=True) + op.drop_index(op.f('ix_microservice_id'), table_name='microservice') + op.drop_table('microservice') + op.drop_index(op.f('ix_team_id'), table_name='team') + op.drop_table('team') + op.drop_index(op.f('ix_servicemetric_id'), table_name='servicemetric') + op.drop_table('servicemetric') + op.drop_index(op.f('ix_scorecardmetrics_id'), table_name='scorecardmetrics') + op.drop_table('scorecardmetrics') + op.drop_index(op.f('ix_scorecard_id'), table_name='scorecard') + op.drop_table('scorecard') + op.drop_index(op.f('ix_microservicescorecard_id'), table_name='microservicescorecard') + op.drop_table('microservicescorecard') + op.drop_index(op.f('ix_metric_id'), table_name='metric') + op.drop_index(op.f('ix_metric_code'), table_name='metric') + op.drop_table('metric') + # ### end Alembic commands ### diff --git a/app/models/__init__.py b/app/models/__init__.py index f98ae1f..774d6f7 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -4,4 +4,4 @@ from .scoreCard import * from .scoreCardMetrics import * from .serviceMetric import * -from .team import * +from .team import * \ No newline at end of file diff --git a/app/models/metric.py b/app/models/metric.py index 4ae3ebe..1d71180 100644 --- a/app/models/metric.py +++ b/app/models/metric.py @@ -10,6 +10,7 @@ class Metric(Base): area = Column(String, nullable=False) description = Column(String, nullable=False) type = Column(Enum(*typeStates, name="type", create_type=False), nullable=False) + def __repr__(self): return f"{self.name} ({self.area})" diff --git a/app/models/scoreCard.py b/app/models/scoreCard.py index a37392a..90c362e 100644 --- a/app/models/scoreCard.py +++ b/app/models/scoreCard.py @@ -1,12 +1,16 @@ from app.database.base_class import Base from sqlalchemy import Column, Integer, String +from .microservice import Microservice +from .scoreCardMetrics import ScoreCardMetrics class Scorecard(Base): id = Column(Integer, primary_key=True, index=True) + code = Column(String, nullable=False) name = Column(String, nullable=False) code = Column(String, nullable=False) description = Column(String, nullable=False) + def __repr__(self): return f"" diff --git a/app/models/scorecard.py b/app/models/scorecard.py index a37392a..90c362e 100644 --- a/app/models/scorecard.py +++ b/app/models/scorecard.py @@ -1,12 +1,16 @@ from app.database.base_class import Base from sqlalchemy import Column, Integer, String +from .microservice import Microservice +from .scoreCardMetrics import ScoreCardMetrics class Scorecard(Base): id = Column(Integer, primary_key=True, index=True) + code = Column(String, nullable=False) name = Column(String, nullable=False) code = Column(String, nullable=False) description = Column(String, nullable=False) + def __repr__(self): return f"" diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index cd6438d..0ec411d 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -8,3 +8,4 @@ from .microserviceTeamScorecard import * from .microseviceInfo import * from .metric_info import * +from .scorecardServiceMetric import * \ No newline at end of file diff --git a/app/schemas/apiException.py b/app/schemas/apiException.py new file mode 100644 index 0000000..48131b8 --- /dev/null +++ b/app/schemas/apiException.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class CustomException(BaseModel): + message : str + details : list[object] \ No newline at end of file diff --git a/app/schemas/apiResponse.py b/app/schemas/apiResponse.py new file mode 100644 index 0000000..60afa61 --- /dev/null +++ b/app/schemas/apiResponse.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class CustomResponse(BaseModel): + message : str diff --git a/app/schemas/metric.py b/app/schemas/metric.py index 0a1a26c..130a0db 100644 --- a/app/schemas/metric.py +++ b/app/schemas/metric.py @@ -10,9 +10,7 @@ class MetricBase(BaseModel): code: Optional[str] = None area: Optional[List[str]] = None description: Optional[str] = None - type: Optional[str] = None - - + type: Optional[str] = None # Properties to receive on metric creation class MetricCreate(MetricBase): diff --git a/app/schemas/microservice.py b/app/schemas/microservice.py index ffa5192..858bb41 100644 --- a/app/schemas/microservice.py +++ b/app/schemas/microservice.py @@ -21,13 +21,22 @@ class MicroserviceCreate(MicroserviceBase): #new class MicroserviceCreateApi(MicroserviceBase): name: str - description: str + description: Optional[str] = None teamId:Optional[uuid.UUID] = None - scorecardids: Optional [list[int]] = None + scorecardids: Optional [list[int]] = [] + class Config: orm_mode = True +class Microserviceforscorecard(BaseModel): + name: str + description: str + teamId:uuid.UUID = None + id: int + + class Config: + orm_mode = True # Properties to receive on microservice update class MicroserviceUpdate(MicroserviceBase): name: str @@ -44,6 +53,7 @@ class MicroserviceInDBBase(MicroserviceBase): name: str code: str description: str + teamId:uuid.UUID team_name: Optional[str] =None scorecard_names: Optional[list] = None diff --git a/app/schemas/scoreCard.py b/app/schemas/scoreCard.py index 2e06e2a..daca932 100644 --- a/app/schemas/scoreCard.py +++ b/app/schemas/scoreCard.py @@ -1,4 +1,5 @@ from typing import Optional +from . import microservice, scoreCardMetrics, microservice from pydantic import BaseModel from datetime import datetime @@ -7,27 +8,60 @@ class ScoreCardBase(BaseModel): name: Optional[str] = None description: Optional[str] = None + code: Optional[str] = None + # Properties to receive on scorecard creation -class ScoreCardCreate(ScoreCardBase): +class ScoreCardCreate(BaseModel): + name: str + code : str + description: str + + +class ScoreCardinBase(ScoreCardBase): + id: int name: str + code: str description: str + services: list[microservice.MicroserviceCreate] + metrics: list[scoreCardMetrics.ScoreCardMetricsCreate] # Properties to receive on scorecard update -class ScoreCardUpdate(ScoreCardBase): - pass +class ScoreCardUpdate(BaseModel): + name: Optional[str] = None + code: Optional[str] = None + description: Optional[str] = None + services: Optional[list[microservice.MicroserviceCreate]] = [] + metrics: Optional[list[scoreCardMetrics.ScoreCardMetricsCreate]] = [] # Properties shared by models stored in DB class ScoreCardInDBBase(ScoreCardBase): id: int name: str code: str - description: Optional[str]= None + description: str class Config: orm_mode = True -# Properties shared serviceinfo + + +class GetScoreCard(BaseModel): + id: int + name: str + code: str + description: str + services: list[microservice.MicroserviceBase] + +class listScoreCard(BaseModel): + id: int + name: str + code: str + description: str + services: list[microservice.Microserviceforscorecard] + metrics: list[scoreCardMetrics.MetricListforScorecardGet] + +# Properties to return to client class ScoreCard(ScoreCardInDBBase): id: int name: str @@ -37,7 +71,7 @@ class ScoreCard(ScoreCardInDBBase): class Config: orm_mode = True - + # Properties properties stored in DB class ScoreCardInDB(ScoreCardInDBBase): diff --git a/app/schemas/scoreCardMetrics.py b/app/schemas/scoreCardMetrics.py index 1e8ff53..aa5e3bd 100644 --- a/app/schemas/scoreCardMetrics.py +++ b/app/schemas/scoreCardMetrics.py @@ -1,4 +1,5 @@ from typing import Optional, Union +from typing import Optional, Union from pydantic import BaseModel @@ -7,43 +8,62 @@ class ScoreCardMetricsBase(BaseModel): scoreCardId: Optional[int] = None metricId: Optional[int] = None - criteria: Optional[str] = None - desiredValue: Optional[Union[str,float,int,bool]] = None + desiredValue: Optional[Union[str, float, int, bool]] = None weight: Optional[int] = None - + # Properties to receive on ScoreCardMetrics creation class ScoreCardMetricsCreate(ScoreCardMetricsBase): scoreCardId: int - metricId: int criteria: str - desiredValue: Optional[Union[str,float,int,bool]] = None + desiredValue: Union[str, float, int, bool] weight: int - - + # Properties to receive on ScoreCardMetrics update -class ScoreCardMetricsUpdate(ScoreCardMetricsBase): - pass + # Properties shared by models stored in DB class ScoreCardMetricsInDBBase(ScoreCardMetricsBase): id: int scoreCardId: int metricId: int - + criteria: str - desiredValue: Optional[Union[str,float,int,bool]] = None + desiredValue: Optional[Union[str, float, int, bool]] = None weight: int - class Config: orm_mode = True +class ScoreCardMetricsUpdate(ScoreCardMetricsInDBBase): + pass + + +class MetricListforScorecardGet(BaseModel): + id: int # This one is metricID + criteria: str + desiredValue: Optional[Union[str, float, int, bool]] = None + weight: int + + # Properties to return to client class ScoreCardMetrics(ScoreCardMetricsInDBBase): pass # Properties properties stored in DB + + class ScoreCardMetricsInDB(ScoreCardMetricsInDBBase): pass + + +class MetricCreateScorecard(BaseModel): + criteria: str + desiredValue: Union[str, float, int, bool] + weight: int + id: int + + +class MetricTypeScorecard(MetricCreateScorecard): + type: str diff --git a/app/schemas/scorecardServiceMetric.py b/app/schemas/scorecardServiceMetric.py new file mode 100644 index 0000000..767f998 --- /dev/null +++ b/app/schemas/scorecardServiceMetric.py @@ -0,0 +1,34 @@ +from typing import Optional +from pydantic import BaseModel +from . import team, scoreCard, scoreCardMetrics, microservice + + +class ScorecardServiceMetric(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + code: Optional[str] = None + description: Optional[str] = None + metrics: Optional[list[scoreCardMetrics.MetricListforScorecardGet]] = [] + services: Optional[list[microservice.Microserviceforscorecard]] = [] + + class Config: + orm_mode = True + + +class ScorecardServiceMetricCreate(ScorecardServiceMetric): + name: str + description: str + # Here i will get the name of the services then search by code + # to get the id of the service and update the microservicescorecard table + services: Optional[list[int]] = [] + metrics: Optional[list[scoreCardMetrics.MetricCreateScorecard]] = [] + + +# Properties to receive on microserviceScoreCard update +class ScorecardServiceMetricUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + # Here i will get the name of the services then search by code + # to get the id of the service and update the microservicescorecard table + services: Optional[list[int]] = [] + metrics: Optional[list[scoreCardMetrics.MetricCreateScorecard]] = [] diff --git a/app/schemas/serviceMetric.py b/app/schemas/serviceMetric.py index 091c708..0adcdbf 100644 --- a/app/schemas/serviceMetric.py +++ b/app/schemas/serviceMetric.py @@ -9,16 +9,22 @@ class ServiceMetricBase(BaseModel): serviceId: Optional[int] = None metricId: Optional[int] = None value: Optional[Union[float, int, str, bool]] = None - date: Optional[datetime] = None + timestamp: Optional[datetime] = None # Properties to receive on microservice creation -class ServiceMetricCreate(ServiceMetricBase): +class ServiceMetricCreate(BaseModel): + serviceId: Optional[int] = None + metricId: Optional[int] = None + value: Union[float, int, str, bool] + timestamp: Optional[datetime] + + +class ServiceMetricReading(ServiceMetricBase): serviceId: int metricId: int value: Union[float, int, str, bool] - date: datetime - + timestamp: datetime # Properties to receive on microservice update class ServiceMetricUpdate(ServiceMetricBase): @@ -29,16 +35,6 @@ class ServiceMetricUpdate(ServiceMetricBase): class ServiceMetricInDBBase(ServiceMetricBase): id: int serviceId: int - metricId: int - value: Union[float, int, str, bool] - date: datetime - - class Config: - orm_mode = True - - -# Properties to return to client -class ServiceMetricReading(BaseModel): metricId: int value: Union[float, int, str, bool] timestamp: datetime diff --git a/app/schemas/team.py b/app/schemas/team.py index cc7b116..8ac0ff2 100644 --- a/app/schemas/team.py +++ b/app/schemas/team.py @@ -9,7 +9,7 @@ # Shared properties class TeamBase(BaseModel): name: Optional[str] = None - token: Optional[str] = "" + id: Optional[uuid.UUID] = None microservices: Optional[List[Microservice]] = [] diff --git a/app/utils/base.py b/app/utils/base.py index 5a3ccee..c429e66 100644 --- a/app/utils/base.py +++ b/app/utils/base.py @@ -1,5 +1,117 @@ import re +from app.api.exceptions import HTTPResponseCustomized +from app import crud, dependencies +from app import dependencies +from app.crud import CRUDMetric +from app import crud +from app.models.scoreCardMetrics import criteriaStates + +db_session = dependencies.get_db() +metricCrud: crud.CRUDMetric = CRUDMetric(db_session) + def format_code(name): - code = re.sub(r'\s+', '-', name.strip()) - return code \ No newline at end of file + code = re.sub(r'\s+', '-', name.strip()) + return code.lower() + + +def check_unique_ids(objects): + id_set = set() + for obj in objects: + if obj.id in id_set: + raise HTTPResponseCustomized( + status_code=400, detail="Metrics IDs are not unique, Please don't duplicate your metric") + id_set.add(obj.id) + return True + + +def check_metric_weight(objects): + sum = 0 + for obj in objects: + sum += obj.weight + if (sum == 100): + return True + raise HTTPResponseCustomized( + status_code=400, detail="Sum of metric weight is not 100") + + +def check_metric_type(objects): + for obj in objects: + print(obj.type) + print(obj.desiredValue) + if (obj.type == "integer"): + if (not isinstance(obj.desiredValue, int)): + raise HTTPResponseCustomized( + status_code=400, detail="desierdValue is not correct for integer metric") + elif (obj.type == "boolean"): + if (not isinstance(obj.desiredValue, bool)): + raise HTTPResponseCustomized( + status_code=400, detail="desierdValue is not correct for boolean metric") + elif (obj.type == "string"): + if (not isinstance(obj.desiredValue, str)): + raise HTTPResponseCustomized( + status_code=400, detail="desierdValue is not correct for string metric") + elif (obj.type == "float"): + if (not isinstance(obj.desiredValue, float)): + raise HTTPResponseCustomized( + status_code=400, detail="desierdValue is not correct for float metric") + else: + raise HTTPResponseCustomized(status_code=400, detail="desiredValue is not correct for anytype of metric") + + +#criteriaStates = ("greater", "smaller", "equal", "greater or equal", "smaller or equal") + + +def check_metric_criteria(objects): + for obj in objects: + if (obj.criteria not in criteriaStates): + raise HTTPResponseCustomized( + status_code=400, detail="Criteria is not correct") + + if (obj.type == "string" or obj.type == "boolean"): + if (obj.criteria != "equal"): + raise HTTPResponseCustomized( + status_code=400, detail="Criteria is not correct for this metric, you must make it equal") + + +def check_metric(objects): + check_unique_ids(objects) + check_metric_weight(objects) + check_metric_type(objects) + check_metric_criteria(objects) + + +def parse_stringified_value(value: str, target_type: str) -> int | float | bool | str: + try: + if target_type == 'boolean': + if value.lower() in ('true', '1'): + return True + elif value.lower() in ('false', '0'): + return False + else: + raise ValueError(f"Cannot convert {value} to boolean.") + elif target_type == 'integer': + try: + return int(value) + except ValueError: + raise ValueError(f"Cannot convert {value} to integer.") + elif target_type == 'float': + try: + return float(value) + except ValueError: + raise ValueError(f"Cannot convert {value} to float.") + elif target_type == 'string': + return value + except: + raise ValueError(f"Unsupported target type: {target_type}") + + +def stringify_value(value) -> str: + if isinstance(value, bool): + return 'True' if value else 'False' + elif isinstance(value, (int, float)): + return str(value) + elif isinstance(value, str): + return value + else: + raise ValueError(f"Unsupported value type: {type(value)}") diff --git a/app/utils/utity_datatype.py b/app/utils/utity_datatype.py index 83e37b9..fff0c4f 100644 --- a/app/utils/utity_datatype.py +++ b/app/utils/utity_datatype.py @@ -1,10 +1,12 @@ def parse_stringified_value(value: str, target_type: str) -> int | float | bool | str: + value_str = str(value).lower() + if target_type == 'boolean': - if value.lower() in ('true', '1'): + if value_str in ('true', '1'): return True - elif value.lower() in ('false', '0'): + elif value_str in ('false', '0'): return False else: raise ValueError(f"Cannot convert {value} to boolean.") diff --git a/docker-compose.yml b/docker-compose.yml index f921df5..a6e9741 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,9 @@ services: - OAUTH2_PROXY_COOKIE_SECRET=mY1hDibXMr0fvi9KdXFbKMAWd2ojgfLn - OAUTH2_PROXY_COOKIE_SECURE=false - OAUTH2_PROXY_EMAIL_DOMAINS=* - - OAUTH2_PROXY_REDIRECT_URL=http://localhost:4180/oauth2/callback + - OAUTH2_PROXY_REDIRECT_URL=http://localhost:3000/oauth2/callback - OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180 - - OAUTH2_PROXY_UPSTREAMS=http://localhost:3000 + - OAUTH2_PROXY_UPSTREAMS=http://localhost:8000 - OAUTH2_PROXY_PASS_ACCESS_TOKEN=true # Pass access token to upstream - OAUTH2_PROXY_COOKIE_EXPIRE=20s # Set to 0 to create a session cookie # - OAUTH2_PROXY_COOKIE_DOMAIN="http://localhost:4180" @@ -49,7 +49,7 @@ services: - backend depends_on: - database - # - oauth2-proxy + - oauth2-proxy env_file: .env ports: - "8000:8000" @@ -71,7 +71,7 @@ services: depends_on: - backend - database - # - oauth2-proxy + - oauth2-proxy env_file: .env ports: - "3000:3000" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 836a98c..b6c5e90 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,9 +17,14 @@ "axios": "^1.7.5", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "chart.js": "^4.4.4", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-datalabels": "^2.2.0", "fontawesome-free": "^1.0.4", "react": "^18.3.1", "react-bootstrap": "^2.10.4", + "react-chartjs-2": "^5.2.0", + "react-datepicker": "^7.3.0", "react-dom": "^18.3.1", "react-router-dom": "^6.26.1", "react-scripts": "5.0.1", @@ -2482,6 +2487,54 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", + "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.24", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.24.tgz", + "integrity": "sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, "node_modules/@fortawesome/fontawesome-free": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", @@ -3503,6 +3556,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -6044,6 +6102,34 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -6144,6 +6230,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6888,6 +6982,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -8860,19 +8964,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "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", @@ -15148,6 +15239,39 @@ } } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-datepicker": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.4.0.tgz", + "integrity": "sha512-vSSok4DTZ9/Os8O4HjZLxh4SZVFU6dQvoCX6mfbNdBqMsBBdzftrvMz0Nb4UUVVbgj9o8PfX84K3/31oPrTqmg==", + "dependencies": { + "@floating-ui/react": "^0.26.23", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, + "node_modules/react-datepicker/node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -16993,6 +17117,11 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwindcss": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", diff --git a/frontend/package.json b/frontend/package.json index 97d4520..2dabe2f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,9 +12,14 @@ "axios": "^1.7.5", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "chart.js": "^4.4.4", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-datalabels": "^2.2.0", "fontawesome-free": "^1.0.4", "react": "^18.3.1", "react-bootstrap": "^2.10.4", + "react-chartjs-2": "^5.2.0", + "react-datepicker": "^7.3.0", "react-dom": "^18.3.1", "react-router-dom": "^6.26.1", "react-scripts": "5.0.1", diff --git a/frontend/src/api/services/index.js b/frontend/src/api/services/index.js new file mode 100644 index 0000000..0d56beb --- /dev/null +++ b/frontend/src/api/services/index.js @@ -0,0 +1,197 @@ +const BASE_URL = 'http://127.0.0.1:8000' +const API_URL = 'api/v1' + +export const handleDelete = async (service_id) => { + try { + const response = await fetch(`${BASE_URL}/${API_URL}/services/${service_id}`, { + method: 'DELETE', + }); + + console.log(`Service with id ${service_id} deleted successfully`); + return response; + } catch (error) { + console.error('Error deleting service:', error); + } +}; + +export const postService = async (service) => { + const request = { + name: service.name, + description: service.description, + teamId: service.teamId, + scorecardids: service.scorecardids + } + + var data = await fetch(`${BASE_URL}/${API_URL}/services/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }) + .then((response) => response.json()) + var responseData = { + message: data.message, + object: data + } + console.log(responseData) +} + +export const editService = async (service_id, service) => { + const request = { + name: service.name, + description: service.description, + teamId: service.teamId, + scorecardids: service.scorecardids + } + + var data = await fetch(`${BASE_URL}/${API_URL}/services/${service_id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }) + .then((response) => response.json()) + var responseData = { + message: data.message, + object: data + } + console.log(responseData) +} + +export const getAllServices = async () => { + try { + const response = await fetch(`${BASE_URL}/${API_URL}/services`); + const data = await response.json(); + return data; // Return the data array + } catch (error) { + console.error('Error fetching services:', error); + } +}; + +export const getAllTeams = async() => { + try { + const response = await fetch(`${BASE_URL}/${API_URL}/teams`); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching teams:', error); + } +} + +export const getAllScorecrds = async() => { + try { + const response = await fetch(`${BASE_URL}/${API_URL}/scorecard/`); + const data = await response.json(); + console.log(data) + return data; + } catch (error) { + console.error('Error fetching scorecards:', error); + } +} + +export const getMetricReadings = async (service_id, from_date, to_date) => { + let request = { + from_date: from_date, + to_date: to_date + }; + + const response = await fetch(`${BASE_URL}/${API_URL}/services/${service_id}/metric_reading`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }) + const data = await response.json(); + + return data; +}; + + +export const getServiceById = async (service_id, navigate) => { + try{ + + const response = await fetch(`${BASE_URL}/${API_URL}/services/${service_id}`); + const data = await response.json(); + + if( data.message == "Not Found") { + // replace path history to a 404 not found page + navigate('/404'); + } + + console.log(data); + return data; // Return the data array + + } catch (error) { + console.log(error); + } +}; + +export const getServiceDetailsById = async (service_id, navigate) => { + try{ + const response = await fetch(`${BASE_URL}/${API_URL}/services/${service_id}/details`); + const data = await response.json(); + + if( data.message == "Not Found") { + // replace path history to a 404 not found page + navigate('/404'); + } + + return data; // Return the data array + + } catch (error) { + console.log(error); + } +}; + +export const getServiceInfoById = async (service_id, navigate) => { + try{ + const response = await fetch(`${BASE_URL}/${API_URL}/serviceinfo/${service_id}`); + const data = await response.json(); + + if( data.message == "Not Found") { + // replace path history to a 404 not found page + navigate('/404'); + } + + console.log(data); + return data; // Return the data array + + } catch (error) { + console.log(error); + } +}; + +export const getScorecardById = async(scorecard_id) => { + try { + const response = await fetch(`${BASE_URL}/${API_URL}/scorecard/${scorecard_id}`); + const data = await response.json(); + return data; + } catch (error) { + console.log(`error while fetching service scorecard ${scorecard_id}: ${error}`) + } +} + +export const getServiceMetricInfo = async(service_id, scorecard_id) => { + try { + const response = await fetch( + `${BASE_URL}/${API_URL}/metricinfo/${service_id}/${scorecard_id}` + ); + const data = await response.json(); + return data; + } catch (error) { + console.log(`error while fetching service ${service_id} metric : ${error}`) + } +} + +export const getMetricById = async(metric_id) => { + try { + const response = await fetch(`${BASE_URL}/${API_URL}/metrics/${metric_id}`); + const data = await response.json(); + return data; + } catch (error) { + console.log(`error while fetching scorecard metric ${metric_id}: ${error}`) + } +} diff --git a/frontend/src/assets/member1.jpg b/frontend/src/assets/member1.jpg new file mode 100644 index 0000000..fcf3c45 Binary files /dev/null and b/frontend/src/assets/member1.jpg differ diff --git a/frontend/src/assets/member2.jpg b/frontend/src/assets/member2.jpg new file mode 100644 index 0000000..da7990b Binary files /dev/null and b/frontend/src/assets/member2.jpg differ diff --git a/frontend/src/assets/member3.jpg b/frontend/src/assets/member3.jpg new file mode 100644 index 0000000..ee8243a Binary files /dev/null and b/frontend/src/assets/member3.jpg differ diff --git a/frontend/src/components/DateTimePicker.jsx b/frontend/src/components/DateTimePicker.jsx new file mode 100644 index 0000000..cf9cf58 --- /dev/null +++ b/frontend/src/components/DateTimePicker.jsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { Form, Button, Col, Row } from 'react-bootstrap'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import { format } from 'date-fns'; + +function DateTimeRangePicker({ onStartDateChange, onEndDateChange }) { + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const [formattedStrDate, setFormattedStrDate] = useState(''); + const [formattedEndDate, setFormattedEndDate] = useState(''); + + const isoFormat = date => { + const inputDate = new Date(date); + + inputDate.setUTCDate(inputDate.getUTCDate()); + console.log(inputDate.toISOString()); + return inputDate.toISOString(); +}; + + const handleStartDate = (date) => { + setStartDate(date); + onStartDateChange(isoFormat(date)); + console.log(startDate) + }; + const handleEndDate = (date) => { + setEndDate(date); + onEndDateChange(isoFormat(date)); + console.log(endDate) + }; + + return ( +
+ + + + From Date + + + + + + To Date + + + + +
+ ); +} + +export default DateTimeRangePicker; diff --git a/frontend/src/components/TagsBox.jsx b/frontend/src/components/TagsBox.jsx index cd1d787..99c13d9 100644 --- a/frontend/src/components/TagsBox.jsx +++ b/frontend/src/components/TagsBox.jsx @@ -1,9 +1,8 @@ import React, { useRef, useState } from 'react' import TagsList from '../components/common/TagsList.jsx' -import '../styles/components/TagsBox.css' +import '../styles/components/TagsList.css' const TagsBox = (props) => { - // const inputRef = useRef(null); const [inputValue, setInputValue] = useState(''); const addTag = (e) => { @@ -14,16 +13,10 @@ const TagsBox = (props) => { }; const createTags = () => { - // const inputElem = inputRef.current; - // if (inputElem == null) { console.log('tagsBox not found'); return; } - // const tagsString = inputElem.value?.replace(/\s+/g, ' ') ?? ""; - // const tagsList = tagsString.split(','); - const tagsString = inputValue?.replace(/\s+/g, ' ') ?? ""; const tagsList = tagsString.split(','); props.setTags((prevTags) => Array.from(new Set([...prevTags, ...tagsList]))) - // inputElem.value = ''; } const handleInputChange = (e) => { setInputValue(e.target.value) diff --git a/frontend/src/components/common/TagsList.jsx b/frontend/src/components/common/TagsList.jsx index c656641..93ee665 100644 --- a/frontend/src/components/common/TagsList.jsx +++ b/frontend/src/components/common/TagsList.jsx @@ -2,8 +2,9 @@ import React from 'react' const TagsList = ({ tags, setTags }) => { - const removeTag = (tagName) => { - setTags((tags) => tags.filter(tag => tag != tagName)); + const removeTag = (target) => { + setTags((tags) => tags.filter(tag => tag != target)); + console.log('tags: ', tags) }; return ( diff --git a/frontend/src/components/common/TagsObj.jsx b/frontend/src/components/common/TagsObj.jsx new file mode 100644 index 0000000..971c8d4 --- /dev/null +++ b/frontend/src/components/common/TagsObj.jsx @@ -0,0 +1,29 @@ +import React from 'react' + +const TagsList = ({ tags, setTags, serviceIds }) => { + + const removeTag = (tagId) => { + setTags((prevTags) => { + const updatedTags = tags.filter(tag => tag.id != tagId); + serviceIds(updatedTags); + console.log('Updated service tags: ', updatedTags); + return updatedTags; + }); + console.log('tag removed: id: ', tagId) + console.log('tags: ', tags) + }; + + return ( + + ); + +} +export default TagsList \ No newline at end of file diff --git a/frontend/src/components/common/charts/BarChart.jsx b/frontend/src/components/common/charts/BarChart.jsx new file mode 100644 index 0000000..928ee5a --- /dev/null +++ b/frontend/src/components/common/charts/BarChart.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import 'chartjs-adapter-date-fns'; +import { Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, TimeScale, CategoryScale, LinearScale, PointElement, Title, Tooltip, Legend, BarElement +} from 'chart.js'; + +ChartJS.register( + TimeScale, + CategoryScale, + LinearScale, + PointElement, + Title, + Tooltip, + Legend, + BarElement, +); + +const BarChart = ({ title, points }) => { + // Prepare labels and data from points + const labels = points.map(point => point.time); // Extract time for labels + const dataValues = points.map(point => (point.value === 'true' ? 1 : 0)); // Convert value to binary + + const booleanOptions = { + scales: { + x: { + type: 'time', + time: { + minUnit: 'minute', + tooltipFormat: 'yyyy MMM dd hh:mm a', + displayFormats: { + month: 'yyyy MMM', + day: 'MMM dd', + hour: 'hh a', + minute: 'hh:mm a', + second: 'mm:ss' + } + }, + ticks: { + autoSkip: true, + maxTicksLimit: 10, + }, + grid: { + display: true, + }, + title: { + display: true, + text: "Time" + } + }, + y: { + min: 0, + max: 1, + ticks: { + callback: function(value) { + // Show only 0 and 1 on the y-axis + return (value === 0 || value === 1) ? value : ''; + } + }, + title: { + display: true, + text: 'Value', + }, + }, + }, + plugins: { + legend: { + position: 'top', + }, + }, + }; + + const booleanData = { + labels: labels, // Use extracted labels + datasets: [ + { + label: title, + data: dataValues, // Use extracted data values + backgroundColor: 'rgba(54, 162, 235, 0.6)', + barThickness: 1, + }, + ], + }; + + return ( + + ); +} + +export default BarChart; diff --git a/frontend/src/components/common/charts/LineChart.jsx b/frontend/src/components/common/charts/LineChart.jsx new file mode 100644 index 0000000..b69d153 --- /dev/null +++ b/frontend/src/components/common/charts/LineChart.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import 'chartjs-adapter-date-fns'; + +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, TimeScale, CategoryScale, LinearScale, PointElement, Title, Tooltip, Legend, LineElement +} from 'chart.js'; + +ChartJS.register( + TimeScale, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +); + +const LineChart = ({ title, points }) => { + // Prepare labels and data from points + const labels = points.map(point => point.time); // Extract time for labels + const dataValues = points.map(point => point.value); // Extract values for the y-axis + + const lineOptions = { + scales: { + x: { + type: 'time', + time: { + minUnit: 'minute', + tooltipFormat: 'yyyy MMM dd hh:mm a', + displayFormats: { + month: 'yyyy MMM', + day: 'MMM dd', + hour: 'hh a', + minute: 'hh:mm a', + second: 'mm:ss' + } + }, + ticks: { + autoSkip: true, + maxTicksLimit: 10, + }, + grid: { + display: true, + }, + title: { + display: true, + text: "Time" + } + }, + y: { + min: 0, + title: { + display: true, + text: "Value" + } + }, + }, + plugins: { + legend: { + display: true, + labels: { + text: "metric", + padding: 20, + }, + }, + }, + }; + + const lineData = { + labels: labels, + datasets: [ + { + label: title, + data: dataValues, // Use extracted data values + fill: true, + backgroundColor: "rgba(75,192,192,0.2)", + borderColor: "rgba(75,192,192,1)" + } + ] + }; + + return ( + + ); +} + +export default LineChart; diff --git a/frontend/src/components/common/charts/ScatterChart.jsx b/frontend/src/components/common/charts/ScatterChart.jsx new file mode 100644 index 0000000..3f9206f --- /dev/null +++ b/frontend/src/components/common/charts/ScatterChart.jsx @@ -0,0 +1,102 @@ +import React from 'react' +import { Scatter } from 'react-chartjs-2'; +import { + Chart as ChartJS, CategoryScale, LinearScale, PointElement, Title, Tooltip, Legend +} from 'chart.js'; +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + Title, + Tooltip, + Legend, +); + +const ScatterChart = ({ title, points }) => { + // Extract unique categories from the points + const categories = [...new Set(points.map(point => point.value))].sort(); + + // Chart.js options + const scatterOptions = { + responsive: true, + scales: { + x: { + type: 'time', + time: { + minUnit: 'minute', + tooltipFormat: 'yyyy MMM dd hh:mm a', + displayFormats: { + month: 'yyyy MMM', + day: 'MMM dd', + hour: 'hh a', + minute: 'hh:mm a', + second: 'mm:ss' + } + }, + ticks: { + maxTicksLimit: 10, // Limit the number of ticks on the x-axis + }, + title: { + display: true, + text: 'Time', + }, + }, + y: { + type: 'category', + title: { + display: true, + text: 'Categories', + }, + labels: categories, // Display the unique categories on the y-axis + }, + }, + plugins: { + legend: { + position: 'top', + }, + }, +}; + + // Helper function to get mapped points + const mapPoints = () => { + return points.map(point => { + const categoryIndex = categories.indexOf(point.value); + return { + x: new Date(point.time), // Use ISO time for x value + y: categoryIndex !== -1 ? categories[categoryIndex] : 'Unknown', // Map to category + }; + }); + }; + + // Dataset for the scatter chart + const scatterData = { + datasets: [ + { + label: title, + data: mapPoints(), // Map points to x,y format + backgroundColor: points.map(point => { + const categoryIndex = categories.indexOf(point.value); + const colors = [ + 'rgba(255, 99, 132, 0.6)', + 'rgba(54, 162, 235, 0.6)', + 'rgba(255, 206, 86, 0.6)', + 'rgba(75, 192, 192, 0.6)', + 'rgba(153, 102, 255, 0.6)', + 'rgba(255, 159, 64, 0.6)', + 'rgba(201, 203, 207, 0.6)', + ]; + return categoryIndex === -1 ? 'rgba(0, 0, 0, 0.1)' : + colors[categoryIndex % colors.length]; + }), + pointStyle: 'rect', + pointRadius: 10, + }, + ], + }; + + return ; +}; + +export default ScatterChart; + + diff --git a/frontend/src/components/layout/NavBar.jsx b/frontend/src/components/layout/NavBar.jsx index ea19d33..406a55c 100644 --- a/frontend/src/components/layout/NavBar.jsx +++ b/frontend/src/components/layout/NavBar.jsx @@ -1,11 +1,14 @@ -import React from 'react' +import React from 'react'; +import '../../styles/layouts/Layout.css'; const NavBar = () => { return (