From a61a360e9e05c5b105bd1c6b9bb0fd639961fc15 Mon Sep 17 00:00:00 2001 From: Stefan Fleckenstein Date: Wed, 11 Dec 2024 21:09:50 +0000 Subject: [PATCH] feat: top level observation review list (#2337) --- backend/application/core/api/filters.py | 50 ++- .../core/api/serializers_observation.py | 4 + backend/application/core/api/views.py | 35 +- frontend/src/App.tsx | 4 + frontend/src/commons/layout/Menu.tsx | 2 +- .../AssessmentBulkApproval.tsx | 6 +- .../ObservationLogApprovalEmbeddedList.tsx | 180 ---------- .../ObservationLogApprovalList.tsx | 310 +++++++++++++----- .../ObservationLogEmbeddedList.tsx | 2 +- .../observation_logs/ObservationLogShow.tsx | 4 +- .../observations/ObservationReviewList.tsx | 120 +++++-- .../src/core/observations/ObservationShow.tsx | 6 +- frontend/src/core/observations/functions.ts | 2 + frontend/src/core/products/ProductReviews.tsx | 4 +- frontend/src/core/reviews/Reviews.tsx | 139 ++++++++ 15 files changed, 559 insertions(+), 309 deletions(-) delete mode 100644 frontend/src/core/observation_logs/ObservationLogApprovalEmbeddedList.tsx create mode 100644 frontend/src/core/reviews/Reviews.tsx diff --git a/backend/application/core/api/filters.py b/backend/application/core/api/filters.py index 74f979070..5fedab57d 100644 --- a/backend/application/core/api/filters.py +++ b/backend/application/core/api/filters.py @@ -284,16 +284,58 @@ class ObservationLogFilter(FilterSet): origin_component_name_version = CharFilter( field_name="observation__origin_component_name_version", lookup_expr="icontains" ) + origin_docker_image_name_tag_short = CharFilter( + field_name="observation__origin_docker_image_name_tag_short", + lookup_expr="icontains", + ) + origin_endpoint_hostname = CharFilter( + field_name="observation__origin_endpoint_hostname", lookup_expr="icontains" + ) + origin_source_file = CharFilter( + field_name="observation__origin_source_file", lookup_expr="icontains" + ) + origin_cloud_qualified_resource = CharFilter( + field_name="observation__origin_cloud_qualified_resource", + lookup_expr="icontains", + ) + origin_kubernetes_qualified_resource = CharFilter( + field_name="observation__origin_kubernetes_qualified_resource", + lookup_expr="icontains", + ) ordering = OrderingFilter( # tuple-mapping retains order fields=( ("id", "id"), ("user__full_name", "user_full_name"), - ("observation__product__name", "product_name"), - ("observation__product__product_group__name", "product.product_group_name"), - ("observation__branch__name", "branch_name"), - ("observation__title", "observation_title"), + ("observation__product__name", "observation_data.product_data.name"), + ( + "observation__product__product_group__name", + "observation_data.product_data.product_group_name", + ), + ("observation__branch__name", "observation_data.branch_name"), + ("observation__title", "observation_data.title"), + ( + "observation__origin_component_name_version", + "observation_data.origin_component_name_version", + ), + ( + "observation__origin_docker_image_name_tag_short", + "observation_data.origin_docker_image_name_tag_short", + ), + ( + "observation__origin_endpoint_hostname", + "observation_data.origin_endpoint_hostname", + ), + ("observation__origin_source_file", "observation_data.origin_source_file"), + ( + "observation__origin_cloud_qualified_resource", + "observation_data.origin_cloud_qualified_resource", + ), + ( + "observation__origin_kubernetes_qualified_resource", + "observation_data.origin_kubernetes_qualified_resource", + ), ("severity", "severity"), ("status", "status"), ("comment", "comment"), diff --git a/backend/application/core/api/serializers_observation.py b/backend/application/core/api/serializers_observation.py index e6decdacc..0802433eb 100644 --- a/backend/application/core/api/serializers_observation.py +++ b/backend/application/core/api/serializers_observation.py @@ -593,3 +593,7 @@ class PotentialDuplicateSerializer(ModelSerializer): class Meta: model = Potential_Duplicate fields = "__all__" + + +class CountSerializer(Serializer): + count = IntegerField() diff --git a/backend/application/core/api/views.py b/backend/application/core/api/views.py index 14029af39..409648239 100644 --- a/backend/application/core/api/views.py +++ b/backend/application/core/api/views.py @@ -12,7 +12,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.status import HTTP_204_NO_CONTENT +from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT from rest_framework.viewsets import GenericViewSet, ModelViewSet from application.access_control.services.authorization import user_has_permission_or_403 @@ -40,6 +40,7 @@ UserHasServicePermission, ) from application.core.api.serializers_observation import ( + CountSerializer, EvidenceSerializer, ObservationAssessmentSerializer, ObservationBulkAssessmentSerializer, @@ -626,6 +627,18 @@ def bulk_assessment(self, request): ) return Response(status=HTTP_204_NO_CONTENT) + @extend_schema( + methods=["GET"], + request=None, + responses={HTTP_200_OK: CountSerializer}, + ) + @action(detail=False, methods=["get"]) + def count_reviews(self, request): + count = ( + get_observations().filter(current_status=Status.STATUS_IN_REVIEW).count() + ) + return Response(status=HTTP_200_OK, data={"count": count}) + class ObservationTitleViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin): serializer_class = ObservationTitleSerializer @@ -684,11 +697,11 @@ def approval(self, request, pk=None): return Response() @extend_schema( - methods=["PATCH"], + methods=["POST"], request=ObservationLogBulkApprovalSerializer, responses={HTTP_204_NO_CONTENT: None}, ) - @action(detail=False, methods=["patch"]) + @action(detail=False, methods=["post"]) def bulk_approval(self, request): request_serializer = ObservationLogBulkApprovalSerializer(data=request.data) if not request_serializer.is_valid(): @@ -701,6 +714,22 @@ def bulk_approval(self, request): ) return Response(status=HTTP_204_NO_CONTENT) + @extend_schema( + methods=["GET"], + request=None, + responses={HTTP_200_OK: CountSerializer}, + ) + @action(detail=False, methods=["get"]) + def count_approvals(self, request): + count = ( + get_observation_logs() + .filter( + assessment_status=Assessment_Status.ASSESSMENT_STATUS_NEEDS_APPROVAL + ) + .count() + ) + return Response(status=HTTP_200_OK, data={"count": count}) + class EvidenceViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin): serializer_class = EvidenceSerializer diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fad286a14..e72078220 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,7 @@ import observation_logs from "./core/observation_logs"; import observations from "./core/observations"; import product_groups from "./core/product_groups"; import products from "./core/products"; +import Reviews from "./core/reviews/Reviews"; import { Dashboard } from "./dashboard"; import parsers from "./import_observations/parsers"; import LicenseAdministration from "./licenses/license_administration/LicenseAdministration"; @@ -65,6 +66,9 @@ const App = () => { } /> } /> } /> + } /> + } /> + } /> } /> { dense={dense} /> } diff --git a/frontend/src/core/observation_logs/AssessmentBulkApproval.tsx b/frontend/src/core/observation_logs/AssessmentBulkApproval.tsx index cca8cc8da..c6721c7cf 100644 --- a/frontend/src/core/observation_logs/AssessmentBulkApproval.tsx +++ b/frontend/src/core/observation_logs/AssessmentBulkApproval.tsx @@ -19,15 +19,15 @@ const AssessmentBulkApproval = () => { const assessmentUpdate = async (data: any) => { setLoading(true); - const patch = { + const post_data = { assessment_status: data.assessment_status, approval_remark: data.approval_remark, observation_logs: selectedIds, }; httpClient(window.__RUNTIME_CONFIG__.API_BASE_URL + "/observation_logs/bulk_approval/", { - method: "PATCH", - body: JSON.stringify(patch), + method: "POST", + body: JSON.stringify(post_data), }) .then(() => { refresh(); diff --git a/frontend/src/core/observation_logs/ObservationLogApprovalEmbeddedList.tsx b/frontend/src/core/observation_logs/ObservationLogApprovalEmbeddedList.tsx deleted file mode 100644 index 7cf608490..000000000 --- a/frontend/src/core/observation_logs/ObservationLogApprovalEmbeddedList.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { - AutocompleteInput, - Datagrid, - DateField, - FilterForm, - FunctionField, - ListContextProvider, - ReferenceInput, - ResourceContextProvider, - TextField, - TextInput, - useListController, -} from "react-admin"; - -import { CustomPagination } from "../../commons/custom_fields/CustomPagination"; -import { feature_vex_enabled } from "../../commons/functions"; -import { AutocompleteInputMedium } from "../../commons/layout/themes"; -import { getSettingListSize } from "../../commons/user_settings/functions"; -import { ASSESSMENT_STATUS_NEEDS_APPROVAL } from "../types"; -import { OBSERVATION_SEVERITY_CHOICES, OBSERVATION_STATUS_CHOICES } from "../types"; -import { commentShortened } from "./functions"; - -function listFilters(product: any) { - const filters = []; - if (product && product.has_branches) { - filters.push( - - - - ); - } - filters.push(); - - if (product && product.has_component) { - filters.push(); - } - if (product && product.has_docker_image) { - filters.push(); - } - if (product && product.has_endpoint) { - filters.push(); - } - if (product && product.has_source) { - filters.push(); - } - if (product && product.has_cloud_resource) { - filters.push(); - } - if (product && product.has_kubernetes_resource) { - filters.push(); - } - - filters.push( - - - , - , - - ); - return filters; -} - -type ObservationLogApprovalEmbeddedListProps = { - product: any; -}; - -const ObservationLogApprovalEmbeddedList = ({ product }: ObservationLogApprovalEmbeddedListProps) => { - const listContext = useListController({ - filter: { product: Number(product.id), assessment_status: ASSESSMENT_STATUS_NEEDS_APPROVAL }, - perPage: 25, - resource: "observation_logs", - sort: { field: "created", order: "ASC" }, - disableSyncWithLocation: true, - storeKey: "observation_logs.approvalembedded", - }); - - if (listContext.isLoading) { - return
Loading...
; - } - - const ShowObservationLogs = (id: any) => { - return "../../../../observation_logs/" + id + "/show"; - }; - - localStorage.setItem("observationlogapprovalembeddedlist", "true"); - localStorage.removeItem("observationlogapprovallist"); - localStorage.removeItem("observationlogembeddedlist"); - - return ( - - -
- - - {product && product.has_branches && ( - - )} - - {product && product.has_component && ( - - )} - {product && product.has_docker_image && ( - - )} - {product && product.has_endpoint && ( - - )} - {product && product.has_source && ( - - )} - {product && product.has_cloud_resource && ( - - )} - {product && product.has_kubernetes_resource && ( - - )} - - - - {feature_vex_enabled() && ( - - )} - commentShortened(record.comment)} - sortable={false} - sx={{ wordBreak: "break-word" }} - /> - - - -
-
-
- ); -}; - -export default ObservationLogApprovalEmbeddedList; diff --git a/frontend/src/core/observation_logs/ObservationLogApprovalList.tsx b/frontend/src/core/observation_logs/ObservationLogApprovalList.tsx index 6b88f9d9e..efeb964d1 100644 --- a/frontend/src/core/observation_logs/ObservationLogApprovalList.tsx +++ b/frontend/src/core/observation_logs/ObservationLogApprovalList.tsx @@ -1,19 +1,21 @@ -import ChecklistIcon from "@mui/icons-material/Checklist"; import { AutocompleteInput, Datagrid, DateField, + FilterForm, FunctionField, - List, + ListContextProvider, ReferenceInput, + ResourceContextProvider, TextField, TextInput, + useListController, } from "react-admin"; import { Fragment } from "react/jsx-runtime"; +import { PERMISSION_OBSERVATION_LOG_APPROVAL } from "../../access_control/types"; import { CustomPagination } from "../../commons/custom_fields/CustomPagination"; import { feature_vex_enabled } from "../../commons/functions"; -import ListHeader from "../../commons/layout/ListHeader"; import { AutocompleteInputMedium, AutocompleteInputWide } from "../../commons/layout/themes"; import { getSettingListSize } from "../../commons/user_settings/functions"; import { ASSESSMENT_STATUS_NEEDS_APPROVAL } from "../types"; @@ -21,94 +23,234 @@ import { OBSERVATION_SEVERITY_CHOICES, OBSERVATION_STATUS_CHOICES } from "../typ import AssessmentBulkApproval from "./AssessmentBulkApproval"; import { commentShortened } from "./functions"; -const BulkActionButtons = () => ( - - - -); +const BulkActionButtons = ({ product }: any) => { + return ( + + {(!product || (product && product.permissions.includes(PERMISSION_OBSERVATION_LOG_APPROVAL))) && ( + + )} + + ); +}; + +function listFilters(product: any) { + const filters = []; + if (!product) { + filters.push( + + + + ); + } + if (!product) { + filters.push( + + + + ); + } + if (!product) { + filters.push( + + + + ); + } + + if (product && product.has_branches) { + filters.push( + + + + ); + } -const listFilters = [ - - - , - - - , - - - , - , - - - , - , - , - , -]; + filters.push(); -const ObservationLogApprovalList = () => { - localStorage.setItem("observationlogapprovallist", "true"); - localStorage.removeItem("observationlogapprovalembeddedlist"); + if (!product || (product && product.has_component)) { + filters.push(); + } + if (!product || (product && product.has_docker_image)) { + filters.push(); + } + if (!product || (product && product.has_endpoint)) { + filters.push(); + } + if (!product || (product && product.has_source)) { + filters.push(); + } + if (!product || (product && product.has_cloud_resource)) { + filters.push(); + } + if (!product || (product && product.has_kubernetes_resource)) { + filters.push(); + } + + filters.push( + + + , + , + + ); + return filters; +} + +type ObservationLogApprovalListProps = { + product?: any; +}; + +const ObservationLogApprovalList = ({ product }: ObservationLogApprovalListProps) => { + let filter = {}; + filter = { assessment_status: ASSESSMENT_STATUS_NEEDS_APPROVAL }; + if (product) { + filter = { ...filter, product: Number(product.id) }; + } + let storeKey = "observation_logs.approval"; + if (product) { + storeKey = "observation_logs.approvalproduct"; + } + const listContext = useListController({ + filter: filter, + perPage: 25, + resource: "observation_logs", + sort: { field: "created", order: "ASC" }, + disableSyncWithLocation: true, + storeKey: storeKey, + }); + + if (listContext.isLoading) { + return
Loading...
; + } + + const ShowObservationLogs = (id: any) => { + return "../../../../observation_logs/" + id + "/show"; + }; + + if (product) { + localStorage.setItem("observationlogapprovallistproduct", "true"); + localStorage.removeItem("observationlogapprovallist"); + } else { + localStorage.setItem("observationlogapprovallist", "true"); + localStorage.removeItem("observationlogapprovallistproduct"); + } localStorage.removeItem("observationlogembeddedlist"); return ( - - - } - filters={listFilters} - sort={{ field: "created", order: "ASC" }} - disableSyncWithLocation={false} - storeKey="observation_logs.approval" - actions={false} - sx={{ marginTop: 1 }} - > - }> - - - - - - - - {feature_vex_enabled() && ( - + +
+ + + ) + } + rowClick={ShowObservationLogs} + resource="observation_logs" + > + {!product && } + {!product && ( + + )} + {(!product || (product && product.has_branches)) && ( + + )} + + {(!product || (product && product.has_component)) && ( + + )} + {(!product || (product && product.has_docker_image)) && ( + + )} + {(!product || (product && product.has_endpoint)) && ( + + )} + {(!product || (product && product.has_source)) && ( + + )} + {(!product || (product && product.has_cloud_resource)) && ( + + )} + {(!product || (product && product.has_kubernetes_resource)) && ( + + )} + + + + {feature_vex_enabled() && ( + + )} + commentShortened(record.comment)} + sortable={false} sx={{ wordBreak: "break-word" }} /> - )} - commentShortened(record.comment)} - sortable={false} - sx={{ wordBreak: "break-word" }} - /> - - - - + + + +
+
+ ); }; diff --git a/frontend/src/core/observation_logs/ObservationLogEmbeddedList.tsx b/frontend/src/core/observation_logs/ObservationLogEmbeddedList.tsx index 9f54f918a..1d2a1eef9 100644 --- a/frontend/src/core/observation_logs/ObservationLogEmbeddedList.tsx +++ b/frontend/src/core/observation_logs/ObservationLogEmbeddedList.tsx @@ -44,8 +44,8 @@ const ObservationLogEmbeddedList = ({ observation }: ObservationLogEmbeddedListP }; localStorage.setItem("observationlogembeddedlist", "true"); - localStorage.removeItem("observationlogapprovalembeddedlist"); localStorage.removeItem("observationlogapprovallist"); + localStorage.removeItem("observationlogapprovallistproduct"); return ( diff --git a/frontend/src/core/observation_logs/ObservationLogShow.tsx b/frontend/src/core/observation_logs/ObservationLogShow.tsx index 47d2c635d..8c0153ccf 100644 --- a/frontend/src/core/observation_logs/ObservationLogShow.tsx +++ b/frontend/src/core/observation_logs/ObservationLogShow.tsx @@ -43,14 +43,14 @@ const ShowActions = () => { if ( observation_log && observation_log.observation_data && - localStorage.getItem("observationlogapprovalembeddedlist") + localStorage.getItem("observationlogapprovallistproduct") ) { filter = { product: observation_log.observation_data.product, assessment_status: ASSESSMENT_STATUS_NEEDS_APPROVAL, }; sort = { field: "created", order: "ASC" }; - storeKey = "observation_logs.approvalembedded"; + storeKey = "observation_logs.approvalproduct"; } return ( diff --git a/frontend/src/core/observations/ObservationReviewList.tsx b/frontend/src/core/observations/ObservationReviewList.tsx index 58f36bd59..63f68a719 100644 --- a/frontend/src/core/observations/ObservationReviewList.tsx +++ b/frontend/src/core/observations/ObservationReviewList.tsx @@ -20,23 +20,65 @@ import { PERMISSION_OBSERVATION_ASSESSMENT } from "../../access_control/types"; import { CustomPagination } from "../../commons/custom_fields/CustomPagination"; import { SeverityField } from "../../commons/custom_fields/SeverityField"; import { humanReadableDate } from "../../commons/functions"; -import { AutocompleteInputMedium } from "../../commons/layout/themes"; +import { AutocompleteInputMedium, AutocompleteInputWide } from "../../commons/layout/themes"; import { getSettingListSize } from "../../commons/user_settings/functions"; import { AGE_CHOICES, OBSERVATION_SEVERITY_CHOICES, OBSERVATION_STATUS_IN_REVIEW, - OBSERVATION_STATUS_OPEN, Observation, PURL_TYPE_CHOICES, Product, } from "../types"; import ObservationBulkAssessment from "./ObservationBulkAssessment"; import ObservationExpand from "./ObservationExpand"; -import { IDENTIFIER_OBSERVATION_REVIEW_LIST, setListIdentifier } from "./functions"; +import { + IDENTIFIER_OBSERVATION_REVIEW_LIST, + IDENTIFIER_OBSERVATION_REVIEW_LIST_PRODUCT, + setListIdentifier, +} from "./functions"; function listFilters(product: Product) { const filters = []; + if (!product) { + filters.push( + + + + ); + } + if (!product) { + filters.push( + + + + ); + } + if (!product) { + filters.push( + + + + ); + } if (product && product.has_branches) { filters.push( ); filters.push( ); } - if (product && product.has_docker_image) { + if (!product || (product && product.has_docker_image)) { filters.push(); } - if (product && product.has_endpoint) { + if (!product || (product && product.has_endpoint)) { filters.push(); } - if (product && product.has_source) { + if (!product || (product && product.has_source)) { filters.push(); } - if (product && product.has_cloud_resource) { + if (!product || (product && product.has_cloud_resource)) { filters.push(); } - if (product && product.has_kubernetes_resource) { + if (!product || (product && product.has_kubernetes_resource)) { filters.push(); } @@ -110,28 +152,42 @@ const ShowObservations = (id: any) => { }; type ObservationsReviewListProps = { - product: any; + product?: any; }; -const BulkActionButtons = (product: any) => ( +const BulkActionButtons = ({ product }: any) => ( - {product.product.permissions.includes(PERMISSION_OBSERVATION_ASSESSMENT) && ( - + {(!product || (product && product.permissions.includes(PERMISSION_OBSERVATION_ASSESSMENT))) && ( + )} ); const ObservationsReviewList = ({ product }: ObservationsReviewListProps) => { - setListIdentifier(IDENTIFIER_OBSERVATION_REVIEW_LIST); + if (product) { + setListIdentifier(IDENTIFIER_OBSERVATION_REVIEW_LIST_PRODUCT); + } else { + setListIdentifier(IDENTIFIER_OBSERVATION_REVIEW_LIST); + } + + let filter = {}; + filter = { current_status: OBSERVATION_STATUS_IN_REVIEW }; + let filterDefaultValues = {}; + let storeKey = "observations.review"; + if (product) { + filter = { ...filter, product: Number(product.id) }; + filterDefaultValues = { branch: product.repository_default_branch }; + storeKey = "observations.review.product"; + } const listContext = useListController({ - filter: { product: Number(product.id), current_status: OBSERVATION_STATUS_IN_REVIEW }, + filter: filter, perPage: 25, resource: "observations", sort: { field: "current_severity", order: "ASC" }, - filterDefaultValues: { current_status: OBSERVATION_STATUS_OPEN, branch: product.repository_default_branch }, + filterDefaultValues: filterDefaultValues, disableSyncWithLocation: false, - storeKey: "observations.review", + storeKey: storeKey, }); if (listContext.isLoading) { @@ -148,8 +204,8 @@ const ObservationsReviewList = ({ product }: ObservationsReviewListProps) => { sx={{ width: "100%" }} rowClick={ShowObservations} bulkActionButtons={ - product && - product.permissions.includes(PERMISSION_OBSERVATION_ASSESSMENT) && ( + (!product || + (product && product.permissions.includes(PERMISSION_OBSERVATION_ASSESSMENT))) && ( ) } @@ -157,44 +213,52 @@ const ObservationsReviewList = ({ product }: ObservationsReviewListProps) => { expand={} expandSingle > - + {!product && } + {!product && } + {(!product || (product && product.has_branches)) && ( + + )} - {product && product.has_component && } + {(!product || (product && product.has_component)) && ( + + )} - {product && product.has_services && } - {product && product.has_component && ( + {(!product || (product && product.has_services)) && ( + + )} + {(!product || (product && product.has_component)) && ( )} - {product && product.has_docker_image && ( + {(!product || (product && product.has_docker_image)) && ( )} - {product && product.has_endpoint && ( + {(!product || (product && product.has_endpoint)) && ( )} - {product && product.has_source && ( + {(!product || (product && product.has_source)) && ( )} - {product && product.has_cloud_resource && ( + {(!product || (product && product.has_cloud_resource)) && ( )} - {product && product.has_kubernetes_resource && ( + {(!product || (product && product.has_kubernetes_resource)) && ( { @@ -54,8 +55,11 @@ const ShowActions = () => { current_status: OBSERVATION_STATUS_OPEN, }; storeKey = "observations.dashboard"; - } else if (observation && localStorage.getItem(IDENTIFIER_OBSERVATION_REVIEW_LIST) === "true") { + } else if (observation && localStorage.getItem(IDENTIFIER_OBSERVATION_REVIEW_LIST_PRODUCT) === "true") { filter = { product: observation.product, current_status: OBSERVATION_STATUS_IN_REVIEW }; + storeKey = "observations.review.product"; + } else if (localStorage.getItem(IDENTIFIER_OBSERVATION_REVIEW_LIST) === "true") { + filter = { current_status: OBSERVATION_STATUS_IN_REVIEW }; storeKey = "observations.review"; } diff --git a/frontend/src/core/observations/functions.ts b/frontend/src/core/observations/functions.ts index f59b4136d..7272443b1 100644 --- a/frontend/src/core/observations/functions.ts +++ b/frontend/src/core/observations/functions.ts @@ -2,12 +2,14 @@ export const IDENTIFIER_OBSERVATION_LIST = "observationlist"; export const IDENTIFIER_OBSERVATION_EMBEDDED_LIST = "observationembeddedlist"; export const IDENTIFIER_OBSERVATION_DASHBOARD_LIST = "observationdashboardlist"; export const IDENTIFIER_OBSERVATION_REVIEW_LIST = "observationreviewlist"; +export const IDENTIFIER_OBSERVATION_REVIEW_LIST_PRODUCT = "observationreviewlistproduct"; export function setListIdentifier(identifier: string): void { localStorage.removeItem(IDENTIFIER_OBSERVATION_LIST); localStorage.removeItem(IDENTIFIER_OBSERVATION_EMBEDDED_LIST); localStorage.removeItem(IDENTIFIER_OBSERVATION_DASHBOARD_LIST); localStorage.removeItem(IDENTIFIER_OBSERVATION_REVIEW_LIST); + localStorage.removeItem(IDENTIFIER_OBSERVATION_REVIEW_LIST_PRODUCT); localStorage.setItem(identifier, "true"); } diff --git a/frontend/src/core/products/ProductReviews.tsx b/frontend/src/core/products/ProductReviews.tsx index a1bfe5ad0..5d37afc06 100644 --- a/frontend/src/core/products/ProductReviews.tsx +++ b/frontend/src/core/products/ProductReviews.tsx @@ -4,7 +4,7 @@ import { Fragment } from "react"; import { getElevation } from "../../metrics/functions"; import ProductRuleApprovalList from "../../rules/product_rules/ProductRuleApprovalList"; -import ObservationLogApprovalEmbeddedList from "../observation_logs/ObservationLogApprovalEmbeddedList"; +import ObservationLogApprovalList from "../observation_logs/ObservationLogApprovalList"; import ObservationsReviewList from "../observations/ObservationReviewList"; type ProductReviewsProps = { @@ -59,7 +59,7 @@ const ProductReviews = ({ product }: ProductReviewsProps) => { - + )} diff --git a/frontend/src/core/reviews/Reviews.tsx b/frontend/src/core/reviews/Reviews.tsx new file mode 100644 index 000000000..dfc0aa4dc --- /dev/null +++ b/frontend/src/core/reviews/Reviews.tsx @@ -0,0 +1,139 @@ +import ChecklistIcon from "@mui/icons-material/Checklist"; +import { Badge, Box, Divider, Paper, Tab, Tabs } from "@mui/material"; +import { Fragment, useEffect, useState } from "react"; +import { useNotify } from "react-admin"; +import { Link, matchPath, useLocation } from "react-router-dom"; + +import ListHeader from "../../commons/layout/ListHeader"; +import { httpClient } from "../../commons/ra-data-django-rest-framework"; +import observation_logs from "../observation_logs"; +import ObservationLogApprovalList from "../observation_logs/ObservationLogApprovalList"; +import observations from "../observations"; +import ObservationsReviewList from "../observations/ObservationReviewList"; + +function useRouteMatch(patterns: readonly string[]) { + const { pathname } = useLocation(); + + for (const pattern of patterns) { + const possibleMatch = matchPath(pattern, pathname); + if (possibleMatch !== null) { + return possibleMatch; + } + } + return null; +} + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + return ( + + ); +} + +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + "aria-controls": `simple-tabpanel-${index}`, + }; +} + +export default function Reviews() { + const notify = useNotify(); + const [count_observation_reviews, setCountObservationReviews] = useState(0); + const [count_observation_log_approvals, setCountObservationLogApprovals] = useState(0); + + const fetchObservationReviews = async () => { + httpClient(window.__RUNTIME_CONFIG__.API_BASE_URL + "/observations/count_reviews/") + .then((response) => { + setCountObservationReviews(response.json.count); + }) + .catch((error) => { + notify(error.message, { type: "warning" }); + }); + }; + + const fetchObservationLogApprovals = async () => { + httpClient(window.__RUNTIME_CONFIG__.API_BASE_URL + "/observation_logs/count_approvals/") + .then((response) => { + setCountObservationLogApprovals(response.json.count); + }) + .catch((error) => { + notify(error.message, { type: "warning" }); + }); + }; + + useEffect(() => { + fetchObservationReviews(); + fetchObservationLogApprovals(); + }); + const routeMatch = useRouteMatch(["/reviews/observation_reviews", "/reviews/observation_log_approvals"]); + function currentTab(): number { + switch (routeMatch?.pattern?.path) { + case "/reviews/observation_reviews": { + return 0; + } + case "/reviews/observation_log_approvals": { + return 1; + } + default: { + return 0; + } + } + } + + return ( + + + + + + + + } + to="/reviews/observation_reviews" + component={Link} + {...a11yProps(0)} // nosemgrep: typescript.react.best-practice.react-props-spreading.react-props-spreading + // nosemgrep because the props are well defined in the import + /> + + + + } + to="/reviews/observation_log_approvals" + component={Link} + {...a11yProps(1)} // nosemgrep: typescript.react.best-practice.react-props-spreading.react-props-spreading + // nosemgrep because the props are well defined in the import + /> + + + + + + + + + + + ); +}