diff --git a/app/src/components/elements/CollapsePaper.js b/app/src/components/elements/CollapsePaper.js
new file mode 100644
index 00000000..71c1035e
--- /dev/null
+++ b/app/src/components/elements/CollapsePaper.js
@@ -0,0 +1,60 @@
+import {
+ KeyboardArrowDownRounded,
+ KeyboardArrowUpRounded,
+} from "@mui/icons-material";
+import { Badge, Box, Collapse, IconButton, Paper } from "@mui/material";
+import { useState } from "react";
+
+export function CollapsePaper({
+ title,
+ count,
+ children,
+ defaultExpanded = false,
+ sx,
+}) {
+ const [expanded, setExpanded] = useState(defaultExpanded);
+
+ return (
+
+ {
+ if (e.target === e.currentTarget) {
+ setExpanded(!expanded);
+ }
+ }}
+ >
+ setExpanded(!expanded)}
+ sx={{
+ alignItems: "center",
+ gap: "10px",
+ "& span": {
+ position: "relative",
+ transform: "scale(1)",
+ backgroundColor: "dimgray",
+ },
+ }}
+ >
+ {title}
+
+ setExpanded(!expanded)}
+ >
+ {expanded ? : }
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/src/components/elements/ColorSlider.js b/app/src/components/elements/ColorSlider.js
index 36cf3ec6..eaf2c9c0 100644
--- a/app/src/components/elements/ColorSlider.js
+++ b/app/src/components/elements/ColorSlider.js
@@ -25,7 +25,12 @@ const baseMarks = [
},
];
-export function ColorSlider({ marks, defaultValue, onChange }) {
+export function ColorSlider({
+ marks,
+ defaultValue,
+ onChange,
+ hideDescription,
+}) {
const joinedMarks = marks.map((m, i) => ({
...(baseMarks.length > i ? baseMarks[i] : {}),
...m,
@@ -43,7 +48,8 @@ export function ColorSlider({ marks, defaultValue, onChange }) {
marks={joinedMarks}
min={0}
max={4}
- valueLabelDisplay="off"
+ // valueLabelFormat={(v, i) => joinedMarks[i].label}
+ // valueLabelDisplay="on"
onChange={(e) => {
setSelectedMark(
joinedMarks.find((m) => m.value === e.target.value)
@@ -53,7 +59,7 @@ export function ColorSlider({ marks, defaultValue, onChange }) {
sx={{ color: selectedMark?.color || "primary" }}
/>
- {selectedMark?.description && (
+ {!hideDescription && selectedMark?.description && (
theme.palette.review.text,
+ }}
+ variant="outlined"
+ label={user.name}
+ icon={
+ <>
+ {user?.slackUrl && (
+
+
+
+ )}
+ {user?.mail && (
+
+
+
+ )}
+ >
+ }
+ />
+ );
+}
diff --git a/app/src/components/elements/modal/ModalManager.js b/app/src/components/elements/modal/ModalManager.js
index 1a2d1bae..38db7db6 100644
--- a/app/src/components/elements/modal/ModalManager.js
+++ b/app/src/components/elements/modal/ModalManager.js
@@ -10,7 +10,6 @@ import { ChangeReviewer } from "../../model/modals/ChangeReviewer";
import { Tutorial } from "../../model/tutorial/Tutorial";
import { CancelReview } from "../../reviews/modals/CancelReview";
import { DeclineReview } from "../../reviews/modals/DeclineReview";
-import { ViewActionItems } from "../../model/modals/ViewActionItems";
export const MODALS = {
ChangeReviewer,
@@ -23,7 +22,6 @@ export const MODALS = {
DeleteSelected,
DeclineReview,
CancelReview,
- ViewActionItems,
};
export function ModalManager() {
diff --git a/app/src/components/model/hooks/useActionItems.js b/app/src/components/model/hooks/useActionItems.js
new file mode 100644
index 00000000..a8314bb7
--- /dev/null
+++ b/app/src/components/model/hooks/useActionItems.js
@@ -0,0 +1,22 @@
+import { useListThreatsQuery } from "../../../api/gram/threats";
+import { useModelID } from "./useModelID";
+
+export function useActionItems() {
+ const modelId = useModelID();
+ const { data: threats } = useListThreatsQuery({ modelId });
+
+ const actionItems = threats?.threats
+ ? Object.keys(threats?.threats)
+ .map((componentId) => ({
+ componentId,
+ threats: threats?.threats[componentId].filter(
+ (th) => th.isActionItem
+ ),
+ }))
+ .filter(({ threats }) => threats && threats.length > 0)
+ : [];
+
+ console.log(actionItems);
+
+ return actionItems;
+}
diff --git a/app/src/components/model/modals/ApproveReview.js b/app/src/components/model/modals/ApproveReview.js
index 20f99b14..2da5becc 100644
--- a/app/src/components/model/modals/ApproveReview.js
+++ b/app/src/components/model/modals/ApproveReview.js
@@ -20,89 +20,11 @@ import {
useGetReviewQuery,
} from "../../../api/gram/review";
import { modalActions } from "../../../redux/modalSlice";
-import { ColorSlider } from "../../elements/ColorSlider";
import { LoadingPage } from "../../elements/loading/loading-page/LoadingPage";
import { PERMISSIONS } from "../constants";
-import { ActionItemList } from "./ActionItemList";
-
-function LikelihoodSlider({ onChange }) {
- const marks = [
- {
- label: "Rare",
- description: `➢ This will probably never happen/recur
-➢ Every 25 years`,
- },
- {
- label: "Unlikely",
- description: `➢ This is not likely to happen/recur but could
-➢ Every 10 years`,
- },
- {
- label: "Occasional",
- description: `➢ This is unexpected to happen/recur but is certainly possible to occur
-➢ Every 5 years`,
- },
- {
- label: "Likely",
- description: `➢ This will probably happen/recur but is not a persisting issue.
-➢ Every 3 years`,
- },
- {
- label: "Almost certain",
- description: `➢ This will undoubtedly happen/recur
-➢ Every year`,
- },
- ];
-
- return (
- <>
- onChange(marks[e.target.value])}
- />
- >
- );
-}
-
-function ImpactSlider({ onChange }) {
- const marks = [
- {
- label: "Very low",
- description: `➢ Users can not interact with the service <1h
-➢ No regulatory sanctions/fines`,
- },
- {
- label: "Low",
- description: `➢ Users can not interact with the service <1-4h
-➢ Incident reviewed by authorities but dismissed`,
- },
- {
- label: "Medium",
- description: `➢ Users can not interact with the service <4-10h
-➢ Incident reviewed by authorities and regulatory warning`,
- },
- {
- label: "High",
- description: `➢ Users can not interact with the service <10-16h
-➢ Incident reviewed by authorities and sanctions/fines imposed`,
- },
- {
- label: "Very high",
- description: `➢ Users can not interact with the service >16h
-➢ Incident reviewed by authorities and sanctions/fines threaten operations / Loss of licence`,
- },
- ];
-
- return (
- onChange(marks[e.target.value])}
- />
- );
-}
+import { ActionItemList } from "../panels/left/ActionItemList";
+import { ImpactSlider } from "./ImpactSlider";
+import { LikelihoodSlider } from "./LikelihoodSlider";
export function ApproveReview({ modelId }) {
const dispatch = useDispatch();
@@ -145,40 +67,8 @@ export function ApproveReview({ modelId }) {
{(isUninitialized || isLoading) && (
<>
- Risk Evaluation
- {/*
- Every system threat model is connected to a risk ticket. When you
- approve this threat model, it will be automatically created for
- you (if the model is connected to a system).
- */}
- {/*
*/}
-
- Based on the threat model, set the risk value as your estimate of
- the overall risk of all threats/controls found in the threat
- model.
-
-
-
-
- Impact
-
- setExtras({ ...extras, impact: value.label })
- }
- />
-
-
-
- Likelihood
-
- setExtras({ ...extras, likelihood: value.label })
- }
- />
-
-
Action Items
-
+
Summary
diff --git a/app/src/components/model/modals/ImpactSlider.js b/app/src/components/model/modals/ImpactSlider.js
new file mode 100644
index 00000000..736a4fe4
--- /dev/null
+++ b/app/src/components/model/modals/ImpactSlider.js
@@ -0,0 +1,41 @@
+import { ColorSlider } from "../../elements/ColorSlider";
+
+export function ImpactSlider({ onChange, ...props }) {
+ const marks = [
+ {
+ label: "Very low",
+ description: `➢ Users can not interact with the service <1h
+➢ No regulatory sanctions/fines`,
+ },
+ {
+ label: "Low",
+ description: `➢ Users can not interact with the service <1-4h
+➢ Incident reviewed by authorities but dismissed`,
+ },
+ {
+ label: "Medium",
+ description: `➢ Users can not interact with the service <4-10h
+➢ Incident reviewed by authorities and regulatory warning`,
+ },
+ {
+ label: "High",
+ description: `➢ Users can not interact with the service <10-16h
+➢ Incident reviewed by authorities and sanctions/fines imposed`,
+ },
+ {
+ label: "Very high",
+ description: `➢ Users can not interact with the service >16h
+➢ Incident reviewed by authorities and sanctions/fines threaten operations / Loss of licence`,
+ },
+ ];
+
+ return (
+ onChange(marks[e.target.value])}
+ {...props}
+ />
+ );
+}
diff --git a/app/src/components/model/modals/LikelihoodSlider.js b/app/src/components/model/modals/LikelihoodSlider.js
new file mode 100644
index 00000000..6d823f40
--- /dev/null
+++ b/app/src/components/model/modals/LikelihoodSlider.js
@@ -0,0 +1,40 @@
+import { ColorSlider } from "../../elements/ColorSlider";
+
+export function LikelihoodSlider({ onChange, ...props }) {
+ const marks = [
+ {
+ label: "Rare",
+ description: `➢ This will probably never happen/recur
+➢ Every 25 years`,
+ },
+ {
+ label: "Unlikely",
+ description: `➢ This is not likely to happen/recur but could
+➢ Every 10 years`,
+ },
+ {
+ label: "Occasional",
+ description: `➢ This is unexpected to happen/recur but is certainly possible to occur
+➢ Every 5 years`,
+ },
+ {
+ label: "Likely",
+ description: `➢ This will probably happen/recur but is not a persisting issue.
+➢ Every 3 years`,
+ },
+ {
+ label: "Almost certain",
+ description: `➢ This will undoubtedly happen/recur
+➢ Every year`,
+ },
+ ];
+
+ return (
+ onChange(marks[e.target.value])}
+ {...props}
+ />
+ );
+}
diff --git a/app/src/components/model/modals/ViewActionItems.js b/app/src/components/model/modals/ViewActionItems.js
deleted file mode 100644
index eb592bec..00000000
--- a/app/src/components/model/modals/ViewActionItems.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { AssignmentTurnedIn as AssignmentTurnedInIcon } from "@mui/icons-material";
-import {
- Box,
- Button,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- Typography,
-} from "@mui/material";
-import { useDispatch } from "react-redux";
-import { modalActions } from "../../../redux/modalSlice";
-import { ActionItemList } from "./ActionItemList";
-
-export function ViewActionItems({ modelId }) {
- const dispatch = useDispatch();
-
- return (
-
- );
-}
diff --git a/app/src/components/model/modals/ActionItemList.js b/app/src/components/model/panels/left/ActionItemList.js
similarity index 50%
rename from app/src/components/model/modals/ActionItemList.js
rename to app/src/components/model/panels/left/ActionItemList.js
index b2794f83..64999e83 100644
--- a/app/src/components/model/modals/ActionItemList.js
+++ b/app/src/components/model/panels/left/ActionItemList.js
@@ -1,37 +1,50 @@
import AssignmentTurnedInIcon from "@mui/icons-material/AssignmentTurnedIn";
-import { Box, DialogContentText, Typography } from "@mui/material";
-import { useListThreatsQuery } from "../../../api/gram/threats";
-import { useComponent } from "../hooks/useComponent";
-import { Threat } from "../panels/right/Threat";
-import { useModelID } from "../hooks/useModelID";
+import {
+ Badge,
+ Box,
+ Collapse,
+ DialogContentText,
+ IconButton,
+ Paper,
+ Stack,
+ Typography,
+} from "@mui/material";
+import { useActionItems } from "../../hooks/useActionItems";
+import { useComponent } from "../../hooks/useComponent";
+import { Threat } from "../right/Threat";
+import { useState } from "react";
+import {
+ KeyboardArrowDownRounded,
+ KeyboardArrowUpRounded,
+} from "@mui/icons-material";
+import { CollapsePaper } from "../../../elements/CollapsePaper";
-function ComponentActionItem({ componentId, threats }) {
+function ComponentActionItem({
+ componentId,
+ threats,
+ defaultExpanded = false,
+}) {
const component = useComponent(componentId);
return (
- {component.name}
- {threats.map((th) => (
-
- ))}
+
+
+ {threats.map((th, i) => (
+
+ ))}
+
+
);
}
-export function ActionItemList() {
- const modelId = useModelID();
- const { data: threats } = useListThreatsQuery({ modelId });
-
- const actionItems = threats?.threats
- ? Object.keys(threats?.threats)
- .map((componentId) => ({
- componentId,
- threats: threats?.threats[componentId].filter(
- (th) => th.isActionItem
- ),
- }))
- .filter(({ threats }) => threats && threats.length > 0)
- : [];
+export function ActionItemList({ automaticallyExpanded = false }) {
+ const actionItems = useActionItems();
return (
<>
@@ -42,7 +55,11 @@ export function ActionItemList() {
{actionItems.map(({ componentId, threats }) => (
-
+
))}
>
)}
diff --git a/app/src/components/model/panels/left/ActionItemTab.js b/app/src/components/model/panels/left/ActionItemTab.js
new file mode 100644
index 00000000..f291cd10
--- /dev/null
+++ b/app/src/components/model/panels/left/ActionItemTab.js
@@ -0,0 +1,15 @@
+import { Box } from "@mui/material";
+import { ActionItemList } from "./ActionItemList";
+
+export function ActionItemTab() {
+ return (
+
+
+
+ );
+}
diff --git a/app/src/components/model/panels/left/Footer.js b/app/src/components/model/panels/left/Footer.js
index 94d036c5..f988c664 100644
--- a/app/src/components/model/panels/left/Footer.js
+++ b/app/src/components/model/panels/left/Footer.js
@@ -2,6 +2,7 @@ import {
DeleteRounded as DeleteRoundedIcon,
HelpRounded as HelpRoundedIcon,
} from "@mui/icons-material";
+// import IosShareIcon from "@mui/icons-material/IosShare";
import { Box, IconButton, Paper, Tooltip, tooltipClasses } from "@mui/material";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
@@ -12,7 +13,7 @@ import { MODALS } from "../../../elements/modal/ModalManager";
import { PERMISSIONS } from "../../constants";
import { useModelID } from "../../hooks/useModelID";
-export function Footer() {
+export function LeftFooter() {
const dispatch = useDispatch();
const emptyDiagram = useSelector(
@@ -51,6 +52,11 @@ export function Footer() {
+ {/*
+
+
+
+ */}
+
+
+
{component && (
)}
-
+
)}
diff --git a/app/src/components/model/panels/left/LeftTabsHeader.js b/app/src/components/model/panels/left/LeftTabsHeader.js
index b78bb128..983aee15 100644
--- a/app/src/components/model/panels/left/LeftTabsHeader.js
+++ b/app/src/components/model/panels/left/LeftTabsHeader.js
@@ -11,7 +11,7 @@ export function LeftTabsHeader(props) {
setTab(v)}
textColor="inherit"
variant="fullWidth"
@@ -21,8 +21,11 @@ export function LeftTabsHeader(props) {
},
}}
>
-
- {component && }
+
+
+ {component && (
+
+ )}
diff --git a/app/src/components/model/panels/left/Review.js b/app/src/components/model/panels/left/Review.js
index dcc9c3ac..55a23764 100644
--- a/app/src/components/model/panels/left/Review.js
+++ b/app/src/components/model/panels/left/Review.js
@@ -1,20 +1,17 @@
import {
+ Cancel as CancelIcon,
DescriptionRounded as DescriptionRoundedIcon,
+ LockRounded as LockClosedRounded,
+ LockOpenRounded,
ThumbUpRounded as ThumbUpRoundedIcon,
TodayRounded as TodayRoundedIcon,
VisibilityRounded as VisibilityRoundedIcon,
- LockOpenRounded,
- Cancel as CancelIcon,
- LockRounded as LockClosedRounded,
- AssignmentTurnedIn,
} from "@mui/icons-material";
import {
Box,
Button,
Card,
CardContent,
- Chip,
- IconButton,
Skeleton,
Tooltip,
Typography,
@@ -26,11 +23,10 @@ import { useGetModelPermissionsQuery } from "../../../../api/gram/model";
import { useGetReviewQuery } from "../../../../api/gram/review";
import { useReviewExpiration } from "../../../../hooks/useReviewExpiration";
import { modalActions } from "../../../../redux/modalSlice";
+import { UserChip } from "../../../elements/UserChip";
import { MODALS } from "../../../elements/modal/ModalManager";
import { PERMISSIONS } from "../../constants";
import { useModelID } from "../../hooks/useModelID";
-import EmailIcon from "@mui/icons-material/Email";
-import ChatIcon from "@mui/icons-material/Chat";
const ReviewContent = (review) => {
const { hasExpired, validUntil, aboutToExpire } = useReviewExpiration(
@@ -50,7 +46,6 @@ const ReviewContent = (review) => {
description: "",
buttons: [
EditNoteButton,
- ViewActionItemsButton,
ApproveButton,
RequestMeetingButton,
ReassignReviewButton,
@@ -68,7 +63,7 @@ const ReviewContent = (review) => {
description: `Approved by ${review?.reviewer?.name} on ${new Date(
review?.approved_at
).toLocaleDateString()} and valid until ${validUntil.toLocaleDateString()}. To update this model, create a new model based on this one and have it reviewed again.`,
- buttons: [EditNoteButton, ViewActionItemsButton],
+ buttons: [EditNoteButton],
components: [],
color: hasExpired ? "error" : aboutToExpire ? "warning" : "success",
},
@@ -86,7 +81,6 @@ const ReviewContent = (review) => {
: "",
buttons: [
EditNoteButton,
- ViewActionItemsButton,
ApproveButton,
BookMeetingButton,
ReassignReviewButton,
@@ -155,29 +149,6 @@ function CancelReviewButton({ permissions, modelId }) {
);
}
-function ViewActionItemsButton({ modelId }) {
- const dispatch = useDispatch();
-
- return (
- }
- color="inherit"
- variant="outlined"
- sx={{ fontSize: "12px", padding: "2px 10px 2px 10px" }}
- onClick={() =>
- dispatch(
- modalActions.open({
- type: MODALS.ViewActionItems.name,
- props: { modelId },
- })
- )
- }
- >
- Action Items
-
- );
-}
-
function RequestReviewButton({ permissions, modelId }) {
const dispatch = useDispatch();
@@ -356,45 +327,6 @@ function ReviewReviewedBy(props) {
);
}
-function UserChip({ user }) {
- return (
- theme.palette.review.text,
- }}
- variant="outlined"
- label={user.name}
- icon={
- <>
- {user?.slackUrl && (
-
-
-
- )}
- {user?.mail && (
-
-
-
- )}
- >
- }
- />
- );
-}
-
export function Review() {
const modelId = useModelID();
diff --git a/app/src/components/model/panels/left/constants.js b/app/src/components/model/panels/left/constants.js
index 70e5c10d..50b0193b 100644
--- a/app/src/components/model/panels/left/constants.js
+++ b/app/src/components/model/panels/left/constants.js
@@ -1,4 +1,5 @@
export const TAB = {
SYSTEM: 0,
- COMPONENT: 1,
+ ACTION_ITEMS: 1,
+ COMPONENT: 2,
};
diff --git a/app/src/components/model/panels/right/SeveritySlider.js b/app/src/components/model/panels/right/SeveritySlider.js
new file mode 100644
index 00000000..080fe3a8
--- /dev/null
+++ b/app/src/components/model/panels/right/SeveritySlider.js
@@ -0,0 +1,31 @@
+import { ColorSlider } from "../../../elements/ColorSlider";
+
+export function SeveritySlider({ onChange }) {
+ const marks = [
+ {
+ label: "Not defined",
+ description: ``,
+ },
+ {
+ label: "Low",
+ description: ``,
+ },
+ {
+ label: "Medium",
+ description: ``,
+ },
+ {
+ label: "High",
+ description: ``,
+ },
+ ];
+
+ return (
+ onChange(marks[e.target.value])}
+ />
+ );
+}
diff --git a/app/src/components/model/panels/right/Threat.js b/app/src/components/model/panels/right/Threat.js
index 9b99fc96..dc796afa 100644
--- a/app/src/components/model/panels/right/Threat.js
+++ b/app/src/components/model/panels/right/Threat.js
@@ -3,7 +3,16 @@ import {
ClearRounded as ClearRoundedIcon,
} from "@mui/icons-material";
import AssignmentTurnedInIcon from "@mui/icons-material/AssignmentTurnedIn";
-import { Box, Card, CardContent, IconButton, Tooltip } from "@mui/material";
+import {
+ Box,
+ Card,
+ CardContent,
+ IconButton,
+ Paper,
+ Stack,
+ Tooltip,
+ Typography,
+} from "@mui/material";
import { useEffect, useState } from "react";
import { useCreateControlMutation } from "../../../../api/gram/controls";
import {
@@ -26,6 +35,10 @@ import {
useListSuggestionsQuery,
} from "../../../../api/gram/suggestions";
import { useSelectedComponent } from "../../hooks/useSelectedComponent";
+import { ImpactSlider } from "../../modals/ImpactSlider";
+import { LikelihoodSlider } from "../../modals/LikelihoodSlider";
+import { SeveritySlider } from "./SeveritySlider";
+import { CollapsePaper } from "../../../elements/CollapsePaper";
export function Threat({
threat,
@@ -68,6 +81,9 @@ export function Threat({
threatsMap[threat.id]?.includes(c.id)
);
+ const [impact, setImpact] = useState();
+ const [likelihood, setLikelihood] = useState();
+
//TODO clean this up, not the correct way to use useEffect imo
useEffect(() => {
if (title !== threat.title || description !== threat.description) {
@@ -261,6 +277,31 @@ export function Threat({
createNew={createControlWithMitigation}
/>
)}
+
+ {threat.isActionItem && (
+
+
+
+ Impact
+ setImpact(v)}
+ />
+
+
+ Likelihood
+ setLikelihood(v)}
+ />
+
+
+
+ )}
);