Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend implementation for Alert system #361

Merged
merged 12 commits into from
Dec 9, 2024
Merged
144 changes: 113 additions & 31 deletions backend/composer/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,29 +515,89 @@ class Meta:
fields = ('id', 'name', 'uri')

class StatementAlertSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)

connectivity_statement = serializers.PrimaryKeyRelatedField(
queryset=ConnectivityStatement.objects.all(),
required=True
queryset=ConnectivityStatement.objects.all(), required=True
)
alert_type = serializers.PrimaryKeyRelatedField(queryset=AlertType.objects.all(), required=True)
id = serializers.IntegerField(required=False)
alert_type = serializers.PrimaryKeyRelatedField(
queryset=AlertType.objects.all(), required=True
)

class Meta:
model = StatementAlert
fields = ('id', 'alert_type', 'text', 'saved_by', 'created_at', 'updated_at', 'connectivity_statement')
read_only_fields = ('created_at', 'updated_at', 'saved_by', 'alert_type', 'connectivity_statement')

fields = (
"id",
"alert_type",
"text",
"saved_by",
"created_at",
"updated_at",
"connectivity_statement",
)
read_only_fields = ("created_at", "updated_at", "saved_by")
validators = []

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# If 'connectivity_statement' is provided in context, make it not required
if 'connectivity_statement' in self.context:
self.fields['connectivity_statement'].required = False

# If updating an instance, set 'alert_type' and 'connectivity_statement' as read-only
if self.instance:
self.fields['alert_type'].read_only = True
self.fields['connectivity_statement'].read_only = True

def validate(self, data):
# Get 'connectivity_statement' from context or instance
connectivity_statement = self.context.get('connectivity_statement') or data.get('connectivity_statement')
if not connectivity_statement and self.instance:
connectivity_statement = self.instance.connectivity_statement
if not connectivity_statement:
raise serializers.ValidationError({
'connectivity_statement': 'This field is required.'
})
data['connectivity_statement'] = connectivity_statement

# Get 'alert_type' from data or instance
alert_type = data.get('alert_type') or getattr(self.instance, 'alert_type', None)
if not alert_type:
raise serializers.ValidationError({
'alert_type': 'This field is required.'
})
data['alert_type'] = alert_type

alert_id = data.get('id', getattr(self.instance, 'id', None))

# Perform uniqueness check
existing_qs = StatementAlert.objects.filter(
connectivity_statement=connectivity_statement,
alert_type=alert_type
)
if alert_id:
existing_qs = existing_qs.exclude(id=alert_id)
if existing_qs.exists():
raise serializers.ValidationError({
"non_field_errors": "The fields connectivity_statement and alert_type must make a unique set."
})

return data

def create(self, validated_data):
request = self.context.get('request')
request = self.context.get("request")
user = request.user if request else None
validated_data['saved_by'] = user
validated_data["saved_by"] = user
return super().create(validated_data)

def update(self, instance, validated_data):
request = self.context.get('request')
request = self.context.get("request")
user = request.user if request else None
validated_data['saved_by'] = user
validated_data["saved_by"] = user
return super().update(instance, validated_data)


class ConnectivityStatementSerializer(BaseConnectivityStatementSerializer):
"""Connectivity Statement"""

Expand All @@ -563,8 +623,9 @@ class ConnectivityStatementSerializer(BaseConnectivityStatementSerializer):
statement_preview = serializers.SerializerMethodField()
errors = serializers.SerializerMethodField()
graph_rendering_state = GraphStateSerializer(required=False, allow_null=True)
statement_alerts = StatementAlertSerializer(many=True, read_only=False, required=False)

statement_alerts = StatementAlertSerializer(
many=True, read_only=False, required=False
)

def get_available_transitions(self, instance) -> list[CSState]:
request = self.context.get("request", None)
Expand Down Expand Up @@ -694,14 +755,15 @@ class Meta(BaseConnectivityStatementSerializer.Meta):
"statement_preview",
"errors",
"graph_rendering_state",
"statement_alerts"
"statement_alerts",
)


class ConnectivityStatementUpdateSerializer(ConnectivityStatementSerializer):
origins = serializers.PrimaryKeyRelatedField(
many=True, queryset=AnatomicalEntity.objects.all()
)
statement_alerts = StatementAlertSerializer(many=True, required=False)

class Meta:
model = ConnectivityStatement
Expand Down Expand Up @@ -740,7 +802,7 @@ class Meta:
"graph_rendering_state",
"statement_alerts",
)
read_only_fields = ("state","owner", "owner_id")
read_only_fields = ("state", "owner", "owner_id")

def update(self, instance, validated_data):
validated_data.pop("owner", None)
Expand All @@ -766,22 +828,8 @@ def update(self, instance, validated_data):
instance.origins.set(origins)

# Handle statement alerts
alerts_data = validated_data.pop('statement_alerts', [])
for alert_data in alerts_data:
alert_id = alert_data.get('id')
if alert_id:
# Update existing alert
alert_instance = StatementAlert.objects.get(id=alert_id)
alert_instance.text = alert_data.get('text', alert_instance.text)
alert_instance.save()
else:
# Create new alert if id is not provided
StatementAlert.objects.create(
connectivity_statement=instance,
alert_type=alert_data['alert_type'],
text=alert_data.get('text', '')
)

alerts_data = validated_data.pop("statement_alerts", [])
self._update_statement_alerts(instance, alerts_data)

return super().update(instance, validated_data)

Expand All @@ -792,6 +840,40 @@ def to_representation(self, instance):
"""
return ConnectivityStatementSerializer(instance, context=self.context).data

def _update_statement_alerts(self, instance, alerts_data):
existing_alerts = {alert.id: alert for alert in instance.statement_alerts.all()}

for alert_data in alerts_data:
alert_id = alert_data.get("id")
if alert_id and alert_id in existing_alerts:
# Update existing alert
alert_instance = existing_alerts[alert_id]
# Remove 'alert_type' and 'connectivity_statement' from alert_data
alert_data.pop('alert_type', None)
alert_data.pop('connectivity_statement', None)
serializer = StatementAlertSerializer(
alert_instance,
data=alert_data,
context={
"request": self.context.get("request"),
"connectivity_statement": instance, # Pass the parent instance
},
)
serializer.is_valid(raise_exception=True)
serializer.save()
else:
# Create new alert
serializer = StatementAlertSerializer(
data=alert_data,
context={
"request": self.context.get("request"),
"connectivity_statement": instance, # Pass the parent instance
},
)
serializer.is_valid(raise_exception=True)
serializer.save()



class KnowledgeStatementSerializer(ConnectivityStatementSerializer):
"""Knowledge Statement"""
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Pages/StatementDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {formatDate, formatTime, StatementsLabels} from "../helpers/helpers";
import GroupedButtons from "../components/Widgets/CustomGroupedButtons";
import Divider from "@mui/material/Divider";
import NoteDetails from "../components/Widgets/NotesFomList";
import DistillationTab from "../components/DistillationTab";
import DistillationTab from "../components/DistillationTab/DistillationTab";
import {useSectionStyle} from "../styles/styles";
import {useTheme} from "@mui/system";
import IconButton from "@mui/material/IconButton";
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/components/DistillationTab/AlertMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from "react";
import { MenuItem, Typography, Button, Box } from "@mui/material";
import { CheckRounded } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add";
import { vars } from "../../theme/variables";

interface AlertMenuItemProps {
type: any;
isSelected: boolean;
isDisabled: boolean;
onAdd: (typeId: number) => void;
alertStatus: string;
}

const AlertMenuItem: React.FC<AlertMenuItemProps> = ({ type, isSelected, isDisabled, onAdd, alertStatus }) => {
return (
<MenuItem
key={type.id}
value={type.id}
sx={{
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
gap: ".5rem",
padding: "0.625rem 0.625rem 0.625rem 0.5rem",
cursor: alertStatus === 'displayed' ? "default" : "pointer",
"&:hover": {
backgroundColor: "transparent",
"& .add-button": {
opacity: 1,
visibility: "visible",
},
},

'& .MuiSvgIcon-root': {
color: vars.colorPrimary,
visibility: alertStatus === 'displayed' ? "visible" : "hidden",
}
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: '.5rem', flex: 1 }}>
<CheckRounded />
<Typography
sx={{
display: "-webkit-box",
WebkitLineClamp: "2",
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "normal",
wordBreak: "break-word",
color: vars.darkTextColor,
fontWeight: 500,
fontSize: "1rem",
}}
>
{type.name}
</Typography>
</Box>

{!isSelected && alertStatus !== 'displayed' && (
<Button
variant="contained"
startIcon={<AddIcon />}
size="small"
onClick={() => onAdd(type.id)}
disabled={isDisabled}
className="add-button"
sx={{
color: vars.darkBlue,
backgroundColor: vars.lightBlue,
padding: '0.5rem 0.875rem',
opacity: 0,
visibility: "hidden",

'& .MuiButton-startIcon': {
'& .MuiSvgIcon-root': {
color: vars.darkBlue,
visibility: 'visible'
}
},

'&:hover': {
backgroundColor: vars.badgeBg,
color: vars.primary800,
boxShadow: 'none',
'& .MuiButton-startIcon': {
'& .MuiSvgIcon-root': {
color: vars.primary800,
visibility: 'visible'
}
}
}
}}
>
Add
</Button>
)}
</MenuItem>
);
};

export default AlertMenuItem;
73 changes: 73 additions & 0 deletions frontend/src/components/DistillationTab/ConfiramtionDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Typography} from "@mui/material";
import React from "react";
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import Stack from "@mui/material/Stack";
import {vars} from "../../theme/variables";
import {DeleteIcon} from "../icons";
const ConfirmationDialog = ({
open, onConfirm, onCancel
}: {
open: boolean,
onConfirm: () => void,
onCancel: () => void,
}) => {
return (
<Dialog open={open} onClose={onCancel} PaperProps={{
sx: {
width: '25rem',
padding: '1.5rem'
}
}}>
<DialogTitle sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
p: 0,
mb: '1rem',

'& .MuiButtonBase-root': {
p: 0,
'& .MuiSvgIcon-root': {
color: vars.iconPrimaryColor
}
}
}}>
<DeleteIcon />
<IconButton onClick={onCancel}>
<CloseRoundedIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{
p: 0,
mb: '2rem'
}}>
<Stack spacing='.25rem' mb='1.25rem'>
<Typography variant='h4'>
Are you sure you want to delete this alert permanently?
</Typography>
<Typography variant='body2'>
By proceeding, this alert will be permanently deleted from the system. This action cannot be undone. Are you sure you want to continue?
</Typography>
</Stack>
</DialogContent>
<DialogActions sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
p: 0,
'& .MuiButtonBase-root': {
fontSize: '1rem'
}
}}>
<Button fullWidth onClick={onCancel} color="secondary" variant='outlined'>
Cancel
</Button>
<Button fullWidth onClick={onConfirm} color="primary" variant='contained'>
Proceed
</Button>
</DialogActions>
</Dialog>
)
}

export default ConfirmationDialog;
Loading