diff --git a/forms-flow-data-layer/src/db/formio_db.py b/forms-flow-data-layer/src/db/formio_db.py index a12d4ccc26..2cc99ad3e0 100644 --- a/forms-flow-data-layer/src/db/formio_db.py +++ b/forms-flow-data-layer/src/db/formio_db.py @@ -4,7 +4,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from src.config.envs import ENVS -from src.models.formio import FormModel, SubmissionsModel # Import your MongoDB models +from src.models.formio import FormModel, SubmissionModel # Import your MongoDB models from src.utils import get_logger logger = get_logger(__name__) @@ -23,7 +23,7 @@ async def init_formio_db(self): self.__client = AsyncIOMotorClient(ENVS.FORMIO_MONGO_DB_URI) self.formio_db = self.__client[ENVS.FORMIO_DB_NAME] await init_beanie( - database=self.formio_db, document_models=[FormModel, SubmissionsModel] + database=self.formio_db, document_models=[FormModel, SubmissionModel] ) def get_db(self): diff --git a/forms-flow-data-layer/src/graphql/resolvers/__init__.py b/forms-flow-data-layer/src/graphql/resolvers/__init__.py index 348fb85eaf..f039ef5c40 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/__init__.py +++ b/forms-flow-data-layer/src/graphql/resolvers/__init__.py @@ -1,10 +1,10 @@ import strawberry -from src.graphql.resolvers.submission_resolvers import ( - QuerySubmissionsResolver, -) +from src.graphql.resolvers.form_resolvers import QueryFormsResolver +from src.graphql.resolvers.metric_resolvers import QueryMetricsResolver +from src.graphql.resolvers.submission_resolvers import QuerySubmissionsResolver @strawberry.type -class Query(QuerySubmissionsResolver): # Inherit from query classes +class Query(QuerySubmissionsResolver, QueryMetricsResolver, QueryFormsResolver): # Inherit from query classes pass diff --git a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py new file mode 100644 index 0000000000..9dd10206e8 --- /dev/null +++ b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py @@ -0,0 +1,95 @@ +from typing import Optional + +import strawberry + +from src.graphql.schema import FormSchema, PaginationWindow +from src.graphql.service import FormService +from src.middlewares.auth import auth + + +@strawberry.type +class QueryFormsResolver: + @strawberry.field(extensions=[auth.auth_required()]) + async def get_forms( + self, + info: strawberry.Info, + limit: int = 100, + page_no: int = 1, + order_by: str = 'created', + type: Optional[str] = None, + created_by: Optional[str] = None, + form_name: Optional[str] = None, + status: Optional[str] = None, + parent_form_id: Optional[str] = None, + from_date: Optional[str] = None, + to_date: Optional[str] = None + ) -> PaginationWindow[FormSchema]: + """ + GraphQL resolver for querying forms. + + Args: + info (strawberry.Info): GraphQL context information + limit (int): Number of items to return (default: 100) + page_no (int): Pagination number (default: 1) + order_by (str): Filter to sort forms by (default: 'created') + type (Optional[str]): Filter on form type + created_by (Optional[str]): Filter on user who created the form + form_name (Optional[str]): Filter on form name + status (Optional[str]): Filter on form status + parent_form_id (Optional[str]): Filter on form parent id + from_date (Optional[str]): Filter from form date + to_date (Optional[str]): Filter to form date + Returns: + Paginated list of Form objects containing combined PostgreSQL and MongoDB data + """ + # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names + # will be applied automatically. Other filters will require additional handling. + filters = {} + filters["order_by"] = order_by + if type: + filters["type"] = type + if created_by: + filters["created_by"] = created_by + if form_name: + filters["form_name"] = form_name + if status: + filters["status"] = status + if parent_form_id: + filters["parent_form_id"] = parent_form_id + if from_date: + filters["from_date"] = from_date + if to_date: + filters["to_date"] = to_date + + # Convert page_no to offset + offset = (page_no - 1) * limit + + forms = await FormService.get_forms( + user_context=info.context.get("user"), + limit=limit, + offset=offset, + filters=filters + ) + return forms + + + @strawberry.field(extensions=[auth.auth_required()]) + async def get_form( + self, + info: strawberry.Info, + form_id: str, + ) -> Optional[FormSchema]: + """ + GraphQL resolver for querying form. + + Args: + info (strawberry.Info): GraphQL context information + form_id (str): ID of the form + Returns: + Form object containing combined PostgreSQL and MongoDB data + """ + form = await FormService.get_form( + user_context=info.context.get("user"), + form_id=form_id, + ) + return form diff --git a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py new file mode 100644 index 0000000000..67b1753fd6 --- /dev/null +++ b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py @@ -0,0 +1,89 @@ +from typing import List, Optional + +import strawberry + +from src.graphql.schema import MetricSchema +from src.graphql.service import MetricService +from src.middlewares.auth import auth + + +@strawberry.type +class QueryMetricsResolver: + @strawberry.field(extensions=[auth.auth_required()]) + async def get_metrics_submission_status( + self, + info: strawberry.Info, + form_id: str, + order_by: str = 'created', + from_date: Optional[str] = None, + to_date: Optional[str] = None, + ) -> List[MetricSchema]: + """ + GraphQL resolver for querying submission status metrics. + + Args: + info (strawberry.Info): GraphQL context information + form_id (str): ID of the form + order_by (str): Filter to sort submissions by (default: 'created') + from_date (Optional[str]): Filter from submission date + to_date (Optional[str]): Filter to submission date + Returns: + List of Metric objects + """ + # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names + # will be applied automatically. Other filters will require additional handling. + filters = {} + filters["order_by"] = order_by + if form_id: + filters["latest_form_id"] = form_id + if from_date: + filters["from_date"] = from_date + if to_date: + filters["to_date"] = to_date + + metrics = await MetricService.get_submission_metrics( + user_context=info.context.get("user"), + metric='application_status', + filters=filters + ) + return metrics + + + @strawberry.field(extensions=[auth.auth_required()]) + async def get_metrics_submission_created_by( + self, + info: strawberry.Info, + form_id: str, + order_by: str = 'created', + from_date: Optional[str] = None, + to_date: Optional[str] = None, + ) -> List[MetricSchema]: + """ + GraphQL resolver for querying submission created by metrics. + + Args: + info (strawberry.Info): GraphQL context information + form_id (str): ID of the form + order_by (str): Filter to sort submissions by (default: 'created') + from_date (Optional[str]): Filter from submission date + to_date (Optional[str]): Filter to submission date + Returns: + List of Metric objects + """ + # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names + # will be applied automatically. Other filters will require additional handling. + filters = {} + filters["order_by"] = order_by + if form_id: + filters["latest_form_id"] = form_id + if from_date: + filters["from_date"] = from_date + if to_date: + filters["to_date"] = to_date + + metrics = await MetricService.get_submission_metrics( + user_context=info.context.get("user"), + metric='created_by', + filters=filters + ) + return metrics diff --git a/forms-flow-data-layer/src/graphql/schema/__init__.py b/forms-flow-data-layer/src/graphql/schema/__init__.py index 5ed7b5b891..fd1b5ea82d 100644 --- a/forms-flow-data-layer/src/graphql/schema/__init__.py +++ b/forms-flow-data-layer/src/graphql/schema/__init__.py @@ -1,13 +1,17 @@ from src.graphql.schema.form_schema import FormSchema +from src.graphql.schema.metric_schema import MetricSchema from src.graphql.schema.submission_schema import ( PaginatedSubmissionResponse, SubmissionDetailsWithSubmissionData, SubmissionSchema, ) +from src.middlewares.pagination import PaginationWindow __all__ = [ "FormSchema", + "MetricSchema", "SubmissionSchema", "SubmissionDetailsWithSubmissionData", "PaginatedSubmissionResponse", + "PaginationWindow", ] diff --git a/forms-flow-data-layer/src/graphql/schema/form_schema.py b/forms-flow-data-layer/src/graphql/schema/form_schema.py index d1622499a5..982ae1b267 100644 --- a/forms-flow-data-layer/src/graphql/schema/form_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/form_schema.py @@ -4,8 +4,6 @@ from src.middlewares.role_check import RoleCheck -# currently this file is not used in the codebase, but it is kept for future use - @strawberry.type class FormSchema: @@ -14,13 +12,62 @@ class FormSchema: This is the external representation of your database model """ + # FormIO populated fields id: str name: Optional[str] = strawberry.field( extensions=[RoleCheck(["admin"])] - ) # Add this line + ) title: str path: str type: str display: Optional[str] = None - created_at: Optional[str] = None - updated_at: Optional[str] = None + parent_form_id: Optional[str] = None + created: Optional[str] = None + modified: Optional[str] = None + + # WebAPI populated fields + created_by: str + modified_by: str + status: str + version: int + + # BPM populated fields + # None + + # Calculated fields + total_submissions: int + + @staticmethod + def from_result(result: dict): + data = {} + + # Map FormIO Data + if formio:= result.get("formio"): + data.update({ + "id": formio.id, + "title": formio.title, + "name": formio.name, + "path": formio.path, + "type": formio.type, + "display": formio.display, + "parent_form_id": formio.parentFormId, + "created": (formio.created.isoformat() if formio.created else None), + "modified": (formio.modified.isoformat() if formio.modified else None), + }) + + # Map WebAPI data + if webapi := result.get("webapi"): + data.update({ + "created_by": webapi.created_by, + "modified_by": webapi.modified_by, + "status": webapi.status, + "version": webapi.version, + }) + + # Map Calculated data + if calculated := result.get("calculated"): + data.update({ + "total_submissions": calculated["total_submissions"] + }) + + return FormSchema(**data) diff --git a/forms-flow-data-layer/src/graphql/schema/metric_schema.py b/forms-flow-data-layer/src/graphql/schema/metric_schema.py new file mode 100644 index 0000000000..8477df4a9c --- /dev/null +++ b/forms-flow-data-layer/src/graphql/schema/metric_schema.py @@ -0,0 +1,12 @@ +import strawberry + + +@strawberry.type +class MetricSchema: + """ + GraphQL type representing a Metric + This is the external representation of your database model + """ + + metric: str + count: int diff --git a/forms-flow-data-layer/src/graphql/service/__init__.py b/forms-flow-data-layer/src/graphql/service/__init__.py index 39470e71f4..59f150bad5 100644 --- a/forms-flow-data-layer/src/graphql/service/__init__.py +++ b/forms-flow-data-layer/src/graphql/service/__init__.py @@ -1,4 +1,5 @@ from src.graphql.service.form_service import FormService +from src.graphql.service.metric_service import MetricService from src.graphql.service.submission_service import SubmissionService -__all__ = ["FormService", "SubmissionService"] +__all__ = ["FormService", "MetricService", "SubmissionService"] diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index 146ba09ea9..875646465d 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -1,95 +1,89 @@ -from typing import List, Optional +from typing import Optional from beanie import PydanticObjectId -from src.graphql.schema import FormSchema -from src.models.formio import FormModel -from src.utils import get_logger +from src.graphql.schema import FormSchema, PaginationWindow +from src.middlewares.pagination import verify_pagination_params +from src.models.formio import FormModel, SubmissionModel +from src.models.webapi import FormProcessMapper +from src.utils import UserContext, get_logger logger = get_logger(__name__) -# this is not used for now, but we can use it in the future if needed +class FormService(): + """Service class for handling form related operations.""" -# Service Layer for Form-related Operations -class FormService: - @staticmethod - def convert_to_graphql_type(form_model: FormModel) -> FormSchema: - """ - Convert a Beanie FormModel to a GraphQL FormSchema - - Args: - form_model (FormModel): Database model to convert - - Returns: - FormSchema: GraphQL type representation - """ - return FormSchema( - id=str(form_model.id), # Convert ObjectId to string - name=form_model.name, - path=form_model.path, - type=form_model.type, - title=form_model.title, - display=str(form_model.display) if form_model.display else None, - created_at=( - form_model.created_at.isoformat() if form_model.created_at else None - ), - updated_at=( - form_model.updated_at.isoformat() if form_model.updated_at else None - ), - ) - - @staticmethod + @classmethod + @verify_pagination_params async def get_forms( - skip: int = 0, limit: int = 100, type_filter: Optional[str] = None - ) -> List[FormSchema]: + cls, + user_context: UserContext, + limit: int = 100, + offset: int = 0, + filters: dict[str, str] = {}, + ) -> PaginationWindow[FormSchema]: """ - Fetch and convert forms to GraphQL types + Fetches forms from the WebAPI and adds additional details from FormIO. Args: - skip (int): Pagination - number of items to skip - limit (int): Maximum number of items to return - type_filter (Optional[str]): Optional filter by form type - + user_context (UserContext): User context information + limit (int): Number of items to return (default: 100) + offset (int): Pagination offset (default: 0) + filters (dict): Search filters to apply to the query Returns: - List[FormSchema]: List of converted GraphQL form types + Paginated list of Form objects containing combined PostgreSQL and MongoDB data """ - query = FormModel.find_all() - - if type_filter: - query = query.find(FormModel.type == type_filter) - - # Execute query and convert results - forms = await query.skip(skip).limit(limit).to_list() - - # Convert each form to GraphQL type - return [FormService.convert_to_graphql_type(form) for form in forms] - - @staticmethod - async def get_form(form_id: str) -> Optional[FormSchema]: + # Query webapi database + webapi_query, webapi_total_count = await FormProcessMapper.find_all(user_context=user_context, **filters) + + # Apply pagination filters + webapi_query = webapi_query.offset(offset).limit(limit) + + # Combine results with data from formio + results = [] + webapi_results = (await FormProcessMapper.execute(webapi_query)).all() + for wr in webapi_results: + submissions_count = await SubmissionModel.count(filters={"form": PydanticObjectId(wr.form_id)}) + results.append({ + "webapi": wr, + "formio": await FormModel.get(PydanticObjectId(wr.form_id)), + "calculated": {"total_submissions": submissions_count} + }) + + # Convert to GraphQL Schema + forms = [FormSchema.from_result(result=r) for r in results] + return PaginationWindow(items=forms, total_count=webapi_total_count) + + + @classmethod + async def get_form( + cls, + user_context: UserContext, + form_id: str + ) -> Optional[FormSchema]: """ - service to fetch a single form by ID + Fetches a form based on it's form_id from the WebAPI and adds additional details from FormIO. Args: - form_id (str): ID of the form to fetch - + user_context (UserContext): User context information + form_id (str): ID of the form Returns: - Optional[FormSchema]: Matching form or None + Form object containing combined PostgreSQL and MongoDB data """ - try: - # Convert string ID back to PydanticObjectId - object_id = PydanticObjectId(form_id) - - # Fetch the form - form_model = await FormModel.find_one(id=object_id) - - # If found, convert to GraphQL type - return ( - await FormService.convert_to_graphql_type(form_model) - if form_model - else None - ) - - except Exception: - # Handle invalid ID format or not found scenarios - return None + # Query the databases + webapi_result = await FormProcessMapper.first(form_id=form_id) + formio_result = await FormModel.get(PydanticObjectId(form_id)) + submissions_count = await SubmissionModel.count(filters={"form": PydanticObjectId(webapi_result.form_id)}) + + # Combine results + result = { + "webapi": webapi_result, + "formio": formio_result, + "calculated": {"total_submissions": submissions_count} + } + + # Convert to GraphQL Schema + form = FormSchema.from_result(result=result) + return form + diff --git a/forms-flow-data-layer/src/graphql/service/metric_service.py b/forms-flow-data-layer/src/graphql/service/metric_service.py new file mode 100644 index 0000000000..f08129381b --- /dev/null +++ b/forms-flow-data-layer/src/graphql/service/metric_service.py @@ -0,0 +1,38 @@ +from typing import List + +from src.graphql.schema import MetricSchema +from src.models.webapi import Application +from src.utils import UserContext, get_logger + +logger = get_logger(__name__) + + +class MetricService(): + """Service class for handling metric related operations.""" + + @classmethod + async def get_submission_metrics( + cls, + user_context: UserContext, + metric: str, + filters: dict[str, str] = {}, + ) -> List[MetricSchema]: + """ + Fetches aggregated submission metrics. + + Args: + user_context (UserContext): User context information + metric (str): The metric to search on. This should be an existing db column. + filters (dict): Search filters to apply to the query + Returns: + List of Metric objects + """ + # Query webapi database + webapi_query = await Application.find_aggregated_application_metrics(metric, **filters) + webapi_result = (await Application.execute(webapi_query)).all() + + # Convert to GraphQL Schema + metrics = [] + for wr in webapi_result: + metrics.append(MetricSchema(metric=wr.metric, count=wr.count)) + return metrics diff --git a/forms-flow-data-layer/src/graphql/service/submission_service.py b/forms-flow-data-layer/src/graphql/service/submission_service.py index 780e8a0e08..16a0ff56b7 100644 --- a/forms-flow-data-layer/src/graphql/service/submission_service.py +++ b/forms-flow-data-layer/src/graphql/service/submission_service.py @@ -6,7 +6,7 @@ PaginatedSubmissionResponse, SubmissionDetailsWithSubmissionData, ) -from src.models.formio.submission import SubmissionsModel +from src.models.formio.submission import SubmissionModel from src.models.webapi.application import Application from src.utils import get_logger @@ -157,7 +157,7 @@ async def get_submission( if app["submission_id"] ] # Get filtered submissions from MongoDB - mongo_side_submissions = await SubmissionsModel.query_submission( + mongo_side_submissions = await SubmissionModel.query_submission( submission_ids=submission_ids, filter=mongo_search, selected_form_fields=selected_form_fields, diff --git a/forms-flow-data-layer/src/middlewares/auth.py b/forms-flow-data-layer/src/middlewares/auth.py index 2c36a1b267..f14331e03b 100644 --- a/forms-flow-data-layer/src/middlewares/auth.py +++ b/forms-flow-data-layer/src/middlewares/auth.py @@ -63,7 +63,6 @@ async def has_permission( class Auth: - @staticmethod def auth_required(roles: List[str] = None): """ diff --git a/forms-flow-data-layer/src/middlewares/pagination.py b/forms-flow-data-layer/src/middlewares/pagination.py new file mode 100644 index 0000000000..60eb780a76 --- /dev/null +++ b/forms-flow-data-layer/src/middlewares/pagination.py @@ -0,0 +1,24 @@ +import functools +from typing import Generic, List, TypeVar + +import strawberry + +Item = TypeVar("Item") +@strawberry.type +class PaginationWindow(Generic[Item]): + """GraphQL type representing a generic set of paginated items.""" + items: List[Item] + total_count: int + + +def verify_pagination_params(function): + """Verifies pagination parameters are valid.""" + + @functools.wraps(function) + def wrapper(*args, **kwargs): + if (limit := kwargs.get('limit')) and limit < 0: + raise Exception(f"limit ({limit}) must be greater than 0") + if (offset := kwargs.get('offset')) and offset < 0: + raise Exception(f"offset ({offset}) must be greater than 0") + return function(*args, **kwargs) + return wrapper diff --git a/forms-flow-data-layer/src/models/formio/__init__.py b/forms-flow-data-layer/src/models/formio/__init__.py index f4e09d591b..61945509d8 100644 --- a/forms-flow-data-layer/src/models/formio/__init__.py +++ b/forms-flow-data-layer/src/models/formio/__init__.py @@ -1,5 +1,5 @@ from src.models.formio.constants import FormioTables from src.models.formio.form import FormModel -from src.models.formio.submission import SubmissionsModel +from src.models.formio.submission import SubmissionModel -__all__ = ["FormModel", "SubmissionsModel", "FormioTables"] +__all__ = ["FormModel", "SubmissionModel", "FormioTables"] diff --git a/forms-flow-data-layer/src/models/formio/form.py b/forms-flow-data-layer/src/models/formio/form.py index 81573e7de1..468364addd 100644 --- a/forms-flow-data-layer/src/models/formio/form.py +++ b/forms-flow-data-layer/src/models/formio/form.py @@ -1,11 +1,10 @@ +from datetime import datetime from typing import Optional from beanie import Document from .constants import FormioTables -# currently this file is not used in the codebase, but it is kept for future use - class FormModel(Document): title: str @@ -13,9 +12,19 @@ class FormModel(Document): path: str type: str isBundle: Optional[bool] - display: Optional[str] = None - created_at: Optional[str] = None - updated_at: Optional[str] = None + display: Optional[str] = None + created: Optional[datetime] = None + modified: Optional[datetime] = None + parentFormId: Optional[str] = None class Settings: name = FormioTables.FORMS.value + + @classmethod + async def count(cls, **filters): + """Count number of entries that match the passed filters.""" + query = cls.find_all() + for filter, value in filters.items(): + if hasattr(cls, filter): + query = query.find(getattr(cls, filter) == value) + return (await query.count()) diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index 727d5ff648..626ec68b11 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -6,13 +6,23 @@ from .constants import FormioTables -class SubmissionsModel(Document): +class SubmissionModel(Document): data: dict _id: PydanticObjectId + form: PydanticObjectId class Settings: name = FormioTables.SUBMISSIONS.value + @classmethod + async def count(cls, filters): + """Count number of entries that match the passed filters.""" + query = cls.find_all() + for filter, value in filters.items(): + if hasattr(cls, filter): + query = query.find(getattr(cls, filter) == value) + return (await query.count()) + @staticmethod def _build_match_stage(submission_ids: List[str], filter: Optional[dict]) -> dict: """Build the MongoDB match stage.""" @@ -63,29 +73,29 @@ async def query_submission( Query submissions from MongoDB with optional pagination and sorting. """ # Build match stage - match_stage = SubmissionsModel._build_match_stage( + match_stage = SubmissionModel._build_match_stage( submission_ids=submission_ids, filter=filter ) pipeline = [{"$match": match_stage}] # Add sorting if sort_by is specified - if sort_stage := SubmissionsModel._build_sort_stage(sort_by, sort_order): + if sort_stage := SubmissionModel._build_sort_stage(sort_by, sort_order): pipeline.append(sort_stage) # Projection stage - pipeline.append(SubmissionsModel._build_projection_stage(selected_form_fields)) + pipeline.append(SubmissionModel._build_projection_stage(selected_form_fields)) # Only add pagination if page_no and limit specified if page_no is not None and limit is not None: # Get only the count (no document data) count_pipeline = pipeline + [{"$count": "total"}] - count_result = await SubmissionsModel.aggregate(count_pipeline).to_list(length=1) + count_result = await SubmissionModel.aggregate(count_pipeline).to_list(length=1) total = count_result[0]["total"] if count_result else 0 # Add skip and limit stages for pagination pipeline.append({"$skip": (page_no - 1) * limit}) pipeline.append({"$limit": limit}) - items = await SubmissionsModel.aggregate(pipeline).to_list() + items = await SubmissionModel.aggregate(pipeline).to_list() else: - items = await SubmissionsModel.aggregate(pipeline).to_list() + items = await SubmissionModel.aggregate(pipeline).to_list() total = len(items) return { "submissions": items, diff --git a/forms-flow-data-layer/src/models/webapi/__init__.py b/forms-flow-data-layer/src/models/webapi/__init__.py index af0ff0f4c4..231dc73fca 100644 --- a/forms-flow-data-layer/src/models/webapi/__init__.py +++ b/forms-flow-data-layer/src/models/webapi/__init__.py @@ -2,7 +2,7 @@ from src.models.webapi.authorization import Authorization from src.models.webapi.base import BaseModel from src.models.webapi.constants import WebApiTables -from src.models.webapi.formprocess_mapper import FormProcessMapper +from src.models.webapi.form_process_mapper import FormProcessMapper __all__ = [ "Authorization", diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 30db01b92c..9d2d9fa997 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -1,13 +1,12 @@ from datetime import datetime + from sqlalchemy import and_, desc, or_, select from sqlalchemy.sql import func -from src.db.webapi_db import webapi_db - from .authorization import Authorization, AuthType from .base import BaseModel from .constants import WebApiTables -from .formprocess_mapper import FormProcessMapper +from .form_process_mapper import FormProcessMapper class Application(BaseModel): @@ -16,13 +15,27 @@ class Application(BaseModel): This class provides methods to interact with the application table. """ - _application = None + _table_name = WebApiTables.APPLICATION.value + _table = None @classmethod - async def get_table(cls): - if cls._application is None: - cls._application = await webapi_db.get_table(WebApiTables.APPLICATION.value) - return cls._application + async def first(cls, **filters): + return await super().first(**filters) + + @classmethod + async def find_all(cls, **filters): + query = await super().find_all(**filters) + table = await cls.get_table() + + # Apply date filters, if any + if (order_by := filters.get("order_by")) and hasattr(table.c, order_by): + query = query.order_by(order_by) + if from_date := filters.get("from_date"): + query = query.where(getattr(table.c, order_by) >= datetime.fromisoformat(from_date)) + if to_date := filters.get("to_date"): + query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) + + return query @classmethod def filter_query(cls, query, filter_data: dict, application_table, mapper_table): @@ -41,7 +54,6 @@ def filter_query(cls, query, filter_data: dict, application_table, mapper_table) else: # For other fields, use ilike for case-insensitive search query = query.where(col.ilike(f"%{value}%")) - return query @classmethod def paginationed_query(cls, query, page_no: int = 1, limit: int = 5): diff --git a/forms-flow-data-layer/src/models/webapi/authorization.py b/forms-flow-data-layer/src/models/webapi/authorization.py index 7335a49f72..4e581ddddd 100644 --- a/forms-flow-data-layer/src/models/webapi/authorization.py +++ b/forms-flow-data-layer/src/models/webapi/authorization.py @@ -2,8 +2,6 @@ from sqlalchemy import and_, or_, select -from src.db.webapi_db import webapi_db - from .base import BaseModel from .constants import WebApiTables @@ -19,15 +17,8 @@ class Authorization(BaseModel): Authorization class to handle authorization-related information. """ - _authorization_table = None # Class-level cache - - @classmethod - async def get_table(cls): - if cls._authorization_table is None: - cls._authorization_table = await webapi_db.get_table( - WebApiTables.AUTHORIZATION.value - ) - return cls._authorization_table + _table_name = WebApiTables.AUTHORIZATION.value + _table = None # Class-level cache @classmethod async def get_role_conditions(cls, authorization_table, roles: list[str]): diff --git a/forms-flow-data-layer/src/models/webapi/base.py b/forms-flow-data-layer/src/models/webapi/base.py index 3841e0f6b3..4e71f4ff37 100644 --- a/forms-flow-data-layer/src/models/webapi/base.py +++ b/forms-flow-data-layer/src/models/webapi/base.py @@ -1,3 +1,6 @@ +from sqlalchemy import select +from sqlalchemy.sql import func + from src.db.webapi_db import webapi_db @@ -8,9 +11,7 @@ class BaseModel: @classmethod async def _get_session(cls): - """ - Returns the webapi session object. - """ + """Returns the webapi session object.""" if cls._webapi_session is None: cls._webapi_session = await webapi_db.get_session() return cls._webapi_session @@ -22,3 +23,32 @@ async def execute(cls, query): """ async with webapi_db.get_session() as session: return await session.execute(query) + + @classmethod + async def get_table(cls): + """Gets and caches a SQLAlchemy table.""" + if cls._table is None: + cls._table = await webapi_db.get_table(cls._table_name) + return cls._table + + @classmethod + async def count(cls, **filters): + """Count number of entries that match the passed filters.""" + stmt = await cls.find_all(**filters) + return (await cls.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one() + + @classmethod + async def first(cls, **filters): + """Find the first entries that match the passed filters.""" + stmt = await cls.find_all(**filters) + return (await cls.execute(stmt)).first() + + @classmethod + async def find_all(cls, **filters): + """Find all entries that match the passed filters.""" + table = await cls.get_table() + stmt = select(table) + for key, value in filters.items(): + if hasattr(table.c, key): + stmt = stmt.where(getattr(table.c, key) == value) + return stmt diff --git a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py new file mode 100644 index 0000000000..7d56c30c69 --- /dev/null +++ b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py @@ -0,0 +1,85 @@ +from datetime import datetime + +from sqlalchemy import and_, or_, select +from sqlalchemy.schema import Table +from sqlalchemy.sql.expression import Select +from sqlalchemy.sql import func + + +from src.utils import UserContext + +from .authorization import Authorization, AuthType +from .base import BaseModel +from .constants import WebApiTables + + +class FormProcessMapper(BaseModel): + """ + FormProcessMapper class to handle mapper-related information. + """ + + _table_name = WebApiTables.FORM_PROCESS_MAPPER.value + _table = None # cache for the mapper table + + @classmethod + async def first( + cls, + user_context: UserContext = None, + **filters + ): + table = await cls.get_table() + query = await super().first(**filters) + if user_context: + query = await cls.apply_tenant_auth(query, table, user_context) + return query + + @classmethod + async def find_all( + cls, + user_context: UserContext = None, + **filters + ): + table = await cls.get_table() + query = await super().find_all(**filters) + if user_context: + query = await cls.apply_tenant_auth(query, table, user_context) + + # Apply date filters, if any + if (order_by := filters.get("order_by")) and hasattr(table.c, order_by): + query = query.order_by(order_by) + if from_date := filters.get("from_date"): + query = query.where(getattr(table.c, order_by) >= datetime.fromisoformat(from_date)) + if to_date := filters.get("to_date"): + query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) + + # Get total count + count = (await cls.execute(select(func.count()).select_from(query.subquery()))).scalar_one() + return query, count + + @classmethod + async def apply_tenant_auth( + cls, + query: Select, + table: Table, + user_context: UserContext + ): + """Takes a SQLAlchemy Select query and applies additional tenant auth checks.""" + # Parse user context info + tenant_key = user_context.tenant_key + user_groups = user_context.token_info.get("groups", []) + + # Build role conditions array + auth_table = await Authorization.get_table() + role_conditions = await Authorization.get_role_conditions(auth_table, user_groups) + + # Apply tenant auth checks + query = query.join( + auth_table, + and_( + table.c.parent_form_id == auth_table.c.resource_id, + auth_table.c.tenant == tenant_key, + or_(*role_conditions), # ⬅️ Role conditions + auth_table.c.auth_type == AuthType.APPLICATION.value, + ), + ) + return query \ No newline at end of file diff --git a/forms-flow-data-layer/src/models/webapi/formprocess_mapper.py b/forms-flow-data-layer/src/models/webapi/formprocess_mapper.py deleted file mode 100644 index 326be3cde4..0000000000 --- a/forms-flow-data-layer/src/models/webapi/formprocess_mapper.py +++ /dev/null @@ -1,20 +0,0 @@ -from src.db.webapi_db import webapi_db - -from .base import BaseModel -from .constants import WebApiTables - - -class FormProcessMapper(BaseModel): - """ - FormProcessMapper class to handle mapper-related information. - """ - - _mapper = None # cache for the mapper table - - @classmethod - async def get_table(cls): - if cls._mapper is None: - cls._mapper = await webapi_db.get_table( - WebApiTables.FORM_PROCESS_MAPPER.value - ) - return cls._mapper diff --git a/forms-flow-web/package-lock.json b/forms-flow-web/package-lock.json index f00a5bd3eb..feeaf86305 100644 --- a/forms-flow-web/package-lock.json +++ b/forms-flow-web/package-lock.json @@ -33,6 +33,7 @@ "camunda-bpmn-js-behaviors": "^1.2.1", "camunda-bpmn-moddle": "^7.0.1", "camunda-dmn-moddle": "^1.3.0", + "chart.js": "^4.5.0", "connected-react-router": "^6.9.1", "craco-plugin-single-spa-app-aot": "^2.0.4", "create-react-class": "^15.7.0", @@ -53,6 +54,7 @@ "querystring": "^0.2.1", "react": "^17.0.2", "react-bootstrap": "^1.6.0", + "react-chartjs-2": "^5.3.0", "react-date-range": "^1.1.3", "react-datepicker": "^3.8.0", "react-dom": "^17.0.2", @@ -66,7 +68,6 @@ "react-scripts": "^5.0.1", "react-select": "^3.2.0", "react-toastify": "^7.0.4", - "recharts": "^2.12.7", "redux": "^4.1.0", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", @@ -3469,6 +3470,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -4769,60 +4776,6 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" - }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -7234,6 +7187,18 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -8267,116 +8232,6 @@ "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz", "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==" }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "engines": { - "node": ">=12" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8498,11 +8353,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -10291,14 +10141,6 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" }, - "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -11933,14 +11775,6 @@ "node": ">= 0.4" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "engines": { - "node": ">=12" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -17027,6 +16861,16 @@ "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-date-picker": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-8.4.0.tgz", @@ -17678,20 +17522,6 @@ "react": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-toastify": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.4.tgz", @@ -17751,49 +17581,6 @@ "node": ">=8.10.0" } }, - "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/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/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -20845,27 +20632,6 @@ "node": ">= 0.8" } }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -24173,6 +23939,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -24994,60 +24765,6 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, - "@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" - }, - "@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" - }, - "@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" - }, - "@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "requires": { - "@types/d3-color": "*" - } - }, - "@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" - }, - "@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "requires": { - "@types/d3-time": "*" - } - }, - "@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "requires": { - "@types/d3-path": "*" - } - }, - "@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" - }, - "@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" - }, "@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -26939,6 +26656,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -27681,83 +27406,6 @@ "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz", "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==" }, - "d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "requires": { - "internmap": "1 - 2" - } - }, - "d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" - }, - "d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" - }, - "d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" - }, - "d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "requires": { - "d3-color": "1 - 3" - } - }, - "d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" - }, - "d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "requires": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - } - }, - "d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "requires": { - "d3-path": "^3.1.0" - } - }, - "d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "requires": { - "d3-array": "2 - 3" - } - }, - "d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "requires": { - "d3-time": "1 - 3" - } - }, - "d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" - }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -27842,11 +27490,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" }, - "decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -29231,11 +28874,6 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" }, - "fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==" - }, "fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -30402,11 +30040,6 @@ "side-channel": "^1.1.0" } }, - "internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" - }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -33956,6 +33589,12 @@ "prop-types": "^15.6.0" } }, + "react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "requires": {} + }, "react-date-picker": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-8.4.0.tgz", @@ -34420,16 +34059,6 @@ "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", "requires": {} }, - "react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "requires": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - } - }, "react-toastify": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.4.tgz", @@ -34475,41 +34104,6 @@ "picomatch": "^2.2.1" } }, - "recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "requires": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "dependencies": { - "clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - } - } - }, - "recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "requires": { - "decimal.js-light": "^2.4.1" - } - }, "recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -36782,27 +36376,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, - "victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "requires": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/forms-flow-web/package.json b/forms-flow-web/package.json index b2d9842c6a..43ad996371 100644 --- a/forms-flow-web/package.json +++ b/forms-flow-web/package.json @@ -64,6 +64,7 @@ "camunda-bpmn-js-behaviors": "^1.2.1", "camunda-bpmn-moddle": "^7.0.1", "camunda-dmn-moddle": "^1.3.0", + "chart.js": "^4.5.0", "connected-react-router": "^6.9.1", "craco-plugin-single-spa-app-aot": "^2.0.4", "create-react-class": "^15.7.0", @@ -84,6 +85,7 @@ "querystring": "^0.2.1", "react": "^17.0.2", "react-bootstrap": "^1.6.0", + "react-chartjs-2": "^5.3.0", "react-date-range": "^1.1.3", "react-datepicker": "^3.8.0", "react-dom": "^17.0.2", @@ -97,7 +99,6 @@ "react-scripts": "^5.0.1", "react-select": "^3.2.0", "react-toastify": "^7.0.4", - "recharts": "^2.12.7", "redux": "^4.1.0", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", diff --git a/forms-flow-web/src/apiManager/endpoints/config.js b/forms-flow-web/src/apiManager/endpoints/config.js index 9e95cf2b82..48cd83d44c 100644 --- a/forms-flow-web/src/apiManager/endpoints/config.js +++ b/forms-flow-web/src/apiManager/endpoints/config.js @@ -2,7 +2,11 @@ export const WEB_BASE_URL = (window._env_ && window._env_.REACT_APP_WEB_BASE_URL) || process.env.REACT_APP_WEB_BASE_URL; - export const DOCUMENT_SERVICE_URL = +export const GRAPHQL_URL = + (window._env_ && window._env_.REACT_APP_GRAPHQL_API_URL) || + process.env.REACT_APP_GRAPHQL_API_URL; + +export const DOCUMENT_SERVICE_URL = (window._env_ && window._env_.REACT_APP_DOCUMENT_SERVICE_URL) || process.env.REACT_APP_DOCUMENT_SERVICE_URL; diff --git a/forms-flow-web/src/apiManager/endpoints/index.js b/forms-flow-web/src/apiManager/endpoints/index.js index 5e8a44e1e1..8ead3525c8 100644 --- a/forms-flow-web/src/apiManager/endpoints/index.js +++ b/forms-flow-web/src/apiManager/endpoints/index.js @@ -1,6 +1,7 @@ /* eslint-disable max-len */ import { WEB_BASE_URL, + GRAPHQL_URL, MT_ADMIN_BASE_URL, MT_ADMIN_BASE_URL_VERSION, BPM_BASE_URL_EXT, @@ -11,6 +12,7 @@ import { import { AppConfig } from "../../config"; const API = { + GRAPHQL: GRAPHQL_URL, GET_DASHBOARDS: `${WEB_BASE_URL}/dashboards`, METRICS_SUBMISSIONS: `${WEB_BASE_URL}/metrics`, APPLICATION_START: `${WEB_BASE_URL}/application/create`, diff --git a/forms-flow-web/src/apiManager/services/metricsServices.js b/forms-flow-web/src/apiManager/services/metricsServices.js index 00774ba7e9..8f737f9712 100644 --- a/forms-flow-web/src/apiManager/services/metricsServices.js +++ b/forms-flow-web/src/apiManager/services/metricsServices.js @@ -1,4 +1,4 @@ -import { RequestService } from "@formsflow/service"; +import { RequestService, StorageService } from "@formsflow/service"; import API from "../endpoints"; import { setMetricsDateRangeLoading, @@ -27,22 +27,48 @@ export const fetchMetricsSubmissionCount = ( const done = rest.length ? rest[0] : () => { }; return (dispatch) => { dispatch(setMetricsLoadError(false)); - /*eslint max-len: ["error", { "code": 170 }]*/ - let url = `${API.METRICS_SUBMISSIONS}?from=${fromDate}&to=${toDate}&orderBy=${searchBy}&pageNo=${pageNo}&limit=${limit}&sortBy=${sortsBy}&sortOrder=${sortOrder}`; - if (formName) { - url += `&formName=${encodeURIComponent(formName)}`; - } - RequestService.httpGETRequest(url, {}) + // Build GraphQL request params + const url = API.GRAPHQL; + const headers = { + Authorization: `Bearer ${StorageService.get(StorageService.User.AUTH_TOKEN)}`, + 'Content-Type': 'application/json' + }; + const query = ` + query Query { + getForms( + formName: "${formName}" + fromDate: "${fromDate}" + limit: ${limit} + pageNo: ${pageNo} + orderBy: "${searchBy}" + toDate: "${toDate}" + ) { + items { + id + parentFormId + totalSubmissions + type + version + status + title + } + totalCount + } + } + `; + RequestService.httpPOSTRequest(url, { + query: query + }, null, true, headers) .then((res) => { if (res.data) { dispatch(setMetricsDateRangeLoading(false)); dispatch(setMetricsLoader(false)); dispatch(setMetricsStatusLoader(false)); - dispatch(setMetricsSubmissionCount(res.data.applications)); - dispatch(setMetricsTotalItems(res.data.totalCount)); - if (res.data.applications && res.data.applications[0]) { - dispatch(setSelectedMetricsId(res.data.applications[0].parentFormId)); + dispatch(setMetricsSubmissionCount(res.data.data.getForms.items)); + dispatch(setMetricsTotalItems(res.data.data.getForms.totalCount)); + if (res.data.data.getForms?.items[0]) { + dispatch(setSelectedMetricsId(res.data.data.getForms.items[0].parentFormId)); } else { dispatch(setSelectedMetricsId(null)); @@ -72,7 +98,7 @@ export const fetchMetricsSubmissionStatusCount = ( id, fromDate, toDate, - setSearchBy, + searchBy, options = {}, ...rest ) => { @@ -82,12 +108,32 @@ export const fetchMetricsSubmissionStatusCount = ( dispatch(setSelectedMetricsId(id)); } - RequestService.httpGETRequest( - `${API.METRICS_SUBMISSIONS}/${id}?from=${fromDate}&to=${toDate}&orderBy=${setSearchBy}&formType=${options.parentId ? "parent" : "form"}` - ) + // Build GraphQL request params + const url = API.GRAPHQL; + const headers = { + Authorization: `Bearer ${StorageService.get(StorageService.User.AUTH_TOKEN)}`, + 'Content-Type': 'application/json' + }; + const query = ` + query Query { + getMetricsSubmissionStatus( + formId: "${id}" + fromDate: "${fromDate}" + orderBy: "${toDate}" + toDate: "${searchBy}" + ) { + count + metric + } + } + `; + + RequestService.httpPOSTRequest(url, { + query: query + }, null, true, headers) .then((res) => { if (res.data) { - dispatch(setMetricsSubmissionStatusCount(res.data.applications)); + dispatch(setMetricsSubmissionStatusCount(res.data.data.getMetricsSubmissionStatus)); dispatch(setMetricsStatusLoader(false)); // dispatch(setMetricsTotalItems(res.data.totalCount)); done(null, res.data); diff --git a/forms-flow-web/src/components/Dashboard/ApplicationCounter.js b/forms-flow-web/src/components/Dashboard/ApplicationCounter.js index 47aed94df4..161d9c1149 100644 --- a/forms-flow-web/src/components/Dashboard/ApplicationCounter.js +++ b/forms-flow-web/src/components/Dashboard/ApplicationCounter.js @@ -9,7 +9,7 @@ const ApplicationCounter = React.memo((props) => { application, getStatusDetails, noOfApplicationsAvailable, - setSHowSubmissionData, + setShowSubmissionData, } = props; if (noOfApplicationsAvailable === 0) { return ( @@ -28,12 +28,12 @@ const ApplicationCounter = React.memo((props) => {
{ - setSHowSubmissionData(app); + setShowSubmissionData(app); }} key={idx} >
diff --git a/forms-flow-web/src/components/Dashboard/CardFormCounter.js b/forms-flow-web/src/components/Dashboard/CardFormCounter.js index 0c49e4c5d0..ddc8f72887 100644 --- a/forms-flow-web/src/components/Dashboard/CardFormCounter.js +++ b/forms-flow-web/src/components/Dashboard/CardFormCounter.js @@ -4,11 +4,11 @@ import { Translation } from "react-i18next"; import { useSelector } from "react-redux"; const CardFormCounter = React.memo((props) => { - const { submitionData, getStatusDetails } = props; + const { submissionData, getStatusDetails } = props; const selectedMetricsId = useSelector( (state) => state.metrics?.selectedMetricsId ); - const { formName, parentFormId, applicationCount } = submitionData; + const { title, parentFormId, totalSubmissions } = submissionData; return (
{ delay={{ show: 0, hide: 400 }} overlay={(propsData) => ( - {formName} + {title} )} > - {formName} + {title}
@@ -43,7 +43,7 @@ const CardFormCounter = React.memo((props) => {
-
{applicationCount}
+
{totalSubmissions}
{(t) => t("Total Submissions")}
diff --git a/forms-flow-web/src/components/Dashboard/Dashboard.js b/forms-flow-web/src/components/Dashboard/Dashboard.js index b02b34176f..f84bb5689a 100644 --- a/forms-flow-web/src/components/Dashboard/Dashboard.js +++ b/forms-flow-web/src/components/Dashboard/Dashboard.js @@ -72,7 +72,7 @@ const Dashboard = React.memo(() => { const [searchBy, setSearchBy] = useState("created"); const [sortsBy, setSortsBy] = useState("formName"); - const [showSubmissionData, setSHowSubmissionData] = useState(submissionsList[0]); + const [showSubmissionData, setShowSubmissionData] = useState(submissionsList[0]); const [show, setShow] = useState(false); // State to set search text for submission data //Array for pagination dropdown @@ -120,8 +120,7 @@ const Dashboard = React.memo(() => { const getFormattedDate = (date) => { return moment .utc(date) - .format("YYYY-MM-DDTHH:mm:ssZ") - .replace(/\+/g, "%2B"); + .format("YYYY-MM-DDTHH:mm:ssZ"); }; useEffect(() => { @@ -134,7 +133,7 @@ const Dashboard = React.memo(() => { }, [dispatch, activePage, limit, sortsBy, sortOrder, dateRange, searchText, searchBy]); useEffect(() => { - setSHowSubmissionData(submissionsList[0]); + setShowSubmissionData(submissionsList[0]); }, [submissionsList]); const onChangeInput = (option) => { @@ -272,7 +271,7 @@ const Dashboard = React.memo(() => { application={submissionsList} getStatusDetails={getStatusDetails} noOfApplicationsAvailable={noOfApplicationsAvailable} - setSHowSubmissionData={setSHowSubmissionData} + setShowSubmissionData={setShowSubmissionData} /> )}
@@ -353,10 +352,8 @@ const Dashboard = React.memo(() => {

{t("Submission Status")}

-
setShow(false)} aria-label="Close"> - -
- + + { const { submissionsStatusList, submissionData, submissionStatusCountLoader } = props; - const {formVersions, formName, parentFormId} = submissionData; + const {title} = submissionData; + const { t } = useTranslation(); - const sortedVersions = useMemo(()=> - (formVersions?.sort((version1, version2)=> - version1.version > version2.version ? 1 : -1)),[formVersions]); + const [selectedChartValue, setSelectedChartValue] = useState('pie'); - const version = formVersions?.length; - const { t } = useTranslation(); - const pieData = submissionsStatusList || []; + let chartLabels = []; + let chartDataset = []; + submissionsStatusList.map((metric) => { + chartLabels.push(metric.metric); + chartDataset.push(metric.count); + }); + + const chartData = { + labels: chartLabels, + datasets: [{ + label: `${title} Dataset`, + data: chartDataset, + backgroundColor: BACKGROUND_COLORS, + borderColor: BORDER_COLORS, + borderWidth: 1 + }] + }; + + const chartOptions = { + plugins: { + legend: { + position: 'bottom' + } + } + }; - const handlePieData = (value) => { - const isParentId = value === "all"; - const id = isParentId ? parentFormId : value; - const option = {parentId : isParentId}; - props.getStatusDetails(id,option); + const renderChart = () => { + switch (selectedChartValue) { + case 'pie': + return ( + + ); + case 'v-bar': + return ( + + ); + case 'doughnut': + return ( + + ); + case 'polar-area': + return ( + + ); + case 'radar': + return ( + + ); + } }; @@ -40,102 +141,31 @@ const ChartForm = React.memo((props) => {
-
-
-
- - {t("Form Name")} : - -

- {formName} -

-
- -

- {t("Latest Version")} :{" "} - {`v${version}`} -

-
- {sortedVersions.length > 1 && ( -
-

- {t("Select form version")}: -

- -
-)} - - -
-
- -
- - - - - {pieData.map((entry) => ( - -))} - - -
- { - pieData.length ? ( -
- {pieData.map((entry, index) => ( -
- -
{entry.statusName}
+ active={submissionStatusCountLoader} + spinner + text={t("Loading...")} + > +
+
+ {renderChart()} +
+
+
+ setSelectedChartValue(e.target.value)} + className="form-select p-1" + title={t("Choose any")} + aria-label="Select chart type" + > + {CHART_TYPES.map((option, index) => ( + + ))} +
- ))} +
- ) : ( -
- {t("No submissions")} -
- )} -
-
@@ -143,4 +173,4 @@ const ChartForm = React.memo((props) => { ); }); -export default ChartForm; +export default ChartForm; \ No newline at end of file