@@ -24,6 +24,7 @@ import { VendorPage } from "./vendors/VendorPage";
|
|||||||
import { MitigationNewPage } from "./mitigations/MitigationNewPage";
|
import { MitigationNewPage } from "./mitigations/MitigationNewPage";
|
||||||
import { RiskListPage } from "./risks/RiskListPage";
|
import { RiskListPage } from "./risks/RiskListPage";
|
||||||
import { NewRiskPage } from "./risks/NewRiskPage";
|
import { NewRiskPage } from "./risks/NewRiskPage";
|
||||||
|
import ShowRiskView from "./risks/ShowRiskView";
|
||||||
|
|
||||||
export function OrganizationsRoutes() {
|
export function OrganizationsRoutes() {
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +58,7 @@ export function OrganizationsRoutes() {
|
|||||||
/>
|
/>
|
||||||
<Route path="risks" element={<RiskListPage />} />
|
<Route path="risks" element={<RiskListPage />} />
|
||||||
<Route path="risks/new" element={<NewRiskPage />} />
|
<Route path="risks/new" element={<NewRiskPage />} />
|
||||||
|
<Route path="risks/:riskId" element={<ShowRiskView />} />
|
||||||
<Route path="settings" element={<SettingsPage />} />
|
<Route path="settings" element={<SettingsPage />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
ControlViewLinkedMitigationsQuery,
|
||||||
|
ControlViewLinkedMitigationsQuery$data,
|
||||||
|
} from "./__generated__/ControlViewLinkedMitigationsQuery.graphql";
|
||||||
|
import {
|
||||||
|
ControlViewOrganizationMitigationsQuery,
|
||||||
|
ControlViewOrganizationMitigationsQuery$data,
|
||||||
|
} from "./__generated__/ControlViewOrganizationMitigationsQuery.graphql";
|
||||||
|
|
||||||
const controlViewQuery = graphql`
|
const controlViewQuery = graphql`
|
||||||
query ControlViewQuery($controlId: ID!) {
|
query ControlViewQuery($controlId: ID!) {
|
||||||
@@ -115,38 +123,6 @@ const deleteMitigationMappingMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add type definitions for the GraphQL responses
|
|
||||||
interface MitigationNode {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
category: string;
|
|
||||||
importance: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
|
||||||
state: "NOT_STARTED" | "IN_PROGRESS" | "IMPLEMENTED" | "NOT_APPLICABLE";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LinkedMitigationsData {
|
|
||||||
control?: {
|
|
||||||
id: string;
|
|
||||||
mitigations?: {
|
|
||||||
edges: Array<{
|
|
||||||
node: MitigationNode;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrganizationMitigationsData {
|
|
||||||
organization?: {
|
|
||||||
id: string;
|
|
||||||
mitigations?: {
|
|
||||||
edges: Array<{
|
|
||||||
node: MitigationNode;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Control({
|
export function Control({
|
||||||
control,
|
control,
|
||||||
}: {
|
}: {
|
||||||
@@ -163,9 +139,9 @@ export function Control({
|
|||||||
const [isMitigationMappingDialogOpen, setIsMitigationMappingDialogOpen] =
|
const [isMitigationMappingDialogOpen, setIsMitigationMappingDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [linkedMitigationsData, setLinkedMitigationsData] =
|
const [linkedMitigationsData, setLinkedMitigationsData] =
|
||||||
useState<LinkedMitigationsData | null>(null);
|
useState<ControlViewLinkedMitigationsQuery$data | null>(null);
|
||||||
const [organizationMitigationsData, setOrganizationMitigationsData] =
|
const [organizationMitigationsData, setOrganizationMitigationsData] =
|
||||||
useState<OrganizationMitigationsData | null>(null);
|
useState<ControlViewOrganizationMitigationsQuery$data | null>(null);
|
||||||
const [mitigationSearchQuery, setMitigationSearchQuery] = useState("");
|
const [mitigationSearchQuery, setMitigationSearchQuery] = useState("");
|
||||||
const [isLoadingMitigations, setIsLoadingMitigations] = useState(false);
|
const [isLoadingMitigations, setIsLoadingMitigations] = useState(false);
|
||||||
const [isLinkingMitigation, setIsLinkingMitigation] = useState(false);
|
const [isLinkingMitigation, setIsLinkingMitigation] = useState(false);
|
||||||
@@ -184,11 +160,15 @@ export function Control({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (control.id) {
|
if (control.id) {
|
||||||
setIsLoadingMitigations(true);
|
setIsLoadingMitigations(true);
|
||||||
fetchQuery(environment, linkedMitigationsQuery, {
|
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
||||||
controlId: control.id,
|
environment,
|
||||||
}).subscribe({
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
setLinkedMitigationsData(data as LinkedMitigationsData);
|
setLinkedMitigationsData(data);
|
||||||
setIsLoadingMitigations(false);
|
setIsLoadingMitigations(false);
|
||||||
},
|
},
|
||||||
error: (error: Error) => {
|
error: (error: Error) => {
|
||||||
@@ -206,19 +186,27 @@ export function Control({
|
|||||||
setIsLoadingMitigations(true);
|
setIsLoadingMitigations(true);
|
||||||
|
|
||||||
// Fetch all mitigations for the organization
|
// Fetch all mitigations for the organization
|
||||||
fetchQuery(environment, organizationMitigationsQuery, {
|
fetchQuery<ControlViewOrganizationMitigationsQuery>(
|
||||||
organizationId,
|
environment,
|
||||||
}).subscribe({
|
organizationMitigationsQuery,
|
||||||
|
{
|
||||||
|
organizationId,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
setOrganizationMitigationsData(data as OrganizationMitigationsData);
|
setOrganizationMitigationsData(data);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
// Fetch linked mitigations for this control
|
// Fetch linked mitigations for this control
|
||||||
fetchQuery(environment, linkedMitigationsQuery, {
|
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
||||||
controlId: control.id,
|
environment,
|
||||||
}).subscribe({
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
setLinkedMitigationsData(data as LinkedMitigationsData);
|
setLinkedMitigationsData(data);
|
||||||
setIsLoadingMitigations(false);
|
setIsLoadingMitigations(false);
|
||||||
},
|
},
|
||||||
error: (error: Error) => {
|
error: (error: Error) => {
|
||||||
@@ -337,11 +325,15 @@ export function Control({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refresh linked mitigations data
|
// Refresh linked mitigations data
|
||||||
fetchQuery(environment, linkedMitigationsQuery, {
|
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
||||||
controlId: control.id,
|
environment,
|
||||||
}).subscribe({
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
setLinkedMitigationsData(data as LinkedMitigationsData);
|
setLinkedMitigationsData(data);
|
||||||
},
|
},
|
||||||
error: (error: Error) => {
|
error: (error: Error) => {
|
||||||
console.error("Error refreshing linked mitigations:", error);
|
console.error("Error refreshing linked mitigations:", error);
|
||||||
@@ -394,11 +386,15 @@ export function Control({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refresh linked mitigations data
|
// Refresh linked mitigations data
|
||||||
fetchQuery(environment, linkedMitigationsQuery, {
|
fetchQuery<ControlViewLinkedMitigationsQuery>(
|
||||||
controlId: control.id,
|
environment,
|
||||||
}).subscribe({
|
linkedMitigationsQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
setLinkedMitigationsData(data as LinkedMitigationsData);
|
setLinkedMitigationsData(data);
|
||||||
},
|
},
|
||||||
error: (error: Error) => {
|
error: (error: Error) => {
|
||||||
console.error("Error refreshing linked mitigations:", error);
|
console.error("Error refreshing linked mitigations:", error);
|
||||||
@@ -589,85 +585,81 @@ export function Control({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredMitigations().map(
|
{filteredMitigations().map((mitigation) => {
|
||||||
(mitigation: MitigationNode) => {
|
const isLinked = isMitigationLinked(mitigation.id);
|
||||||
const isLinked = isMitigationLinked(
|
return (
|
||||||
mitigation.id
|
<tr
|
||||||
);
|
key={mitigation.id}
|
||||||
return (
|
className="border-b hover:bg-gray-50"
|
||||||
<tr
|
>
|
||||||
key={mitigation.id}
|
<td className="py-3 px-4">
|
||||||
className="border-b hover:bg-gray-50"
|
<div className="font-medium">
|
||||||
>
|
{mitigation.name}
|
||||||
<td className="py-3 px-4">
|
</div>
|
||||||
<div className="font-medium">
|
{mitigation.description && (
|
||||||
{mitigation.name}
|
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
||||||
|
{mitigation.description}
|
||||||
</div>
|
</div>
|
||||||
{mitigation.description && (
|
)}
|
||||||
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
</td>
|
||||||
{mitigation.description}
|
<td className="py-3 px-4">
|
||||||
</div>
|
<div
|
||||||
)}
|
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
||||||
</td>
|
mitigation.importance
|
||||||
<td className="py-3 px-4">
|
)} inline-block`}
|
||||||
<div
|
>
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
{formatImportance(mitigation.importance)}
|
||||||
mitigation.importance
|
</div>
|
||||||
)} inline-block`}
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
||||||
|
mitigation.state
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatState(mitigation.state)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||||
|
{isLinked ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingMitigation}
|
||||||
|
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
||||||
>
|
>
|
||||||
{formatImportance(mitigation.importance)}
|
{isUnlinkingMitigation ? (
|
||||||
</div>
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
</td>
|
) : (
|
||||||
<td className="py-3 px-4">
|
<X className="w-4 h-4" />
|
||||||
<div
|
)}
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
<span className="ml-1">Unlink</span>
|
||||||
mitigation.state
|
</Button>
|
||||||
)} inline-block`}
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleLinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isLinkingMitigation}
|
||||||
|
className="text-xs h-7 text-blue-500 border-blue-200 hover:bg-blue-50"
|
||||||
>
|
>
|
||||||
{formatState(mitigation.state)}
|
{isLinkingMitigation ? (
|
||||||
</div>
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
</td>
|
) : (
|
||||||
<td className="py-3 px-4 text-right whitespace-nowrap">
|
<LinkIcon className="w-4 h-4" />
|
||||||
{isLinked ? (
|
)}
|
||||||
<Button
|
<span className="ml-1">Link</span>
|
||||||
variant="outline"
|
</Button>
|
||||||
size="sm"
|
)}
|
||||||
onClick={() =>
|
</td>
|
||||||
handleUnlinkMitigation(mitigation.id)
|
</tr>
|
||||||
}
|
);
|
||||||
disabled={isUnlinkingMitigation}
|
})}
|
||||||
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
{isUnlinkingMitigation ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1">Unlink</span>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleLinkMitigation(mitigation.id)
|
|
||||||
}
|
|
||||||
disabled={isLinkingMitigation}
|
|
||||||
className="text-xs h-7 text-blue-500 border-blue-200 hover:bg-blue-50"
|
|
||||||
>
|
|
||||||
{isLinkingMitigation ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<LinkIcon className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1">Link</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
@@ -720,68 +712,66 @@ export function Control({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{getLinkedMitigations().map(
|
{getLinkedMitigations().map((mitigation) => (
|
||||||
(mitigation: MitigationNode) => (
|
<tr
|
||||||
<tr
|
key={mitigation.id}
|
||||||
key={mitigation.id}
|
className="border-b hover:bg-gray-50"
|
||||||
className="border-b hover:bg-gray-50"
|
>
|
||||||
>
|
<td className="py-3 px-4">
|
||||||
<td className="py-3 px-4">
|
<div className="font-medium">{mitigation.name}</div>
|
||||||
<div className="font-medium">{mitigation.name}</div>
|
{mitigation.description && (
|
||||||
{mitigation.description && (
|
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
||||||
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
{mitigation.description}
|
||||||
{mitigation.description}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</td>
|
||||||
</td>
|
<td className="py-3 px-4">
|
||||||
<td className="py-3 px-4">
|
<div
|
||||||
<div
|
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
mitigation.importance
|
||||||
mitigation.importance
|
)} inline-block`}
|
||||||
)} inline-block`}
|
>
|
||||||
|
{formatImportance(mitigation.importance)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
||||||
|
mitigation.state
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatState(mitigation.state)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="text-xs h-7"
|
||||||
>
|
>
|
||||||
{formatImportance(mitigation.importance)}
|
<Link
|
||||||
</div>
|
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
|
||||||
</td>
|
>
|
||||||
<td className="py-3 px-4">
|
View
|
||||||
<div
|
</Link>
|
||||||
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
</Button>
|
||||||
mitigation.state
|
<Button
|
||||||
)} inline-block`}
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingMitigation}
|
||||||
|
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
||||||
>
|
>
|
||||||
{formatState(mitigation.state)}
|
Unlink
|
||||||
</div>
|
</Button>
|
||||||
</td>
|
</div>
|
||||||
<td className="py-3 px-4 text-right whitespace-nowrap">
|
</td>
|
||||||
<div className="flex gap-2 justify-end">
|
</tr>
|
||||||
<Button
|
))}
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
asChild
|
|
||||||
className="text-xs h-7"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleUnlinkMitigation(mitigation.id)
|
|
||||||
}
|
|
||||||
disabled={isUnlinkingMitigation}
|
|
||||||
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
Unlink
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -318,21 +318,35 @@ function RiskListViewContent({
|
|||||||
key={risk.id}
|
key={risk.id}
|
||||||
className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted cursor-pointer"
|
className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted cursor-pointer"
|
||||||
>
|
>
|
||||||
<td className="p-4 align-middle font-medium w-1/2">
|
<td className="p-0 align-middle font-medium w-1/2">
|
||||||
{risk.name}
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/risks/${risk.id}`}
|
||||||
|
className="block p-4 h-full w-full"
|
||||||
|
>
|
||||||
|
{risk.name}
|
||||||
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 align-middle w-1/4 whitespace-nowrap">
|
<td className="p-0 align-middle w-1/4 whitespace-nowrap">
|
||||||
{floatToProbabilityText(risk.probability)}
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/risks/${risk.id}`}
|
||||||
|
className="block p-4 h-full w-full"
|
||||||
|
>
|
||||||
|
{floatToProbabilityText(risk.probability)}
|
||||||
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 align-middle w-1/4 whitespace-nowrap">
|
<td className="p-0 align-middle w-1/4 whitespace-nowrap">
|
||||||
{floatToImpactText(risk.impact)}
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/risks/${risk.id}`}
|
||||||
|
className="block p-4 h-full w-full"
|
||||||
|
>
|
||||||
|
{floatToImpactText(risk.impact)}
|
||||||
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 align-middle w-[80px]">
|
<td className="p-4 align-middle w-[80px]">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
e.stopPropagation();
|
|
||||||
setRiskToDelete({ id: risk.id, name: risk.name });
|
setRiskToDelete({ id: risk.id, name: risk.name });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
29
apps/console/src/pages/organizations/risks/ShowRiskPage.tsx
Normal file
29
apps/console/src/pages/organizations/risks/ShowRiskPage.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { lazy } from "@probo/react-lazy";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
|
|
||||||
|
const ShowRiskView = lazy(() => import("./ShowRiskView"));
|
||||||
|
|
||||||
|
export function ShowRiskViewSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-96 bg-muted animate-pulse rounded-lg" />
|
||||||
|
</div>
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewRiskPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={location.pathname} fallback={<ShowRiskViewSkeleton />}>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<ShowRiskView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
731
apps/console/src/pages/organizations/risks/ShowRiskView.tsx
Normal file
731
apps/console/src/pages/organizations/risks/ShowRiskView.tsx
Normal file
@@ -0,0 +1,731 @@
|
|||||||
|
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||||
|
import { useParams, Link } from "react-router";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
PreloadedQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
useMutation,
|
||||||
|
fetchQuery,
|
||||||
|
useRelayEnvironment,
|
||||||
|
} from "react-relay";
|
||||||
|
import type { ShowRiskViewQuery } from "./__generated__/ShowRiskViewQuery.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { ShowRiskViewSkeleton } from "./ShowRiskPage";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus, Trash2, Search } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
ShowRiskViewOrganizationMitigationsQuery,
|
||||||
|
ShowRiskViewOrganizationMitigationsQuery$data,
|
||||||
|
} from "./__generated__/ShowRiskViewOrganizationMitigationsQuery.graphql";
|
||||||
|
|
||||||
|
const showRiskViewQuery = graphql`
|
||||||
|
query ShowRiskViewQuery($riskId: ID!) {
|
||||||
|
node(id: $riskId) {
|
||||||
|
id
|
||||||
|
... on Risk {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
probability
|
||||||
|
impact
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
mitigations(first: 100) @connection(key: "Risk__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
createdAt
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add query to fetch all mitigations for the organization
|
||||||
|
const organizationMitigationsQuery = graphql`
|
||||||
|
query ShowRiskViewOrganizationMitigationsQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
id
|
||||||
|
... on Organization {
|
||||||
|
mitigations(first: 100) @connection(key: "Organization__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add mutation to create risk-mitigation mapping
|
||||||
|
const createRiskMappingMutation = graphql`
|
||||||
|
mutation ShowRiskViewCreateRiskMappingMutation(
|
||||||
|
$input: CreateRiskMappingInput!
|
||||||
|
) {
|
||||||
|
createRiskMapping(input: $input) {
|
||||||
|
success
|
||||||
|
risk {
|
||||||
|
id
|
||||||
|
mitigations(first: 100) @connection(key: "Risk__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add mutation to delete risk-mitigation mapping
|
||||||
|
const deleteRiskMappingMutation = graphql`
|
||||||
|
mutation ShowRiskViewDeleteRiskMappingMutation(
|
||||||
|
$input: DeleteRiskMappingInput!
|
||||||
|
) {
|
||||||
|
deleteRiskMapping(input: $input) {
|
||||||
|
success
|
||||||
|
risk {
|
||||||
|
id
|
||||||
|
mitigations(first: 100) @connection(key: "Risk__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function formatDate(dateString: string) {
|
||||||
|
return new Date(dateString).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRiskSeverity(probability: number, impact: number) {
|
||||||
|
const score = probability * impact;
|
||||||
|
if (score >= 0.75) return { level: "High", class: "bg-red-100 text-red-800" };
|
||||||
|
if (score >= 0.4)
|
||||||
|
return { level: "Medium", class: "bg-yellow-100 text-yellow-800" };
|
||||||
|
return { level: "Low", class: "bg-green-100 text-green-800" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowRiskViewContent({
|
||||||
|
queryRef,
|
||||||
|
loadQuery,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<ShowRiskViewQuery>;
|
||||||
|
loadQuery: (variables: { riskId: string }) => void;
|
||||||
|
}) {
|
||||||
|
const data = usePreloadedQuery<ShowRiskViewQuery>(
|
||||||
|
showRiskViewQuery,
|
||||||
|
queryRef
|
||||||
|
);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const environment = useRelayEnvironment();
|
||||||
|
const { organizationId } = useParams<{ organizationId: string }>();
|
||||||
|
|
||||||
|
// Cast the node to Risk type
|
||||||
|
const risk = data.node;
|
||||||
|
const severity = getRiskSeverity(risk.probability!, risk.impact!);
|
||||||
|
|
||||||
|
// Add state for mitigation mapping dialog
|
||||||
|
const [isMitigationDialogOpen, setIsMitigationDialogOpen] = useState(false);
|
||||||
|
const [organizationMitigationsData, setOrganizationMitigationsData] =
|
||||||
|
useState<ShowRiskViewOrganizationMitigationsQuery$data | null>(null);
|
||||||
|
const [mitigationSearchQuery, setMitigationSearchQuery] = useState("");
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||||
|
const [isLoadingMitigations, setIsLoadingMitigations] = useState(false);
|
||||||
|
const [isLinkingMitigation, setIsLinkingMitigation] = useState(false);
|
||||||
|
const [isUnlinkingMitigation, setIsUnlinkingMitigation] = useState(false);
|
||||||
|
const [currentMitigationId, setCurrentMitigationId] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup mutation hooks
|
||||||
|
const [commitCreateRiskMapping] = useMutation(createRiskMappingMutation);
|
||||||
|
const [commitDeleteRiskMapping] = useMutation(deleteRiskMappingMutation);
|
||||||
|
|
||||||
|
// Clear filters when dialog closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMitigationDialogOpen) {
|
||||||
|
setMitigationSearchQuery("");
|
||||||
|
setCategoryFilter(null);
|
||||||
|
}
|
||||||
|
}, [isMitigationDialogOpen]);
|
||||||
|
|
||||||
|
// Load mitigations data when needed
|
||||||
|
const loadMitigationsData = useCallback(() => {
|
||||||
|
if (!organizationId || !risk.id) return;
|
||||||
|
|
||||||
|
setIsLoadingMitigations(true);
|
||||||
|
|
||||||
|
// Fetch all mitigations for the organization
|
||||||
|
fetchQuery<ShowRiskViewOrganizationMitigationsQuery>(
|
||||||
|
environment,
|
||||||
|
organizationMitigationsQuery,
|
||||||
|
{
|
||||||
|
organizationId,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setOrganizationMitigationsData(data);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error fetching organization mitigations:", error);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load mitigations.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [risk.id, environment, organizationId, toast]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const getMitigations = useCallback(() => {
|
||||||
|
if (!organizationMitigationsData?.organization?.mitigations?.edges)
|
||||||
|
return [];
|
||||||
|
return organizationMitigationsData.organization.mitigations.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
}, [organizationMitigationsData]);
|
||||||
|
|
||||||
|
const getMitigationCategories = useCallback(() => {
|
||||||
|
const mitigations = getMitigations();
|
||||||
|
const categories = new Set<string>();
|
||||||
|
|
||||||
|
mitigations.forEach((mitigation) => {
|
||||||
|
if (mitigation.category) {
|
||||||
|
categories.add(mitigation.category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(categories).sort();
|
||||||
|
}, [getMitigations]);
|
||||||
|
|
||||||
|
const filteredMitigations = useCallback(() => {
|
||||||
|
const mitigations = getMitigations();
|
||||||
|
if (!mitigationSearchQuery && !categoryFilter) return mitigations;
|
||||||
|
|
||||||
|
return mitigations.filter((mitigation) => {
|
||||||
|
// Filter by search query
|
||||||
|
const matchesSearch =
|
||||||
|
!mitigationSearchQuery ||
|
||||||
|
mitigation.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(mitigationSearchQuery.toLowerCase()) ||
|
||||||
|
(mitigation.description &&
|
||||||
|
mitigation.description
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(mitigationSearchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
// Filter by category
|
||||||
|
const matchesCategory =
|
||||||
|
!categoryFilter ||
|
||||||
|
categoryFilter === "all" ||
|
||||||
|
mitigation.category === categoryFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
});
|
||||||
|
}, [getMitigations, mitigationSearchQuery, categoryFilter]);
|
||||||
|
|
||||||
|
// Handle linking a mitigation to this risk
|
||||||
|
const handleLinkMitigation = useCallback(
|
||||||
|
(
|
||||||
|
mitigation: NonNullable<
|
||||||
|
NonNullable<
|
||||||
|
ShowRiskViewOrganizationMitigationsQuery$data["organization"]
|
||||||
|
>["mitigations"]
|
||||||
|
>["edges"][0]["node"]
|
||||||
|
) => {
|
||||||
|
if (!risk.id) return;
|
||||||
|
|
||||||
|
setIsLinkingMitigation(true);
|
||||||
|
setCurrentMitigationId(mitigation.id);
|
||||||
|
|
||||||
|
commitCreateRiskMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
riskId: risk.id,
|
||||||
|
mitigationId: mitigation.id,
|
||||||
|
probability: risk.probability,
|
||||||
|
impact: risk.impact,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsLinkingMitigation(false);
|
||||||
|
setCurrentMitigationId(null);
|
||||||
|
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
console.error("Error linking mitigation:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh main query data using the exact same pattern as in ControlView.tsx
|
||||||
|
fetchQuery(environment, showRiskViewQuery, {
|
||||||
|
riskId: risk.id,
|
||||||
|
}).subscribe({
|
||||||
|
next: () => {
|
||||||
|
// Force reload the view data to reflect changes
|
||||||
|
loadQuery({ riskId: risk.id });
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing risk data:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: `Linked mitigation "${mitigation.name}" to this risk.`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsLinkingMitigation(false);
|
||||||
|
setCurrentMitigationId(null);
|
||||||
|
console.error("Error linking mitigation:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
risk.id,
|
||||||
|
risk.probability,
|
||||||
|
risk.impact,
|
||||||
|
commitCreateRiskMapping,
|
||||||
|
toast,
|
||||||
|
environment,
|
||||||
|
loadQuery,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle unlinking a mitigation from this risk
|
||||||
|
const handleUnlinkMitigation = useCallback(
|
||||||
|
(
|
||||||
|
mitigation: NonNullable<
|
||||||
|
NonNullable<
|
||||||
|
ShowRiskViewOrganizationMitigationsQuery$data["organization"]
|
||||||
|
>["mitigations"]
|
||||||
|
>["edges"][0]["node"]
|
||||||
|
) => {
|
||||||
|
if (!risk.id) return;
|
||||||
|
|
||||||
|
setIsUnlinkingMitigation(true);
|
||||||
|
setCurrentMitigationId(mitigation.id);
|
||||||
|
|
||||||
|
commitDeleteRiskMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
riskId: risk.id,
|
||||||
|
mitigationId: mitigation.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsUnlinkingMitigation(false);
|
||||||
|
setCurrentMitigationId(null);
|
||||||
|
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
console.error("Error unlinking mitigation:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh main query data using the exact same pattern as in ControlView.tsx
|
||||||
|
fetchQuery(environment, showRiskViewQuery, {
|
||||||
|
riskId: risk.id,
|
||||||
|
}).subscribe({
|
||||||
|
next: () => {
|
||||||
|
// Force reload the view data to reflect changes
|
||||||
|
loadQuery({ riskId: risk.id });
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing risk data:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: `Unlinked mitigation "${mitigation.name}" from this risk.`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsUnlinkingMitigation(false);
|
||||||
|
setCurrentMitigationId(null);
|
||||||
|
console.error("Error unlinking mitigation:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[risk.id, commitDeleteRiskMapping, toast, environment, loadQuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate title={risk.name ?? ""} description={risk.description || ""}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Risk Overview</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
|
Probability
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-lg">
|
||||||
|
{(risk.probability! * 100).toFixed(0)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Impact</h3>
|
||||||
|
<p className="mt-1 text-lg">
|
||||||
|
{(risk.impact! * 100).toFixed(0)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Severity</h3>
|
||||||
|
<p className="mt-1">
|
||||||
|
<Badge className={severity.class}>{severity.level}</Badge>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Created</h3>
|
||||||
|
<p className="mt-1 text-sm">{formatDate(risk.createdAt!)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Tabs defaultValue="mitigations" className="w-full">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="mitigations">Mitigations</TabsTrigger>
|
||||||
|
<TabsTrigger value="details">Details</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="mitigations" className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-semibold">Risk Mitigations</h2>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsMitigationDialogOpen(true);
|
||||||
|
loadMitigationsData();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Link Mitigation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{risk.mitigations?.edges?.length &&
|
||||||
|
risk.mitigations?.edges?.length > 0 ? (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-full">Mitigation</TableHead>
|
||||||
|
<TableHead className="w-20">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{risk.mitigations?.edges.map(({ node: mitigation }) => (
|
||||||
|
<TableRow key={mitigation.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
|
||||||
|
className="font-medium text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
{mitigation.name}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleUnlinkMitigation(mitigation)}
|
||||||
|
disabled={isUnlinkingMitigation}
|
||||||
|
title="Unlink mitigation"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-10 text-gray-500">
|
||||||
|
<p>No mitigations associated with this risk.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="details">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
|
Full Description
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1">{risk.description}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
|
Last Updated
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1">{formatDate(risk.updatedAt!)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Dialog for linking mitigations */}
|
||||||
|
<Dialog
|
||||||
|
open={isMitigationDialogOpen}
|
||||||
|
onOpenChange={setIsMitigationDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<DialogTitle>Link Mitigation to Risk</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select a mitigation to link to this risk.
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
// Force a reload of the risk data to ensure latest mitigation links
|
||||||
|
loadQuery({ riskId: risk.id });
|
||||||
|
// Then load the organization mitigations
|
||||||
|
loadMitigationsData();
|
||||||
|
}}
|
||||||
|
title="Refresh data"
|
||||||
|
disabled={isLoadingMitigations}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={`${isLoadingMitigations ? "animate-spin" : ""}`}
|
||||||
|
>
|
||||||
|
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
||||||
|
<path d="M3 3v5h5" />
|
||||||
|
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
|
||||||
|
<path d="M16 21h5v-5" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="flex flex-row space-x-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search mitigations..."
|
||||||
|
className="pl-8"
|
||||||
|
value={mitigationSearchQuery}
|
||||||
|
onChange={(e) => setMitigationSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={categoryFilter || "all"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setCategoryFilter(value === "all" ? null : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Filter by category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Categories</SelectItem>
|
||||||
|
{getMitigationCategories().map((category) => (
|
||||||
|
<SelectItem key={category} value={category}>
|
||||||
|
{category}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md max-h-96 overflow-y-auto">
|
||||||
|
{isLoadingMitigations ? (
|
||||||
|
<div className="p-4 text-center">Loading mitigations...</div>
|
||||||
|
) : filteredMitigations().length === 0 ? (
|
||||||
|
<div className="p-4 text-center">No mitigations found.</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y">
|
||||||
|
{filteredMitigations().map((mitigation) => {
|
||||||
|
// For each render, recalculate linked status directly against the current risk data
|
||||||
|
const isLinked = risk.mitigations?.edges?.some(
|
||||||
|
(edge) => edge.node.id === mitigation.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const isProcessing =
|
||||||
|
(isLinkingMitigation &&
|
||||||
|
currentMitigationId === mitigation.id) ||
|
||||||
|
(isUnlinkingMitigation &&
|
||||||
|
currentMitigationId === mitigation.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={mitigation.id}
|
||||||
|
className="p-4 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{mitigation.name}</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={isLinked ? "destructive" : "default"}
|
||||||
|
size="sm"
|
||||||
|
disabled={isProcessing}
|
||||||
|
onClick={() =>
|
||||||
|
isLinked
|
||||||
|
? handleUnlinkMitigation(mitigation)
|
||||||
|
: handleLinkMitigation(mitigation)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isUnlinkingMitigation &&
|
||||||
|
currentMitigationId === mitigation.id
|
||||||
|
? "Unlinking..."
|
||||||
|
: isLinkingMitigation &&
|
||||||
|
currentMitigationId === mitigation.id
|
||||||
|
? "Linking..."
|
||||||
|
: isLinked
|
||||||
|
? "Unlink"
|
||||||
|
: "Link"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsMitigationDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShowRiskView() {
|
||||||
|
const { riskId } = useParams();
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<ShowRiskViewQuery>(showRiskViewQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuery({ riskId: riskId! });
|
||||||
|
}, [loadQuery, riskId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <ShowRiskViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ShowRiskViewSkeleton />}>
|
||||||
|
<ShowRiskViewContent queryRef={queryRef} loadQuery={loadQuery} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<16f4621b72343a04c88df9d0b137c9fd>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type CreateRiskMappingInput = {
|
||||||
|
impact: number;
|
||||||
|
mitigationId: string;
|
||||||
|
probability: number;
|
||||||
|
riskId: string;
|
||||||
|
};
|
||||||
|
export type ShowRiskViewCreateRiskMappingMutation$variables = {
|
||||||
|
input: CreateRiskMappingInput;
|
||||||
|
};
|
||||||
|
export type ShowRiskViewCreateRiskMappingMutation$data = {
|
||||||
|
readonly createRiskMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ShowRiskViewCreateRiskMappingMutation = {
|
||||||
|
response: ShowRiskViewCreateRiskMappingMutation$data;
|
||||||
|
variables: ShowRiskViewCreateRiskMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "CreateRiskMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createRiskMapping",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "success",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ShowRiskViewCreateRiskMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ShowRiskViewCreateRiskMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "32dfd9049a8c6c1d404528bac1316346",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ShowRiskViewCreateRiskMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ShowRiskViewCreateRiskMappingMutation(\n $input: CreateRiskMappingInput!\n) {\n createRiskMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "02f82b14cc6cf7c2e39dad4613e9d191";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<45a4aca30e16d598cd188f3cbc041a0d>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DeleteRiskMappingInput = {
|
||||||
|
mitigationId: string;
|
||||||
|
riskId: string;
|
||||||
|
};
|
||||||
|
export type ShowRiskViewDeleteRiskMappingMutation$variables = {
|
||||||
|
input: DeleteRiskMappingInput;
|
||||||
|
};
|
||||||
|
export type ShowRiskViewDeleteRiskMappingMutation$data = {
|
||||||
|
readonly deleteRiskMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ShowRiskViewDeleteRiskMappingMutation = {
|
||||||
|
response: ShowRiskViewDeleteRiskMappingMutation$data;
|
||||||
|
variables: ShowRiskViewDeleteRiskMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "DeleteRiskMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteRiskMapping",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "success",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ShowRiskViewDeleteRiskMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ShowRiskViewDeleteRiskMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "847faec465a30f457c57c5c1524a707d",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ShowRiskViewDeleteRiskMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ShowRiskViewDeleteRiskMappingMutation(\n $input: DeleteRiskMappingInput!\n) {\n deleteRiskMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "41aa62a4185b1e6061bd7e06abbec85d";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<771e7ea5d1af4e722dc5790db8abd623>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
|
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
|
export type ShowRiskViewOrganizationMitigationsQuery$variables = {
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type ShowRiskViewOrganizationMitigationsQuery$data = {
|
||||||
|
readonly organization: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly mitigations?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly category: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly importance: MitigationImportance;
|
||||||
|
readonly name: string;
|
||||||
|
readonly state: MitigationState;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ShowRiskViewOrganizationMitigationsQuery = {
|
||||||
|
response: ShowRiskViewOrganizationMitigationsQuery$data;
|
||||||
|
variables: ShowRiskViewOrganizationMitigationsQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Mitigation",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "category",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "importance",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "cursor",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "PageInfo",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "pageInfo",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "endCursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "hasNextPage",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v5 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ShowRiskViewOrganizationMitigationsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "mitigations",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Organization__mitigations_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ShowRiskViewOrganizationMitigationsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mitigations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": "mitigations(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Organization__mitigations",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "mitigations"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "ac4f3fc31facab3cb2b9455751a4a528",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"organization",
|
||||||
|
"mitigations"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "ShowRiskViewOrganizationMitigationsQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query ShowRiskViewOrganizationMitigationsQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "608bcf7b0e53529673b25ac7762627e2";
|
||||||
|
|
||||||
|
export default node;
|
||||||
246
apps/console/src/pages/organizations/risks/__generated__/ShowRiskViewQuery.graphql.ts
generated
Normal file
246
apps/console/src/pages/organizations/risks/__generated__/ShowRiskViewQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<c8807fd1479464c46122e4ca904713f3>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
|
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
|
export type ShowRiskViewQuery$variables = {
|
||||||
|
riskId: string;
|
||||||
|
};
|
||||||
|
export type ShowRiskViewQuery$data = {
|
||||||
|
readonly node: {
|
||||||
|
readonly createdAt?: string;
|
||||||
|
readonly description?: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly impact?: number;
|
||||||
|
readonly mitigations?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly category: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly importance: MitigationImportance;
|
||||||
|
readonly name: string;
|
||||||
|
readonly state: MitigationState;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly name?: string;
|
||||||
|
readonly probability?: number;
|
||||||
|
readonly updatedAt?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ShowRiskViewQuery = {
|
||||||
|
response: ShowRiskViewQuery$data;
|
||||||
|
variables: ShowRiskViewQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "riskId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "riskId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v5 = {
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v4/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "probability",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "impact",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "createdAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "updatedAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mitigations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Mitigation",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v4/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "category",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "importance",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "mitigations(first:100)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Risk",
|
||||||
|
"abstractKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ShowRiskViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v5/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ShowRiskViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v5/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "1fa87a20a3f945fa7da2f72e5b4fa52f",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ShowRiskViewQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query ShowRiskViewQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n description\n probability\n impact\n createdAt\n updatedAt\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n }\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "824859cf39078da910b00879c17a2a9d";
|
||||||
|
|
||||||
|
export default node;
|
||||||
1
pkg/coredata/migrations/20250401T082800Z.sql
Normal file
1
pkg/coredata/migrations/20250401T082800Z.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE risks_mitigations ADD COLUMN tenant_id TEXT NOT NULL;
|
||||||
1
pkg/coredata/migrations/20250401T082900Z.sql
Normal file
1
pkg/coredata/migrations/20250401T082900Z.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE risks_mitigations ADD COLUMN created_at TIMESTAMP NOT NULL;
|
||||||
Reference in New Issue
Block a user