Skip to content

Commit ba987ad

Browse files
committed
feat: UI improvements
1 parent 59a0b4b commit ba987ad

File tree

9 files changed

+330
-153
lines changed

9 files changed

+330
-153
lines changed

backend/application/core/api/filters.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,9 @@ class ObservationLogFilter(FilterSet):
290290
fields=(
291291
("id", "id"),
292292
("user__full_name", "user_full_name"),
293-
("product__name", "product_name"),
294-
("product__product_group__name", "product.product_group_name"),
295-
("branch__name", "branch_name"),
293+
("observation__product__name", "product_name"),
294+
("observation__product__product_group__name", "product.product_group_name"),
295+
("observation__branch__name", "branch_name"),
296296
("observation__title", "observation_title"),
297297
("severity", "severity"),
298298
("status", "status"),

backend/application/core/api/serializers_observation.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -537,10 +537,8 @@ class Meta:
537537

538538

539539
class ObservationLogListSerializer(ModelSerializer):
540-
observation_title = SerializerMethodField()
541-
product_name = SerializerMethodField()
540+
observation_data = ObservationListSerializer(source="observation")
542541
branch_name = SerializerMethodField()
543-
origin_component_name_version = SerializerMethodField()
544542
user_full_name = SerializerMethodField()
545543
approval_user_full_name = SerializerMethodField()
546544

@@ -550,24 +548,15 @@ def get_user_full_name(self, obj: Observation_Log) -> Optional[str]:
550548

551549
return None
552550

553-
def get_observation_title(self, obj: Observation_Log) -> str:
554-
return obj.observation.title
555-
556551
def get_approval_user_full_name(self, obj: Observation_Log) -> Optional[str]:
557552
if obj.approval_user:
558553
return obj.approval_user.full_name
559554

560555
return None
561556

562-
def get_product_name(self, obj: Observation_Log) -> str:
563-
return obj.observation.product.name
564-
565557
def get_branch_name(self, obj: Observation_Log) -> str:
566558
return get_branch_name(obj.observation)
567559

568-
def get_origin_component_name_version(self, obj: Observation_Log) -> str:
569-
return get_origin_component_name_version(obj.observation)
570-
571560
class Meta:
572561
model = Observation_Log
573562
fields = "__all__"
@@ -586,7 +575,7 @@ class ObservationLogBulkApprovalSerializer(Serializer):
586575
)
587576
approval_remark = CharField(max_length=255, required=True)
588577
observation_logs = ListField(
589-
child=IntegerField(min_value=1), min_length=0, max_length=10000, required=True
578+
child=IntegerField(min_value=1), min_length=0, max_length=100, required=True
590579
)
591580

592581

backend/application/core/services/observations_bulk_actions.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@
33

44
from django.db.models.query import QuerySet
55
from django.utils import timezone
6-
from rest_framework.exceptions import NotFound, ValidationError
6+
from rest_framework.exceptions import ValidationError
77

8-
from application.access_control.services.authorization import (
9-
user_has_permission,
10-
user_has_permission_or_403,
11-
)
8+
from application.access_control.services.authorization import user_has_permission
129
from application.access_control.services.roles_permissions import Permissions
1310
from application.commons.services.global_request import get_current_user
14-
from application.core.models import Observation, Potential_Duplicate, Product
15-
from application.core.queries.observation import (
16-
get_current_observation_log,
17-
get_observation_log_by_id,
11+
from application.core.models import (
12+
Observation,
13+
Observation_Log,
14+
Potential_Duplicate,
15+
Product,
1816
)
17+
from application.core.queries.observation import get_current_observation_log
1918
from application.core.services.assessment import assessment_approval, save_assessment
2019
from application.core.services.potential_duplicates import (
2120
set_potential_duplicate,
@@ -150,17 +149,44 @@ def _check_observations(
150149
def observation_logs_bulk_approval(
151150
assessment_status: str,
152151
approval_remark: str,
153-
observation_logs: list[int],
152+
observation_log_ids: list[int],
154153
) -> None:
155-
for observation_log_id in observation_logs:
156-
observation_log = get_observation_log_by_id(observation_log_id)
157-
if not observation_log:
158-
raise NotFound(f"Observation Log {observation_log_id} not found")
154+
observation_logs = _check_observation_logs(None, observation_log_ids)
155+
for observation_log in observation_logs:
156+
assessment_approval(observation_log, assessment_status, approval_remark)
157+
set_potential_duplicate_both_ways(observation_log.observation)
159158

160-
user_has_permission_or_403(
161-
observation_log, Permissions.Observation_Log_Approval
162-
)
163159

164-
assessment_approval(observation_log, assessment_status, approval_remark)
160+
def _check_observation_logs(
161+
product: Optional[Product], observation_log_ids: list[int]
162+
) -> QuerySet[Observation_Log]:
163+
observation_logs = Observation_Log.objects.filter(id__in=observation_log_ids)
164+
if len(observation_logs) != len(observation_log_ids):
165+
raise ValidationError("Some observation logs do not exist")
165166

166-
set_potential_duplicate_both_ways(observation_log.observation)
167+
for observation_log in observation_logs:
168+
if product:
169+
if observation_log.observation.product != product:
170+
raise ValidationError(
171+
f"Observation log {observation_log.pk} does not belong to product {product.pk}"
172+
)
173+
else:
174+
if not user_has_permission(
175+
observation_log, Permissions.Observation_Log_Approval
176+
):
177+
raise ValidationError(
178+
f"First observation log without approval permission: {observation_log.pk}"
179+
)
180+
if (
181+
not observation_log.assessment_status
182+
== Assessment_Status.ASSESSMENT_STATUS_NEEDS_APPROVAL
183+
):
184+
raise ValidationError(
185+
f"First observation log that does not need approval: {observation_log.pk}"
186+
)
187+
if get_current_user() == observation_log.user:
188+
raise ValidationError(
189+
f"First observation log where user cannot approve their own assessment: {observation_log.pk}"
190+
)
191+
192+
return observation_logs
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {
2+
AutocompleteInput,
3+
Datagrid,
4+
DateField,
5+
FilterForm,
6+
FunctionField,
7+
ListContextProvider,
8+
ReferenceInput,
9+
ResourceContextProvider,
10+
TextField,
11+
TextInput,
12+
useListController,
13+
} from "react-admin";
14+
15+
import { CustomPagination } from "../../commons/custom_fields/CustomPagination";
16+
import { feature_vex_enabled } from "../../commons/functions";
17+
import { AutocompleteInputMedium, AutocompleteInputWide } from "../../commons/layout/themes";
18+
import { getSettingListSize } from "../../commons/user_settings/functions";
19+
import { ASSESSMENT_STATUS_NEEDS_APPROVAL } from "../types";
20+
import { OBSERVATION_SEVERITY_CHOICES, OBSERVATION_STATUS_CHOICES } from "../types";
21+
import { commentShortened } from "./functions";
22+
23+
function listFilters(product: any) {
24+
const filters = [];
25+
if (product && product.has_branches) {
26+
filters.push(
27+
<ReferenceInput
28+
source="branch"
29+
reference="branches"
30+
queryOptions={{ meta: { api_resource: "branch_names" } }}
31+
sort={{ field: "name", order: "ASC" }}
32+
filter={{ product: product.id }}
33+
alwaysOn
34+
>
35+
<AutocompleteInputMedium optionText="name" label="Branch / Version" />
36+
</ReferenceInput>
37+
);
38+
}
39+
filters.push(<TextInput source="observation_title" label="Observation title" alwaysOn />);
40+
41+
if (product && product.has_component) {
42+
filters.push(<TextInput source="origin_component_name_version" label="Component" alwaysOn />);
43+
}
44+
if (product && product.has_docker_image) {
45+
filters.push(<TextInput source="origin_docker_image_name_tag_short" label="Container" alwaysOn />);
46+
}
47+
if (product && product.has_endpoint) {
48+
filters.push(<TextInput source="origin_endpoint_hostname" label="Host" alwaysOn />);
49+
}
50+
if (product && product.has_source) {
51+
filters.push(<TextInput source="origin_source_file" label="Source" alwaysOn />);
52+
}
53+
if (product && product.has_cloud_resource) {
54+
filters.push(<TextInput source="origin_cloud_qualified_resource" label="Cloud resource" alwaysOn />);
55+
}
56+
if (product && product.has_kubernetes_resource) {
57+
filters.push(<TextInput source="origin_kubernetes_qualified_resource" label="Kubernetes resource" alwaysOn />);
58+
}
59+
60+
filters.push(
61+
<ReferenceInput source="user" reference="users" sort={{ field: "full_name", order: "ASC" }} alwaysOn>
62+
<AutocompleteInputMedium optionText="full_name" />
63+
</ReferenceInput>,
64+
<AutocompleteInput source="severity" label="Severity" choices={OBSERVATION_SEVERITY_CHOICES} alwaysOn />,
65+
<AutocompleteInput source="status" label="Status" choices={OBSERVATION_STATUS_CHOICES} alwaysOn />
66+
);
67+
return filters;
68+
}
69+
70+
type ObservationLogApprovalEmbeddedListProps = {
71+
product: any;
72+
};
73+
74+
const ObservationLogApprovalEmbeddedList = ({ product }: ObservationLogApprovalEmbeddedListProps) => {
75+
const listContext = useListController({
76+
filter: { product: Number(product.id), assessment_status: ASSESSMENT_STATUS_NEEDS_APPROVAL },
77+
perPage: 25,
78+
resource: "observation_logs",
79+
sort: { field: "created", order: "ASC" },
80+
disableSyncWithLocation: true,
81+
storeKey: "observation_logs.approvalembedded",
82+
});
83+
84+
if (listContext.isLoading) {
85+
return <div>Loading...</div>;
86+
}
87+
88+
const ShowObservationLogs = (id: any) => {
89+
return "../../../../observation_logs/" + id + "/show";
90+
};
91+
92+
localStorage.setItem("observationlogapprovalembeddedlist", "true");
93+
localStorage.removeItem("observationlogapprovallist");
94+
localStorage.removeItem("observationlogembeddedlist");
95+
96+
return (
97+
<ResourceContextProvider value="observation_logs">
98+
<ListContextProvider value={listContext}>
99+
<div style={{ width: "100%" }}>
100+
<FilterForm filters={listFilters(product)} />
101+
<Datagrid
102+
size={getSettingListSize()}
103+
sx={{ width: "100%" }}
104+
bulkActionButtons={false}
105+
rowClick={ShowObservationLogs}
106+
resource="observation_logs"
107+
>
108+
{product && product.has_branches && <TextField source="branch_name" label="Branch / Version" />}
109+
<TextField source="observation_data.title" label="Observation" />
110+
{product && product.has_component && (
111+
<TextField
112+
source="observation_data.origin_component_name_version"
113+
label="Component"
114+
sx={{ wordBreak: "break-word" }}
115+
/>
116+
)}
117+
{product && product.has_docker_image && (
118+
<TextField
119+
source="observation_data.origin_docker_image_name_tag_short"
120+
label="Container"
121+
sx={{ wordBreak: "break-word" }}
122+
/>
123+
)}
124+
{product && product.has_endpoint && (
125+
<TextField
126+
source="observation_data.origin_endpoint_hostname"
127+
label="Host"
128+
sx={{ wordBreak: "break-word" }}
129+
/>
130+
)}
131+
{product && product.has_source && (
132+
<TextField
133+
source="observation_data.origin_source_file"
134+
label="Source"
135+
sx={{ wordBreak: "break-word" }}
136+
/>
137+
)}
138+
{product && product.has_cloud_resource && (
139+
<TextField
140+
source="observation_data.origin_cloud_qualified_resource"
141+
label="Cloud resource"
142+
sx={{ wordBreak: "break-word" }}
143+
/>
144+
)}
145+
{product && product.has_kubernetes_resource && (
146+
<TextField
147+
source="observation_data.origin_kubernetes_qualified_resource"
148+
label="Kubernetes resource"
149+
sx={{ wordBreak: "break-word" }}
150+
/>
151+
)}
152+
<TextField source="user_full_name" label="User" />
153+
<TextField source="severity" emptyText="---" />
154+
<TextField source="status" emptyText="---" />
155+
{feature_vex_enabled() && (
156+
<TextField
157+
label="VEX justification"
158+
source="vex_justification"
159+
emptyText="---"
160+
sx={{ wordBreak: "break-word" }}
161+
/>
162+
)}
163+
<FunctionField
164+
label="Comment"
165+
render={(record) => commentShortened(record.comment)}
166+
sortable={false}
167+
sx={{ wordBreak: "break-word" }}
168+
/>
169+
<DateField source="created" showTime />
170+
</Datagrid>
171+
<CustomPagination />
172+
</div>
173+
</ListContextProvider>
174+
</ResourceContextProvider>
175+
);
176+
};
177+
178+
export default ObservationLogApprovalEmbeddedList;

0 commit comments

Comments
 (0)