Add ability to link policies to controls
Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
@@ -4,15 +4,17 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.4.2] - 2025-04-09
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add vendor compliance reports UI
|
- Add vendor compliance reports UI
|
||||||
|
- Controls can now be linked to policies, enabling better organization of compliance documentation and clearer traceability between policies and security controls
|
||||||
|
- New UI for viewing and managing policies related to a specific control
|
||||||
|
|
||||||
|
## [0.4.2] - 2025-04-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Simplified policy data model by removing version field and optimistic concurrency
|
- Simplified policy data model by removing version field and optimistic concurrency
|
||||||
- Refactored policy update flow to load-modify-save pattern
|
- Refactored policy update flow to load-modify-save pattern
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ import {
|
|||||||
ControlOrganizationMitigationsQuery,
|
ControlOrganizationMitigationsQuery,
|
||||||
} from "./__generated__/ControlOrganizationMitigationsQuery.graphql";
|
} from "./__generated__/ControlOrganizationMitigationsQuery.graphql";
|
||||||
import { ControlFragment_Control$key } from "./__generated__/ControlFragment_Control.graphql";
|
import { ControlFragment_Control$key } from "./__generated__/ControlFragment_Control.graphql";
|
||||||
|
import {
|
||||||
|
ControlLinkedPoliciesQuery$data,
|
||||||
|
ControlLinkedPoliciesQuery,
|
||||||
|
} from "./__generated__/ControlLinkedPoliciesQuery.graphql";
|
||||||
|
import {
|
||||||
|
ControlOrganizationPoliciesQuery$data,
|
||||||
|
ControlOrganizationPoliciesQuery,
|
||||||
|
} from "./__generated__/ControlOrganizationPoliciesQuery.graphql";
|
||||||
|
|
||||||
const controlFragment = graphql`
|
const controlFragment = graphql`
|
||||||
fragment ControlFragment_Control on Control {
|
fragment ControlFragment_Control on Control {
|
||||||
@@ -90,6 +98,50 @@ const organizationMitigationsQuery = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Query to fetch linked policies
|
||||||
|
const linkedPoliciesQuery = graphql`
|
||||||
|
query ControlLinkedPoliciesQuery($controlId: ID!) {
|
||||||
|
control: node(id: $controlId) {
|
||||||
|
id
|
||||||
|
... on Control {
|
||||||
|
policies(first: 100) @connection(key: "Control__policies") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
content
|
||||||
|
status
|
||||||
|
reviewDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Query to fetch all policies for the organization
|
||||||
|
const organizationPoliciesQuery = graphql`
|
||||||
|
query ControlOrganizationPoliciesQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
id
|
||||||
|
... on Organization {
|
||||||
|
policies(first: 100) @connection(key: "Organization__policies") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
content
|
||||||
|
status
|
||||||
|
reviewDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// Mutation to create a mapping between a control and a mitigation
|
// Mutation to create a mapping between a control and a mitigation
|
||||||
const createMitigationMappingMutation = graphql`
|
const createMitigationMappingMutation = graphql`
|
||||||
mutation ControlCreateMitigationMappingMutation(
|
mutation ControlCreateMitigationMappingMutation(
|
||||||
@@ -112,6 +164,28 @@ const deleteMitigationMappingMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Mutation to create a mapping between a control and a policy
|
||||||
|
const createPolicyMappingMutation = graphql`
|
||||||
|
mutation ControlCreatePolicyMappingMutation(
|
||||||
|
$input: CreateControlPolicyMappingInput!
|
||||||
|
) {
|
||||||
|
createControlPolicyMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mutation to delete a mapping between a control and a policy
|
||||||
|
const deletePolicyMappingMutation = graphql`
|
||||||
|
mutation ControlDeletePolicyMappingMutation(
|
||||||
|
$input: DeleteControlPolicyMappingInput!
|
||||||
|
) {
|
||||||
|
deleteControlPolicyMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export function Control({
|
export function Control({
|
||||||
controlKey,
|
controlKey,
|
||||||
}: {
|
}: {
|
||||||
@@ -138,6 +212,18 @@ export function Control({
|
|||||||
const [isUnlinkingMitigation, setIsUnlinkingMitigation] = useState(false);
|
const [isUnlinkingMitigation, setIsUnlinkingMitigation] = useState(false);
|
||||||
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Policy state
|
||||||
|
const [isPolicyMappingDialogOpen, setIsPolicyMappingDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [linkedPoliciesData, setLinkedPoliciesData] =
|
||||||
|
useState<ControlLinkedPoliciesQuery$data | null>(null);
|
||||||
|
const [organizationPoliciesData, setOrganizationPoliciesData] =
|
||||||
|
useState<ControlOrganizationPoliciesQuery$data | null>(null);
|
||||||
|
const [policySearchQuery, setPolicySearchQuery] = useState("");
|
||||||
|
const [isLoadingPolicies, setIsLoadingPolicies] = useState(false);
|
||||||
|
const [isLinkingPolicy, setIsLinkingPolicy] = useState(false);
|
||||||
|
const [isUnlinkingPolicy, setIsUnlinkingPolicy] = useState(false);
|
||||||
|
|
||||||
// Create mutation hooks
|
// Create mutation hooks
|
||||||
const [commitCreateMitigationMapping] = useMutation(
|
const [commitCreateMitigationMapping] = useMutation(
|
||||||
createMitigationMappingMutation
|
createMitigationMappingMutation
|
||||||
@@ -145,6 +231,8 @@ export function Control({
|
|||||||
const [commitDeleteMitigationMapping] = useMutation(
|
const [commitDeleteMitigationMapping] = useMutation(
|
||||||
deleteMitigationMappingMutation
|
deleteMitigationMappingMutation
|
||||||
);
|
);
|
||||||
|
const [commitCreatePolicyMapping] = useMutation(createPolicyMappingMutation);
|
||||||
|
const [commitDeletePolicyMapping] = useMutation(deletePolicyMappingMutation);
|
||||||
|
|
||||||
// Load initial linked mitigations data
|
// Load initial linked mitigations data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -415,6 +503,243 @@ export function Control({
|
|||||||
setIsMitigationMappingDialogOpen(true);
|
setIsMitigationMappingDialogOpen(true);
|
||||||
}, [loadMitigationsData]);
|
}, [loadMitigationsData]);
|
||||||
|
|
||||||
|
// Load initial linked policies data
|
||||||
|
useEffect(() => {
|
||||||
|
if (control.id) {
|
||||||
|
setIsLoadingPolicies(true);
|
||||||
|
fetchQuery<ControlLinkedPoliciesQuery>(environment, linkedPoliciesQuery, {
|
||||||
|
controlId: control.id,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedPoliciesData(data);
|
||||||
|
setIsLoadingPolicies(false);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error loading initial policies:", error);
|
||||||
|
setIsLoadingPolicies(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [control.id, environment]);
|
||||||
|
|
||||||
|
// Load policies data
|
||||||
|
const loadPoliciesData = useCallback(() => {
|
||||||
|
if (!organizationId || !control.id) return;
|
||||||
|
|
||||||
|
setIsLoadingPolicies(true);
|
||||||
|
|
||||||
|
// Fetch all policies for the organization
|
||||||
|
fetchQuery<ControlOrganizationPoliciesQuery>(
|
||||||
|
environment,
|
||||||
|
organizationPoliciesQuery,
|
||||||
|
{
|
||||||
|
organizationId,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setOrganizationPoliciesData(data);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
// Fetch linked policies for this control
|
||||||
|
fetchQuery<ControlLinkedPoliciesQuery>(
|
||||||
|
environment,
|
||||||
|
linkedPoliciesQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedPoliciesData(data);
|
||||||
|
setIsLoadingPolicies(false);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error fetching linked policies:", error);
|
||||||
|
setIsLoadingPolicies(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load linked policies.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error fetching organization policies:", error);
|
||||||
|
setIsLoadingPolicies(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load policies.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [control.id, environment, organizationId, toast]);
|
||||||
|
|
||||||
|
// Policy helper functions
|
||||||
|
const getPolicies = useCallback(() => {
|
||||||
|
if (!organizationPoliciesData?.organization?.policies?.edges) return [];
|
||||||
|
return organizationPoliciesData.organization.policies.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
}, [organizationPoliciesData]);
|
||||||
|
|
||||||
|
const getLinkedPolicies = useCallback(() => {
|
||||||
|
if (!linkedPoliciesData?.control?.policies?.edges) return [];
|
||||||
|
return linkedPoliciesData.control.policies.edges.map((edge) => edge.node);
|
||||||
|
}, [linkedPoliciesData]);
|
||||||
|
|
||||||
|
const isPolicyLinked = useCallback(
|
||||||
|
(policyId: string) => {
|
||||||
|
const linkedPolicies = getLinkedPolicies();
|
||||||
|
return linkedPolicies.some((policy) => policy.id === policyId);
|
||||||
|
},
|
||||||
|
[getLinkedPolicies]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredPolicies = useCallback(() => {
|
||||||
|
const policies = getPolicies();
|
||||||
|
if (!policySearchQuery) return policies;
|
||||||
|
|
||||||
|
return policies.filter((policy) => {
|
||||||
|
return (
|
||||||
|
!policySearchQuery ||
|
||||||
|
policy.name.toLowerCase().includes(policySearchQuery.toLowerCase()) ||
|
||||||
|
(policy.content &&
|
||||||
|
policy.content
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(policySearchQuery.toLowerCase()))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [getPolicies, policySearchQuery]);
|
||||||
|
|
||||||
|
// Policy link/unlink handlers
|
||||||
|
const handleLinkPolicy = useCallback(
|
||||||
|
(policyId: string) => {
|
||||||
|
if (!control.id) return;
|
||||||
|
|
||||||
|
setIsLinkingPolicy(true);
|
||||||
|
|
||||||
|
commitCreatePolicyMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId: control.id,
|
||||||
|
policyId: policyId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsLinkingPolicy(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error linking policy:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link policy. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked policies data
|
||||||
|
fetchQuery<ControlLinkedPoliciesQuery>(
|
||||||
|
environment,
|
||||||
|
linkedPoliciesQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedPoliciesData(data);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing linked policies:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Policy successfully linked to control.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsLinkingPolicy(false);
|
||||||
|
console.error("Error linking policy:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link policy. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitCreatePolicyMapping, control.id, environment, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUnlinkPolicy = useCallback(
|
||||||
|
(policyId: string) => {
|
||||||
|
if (!control.id) return;
|
||||||
|
|
||||||
|
setIsUnlinkingPolicy(true);
|
||||||
|
|
||||||
|
commitDeletePolicyMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId: control.id,
|
||||||
|
policyId: policyId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsUnlinkingPolicy(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error unlinking policy:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink policy. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked policies data
|
||||||
|
fetchQuery<ControlLinkedPoliciesQuery>(
|
||||||
|
environment,
|
||||||
|
linkedPoliciesQuery,
|
||||||
|
{
|
||||||
|
controlId: control.id,
|
||||||
|
}
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedPoliciesData(data);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing linked policies:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Policy successfully unlinked from control.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsUnlinkingPolicy(false);
|
||||||
|
console.error("Error unlinking policy:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink policy. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitDeletePolicyMapping, control.id, environment, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenPolicyMappingDialog = useCallback(() => {
|
||||||
|
loadPoliciesData();
|
||||||
|
setIsPolicyMappingDialogOpen(true);
|
||||||
|
}, [loadPoliciesData]);
|
||||||
|
|
||||||
// UI helper functions
|
// UI helper functions
|
||||||
const formatImportance = (importance: string | undefined): string => {
|
const formatImportance = (importance: string | undefined): string => {
|
||||||
if (!importance) return "Unknown";
|
if (!importance) return "Unknown";
|
||||||
@@ -773,6 +1098,256 @@ export function Control({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Policies Section */}
|
||||||
|
<div className="mt-8">
|
||||||
|
{/* Policy Mapping Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={isPolicyMappingDialogOpen}
|
||||||
|
onOpenChange={setIsPolicyMappingDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Link Policies to Control</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Search and select policies to link to this control. This helps
|
||||||
|
track which policies address this control.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-tertiary" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search policies by name or content..."
|
||||||
|
value={policySearchQuery}
|
||||||
|
onChange={(e) => setPolicySearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{isLoadingPolicies ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-info" />
|
||||||
|
<span className="ml-2">Loading policies...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-[50vh] overflow-y-auto pr-2">
|
||||||
|
{filteredPolicies().length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-secondary">
|
||||||
|
No policies found. Try adjusting your search.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className="w-full bg-level-1">
|
||||||
|
<thead className="sticky top-0 bg-white">
|
||||||
|
<tr className="border-b text-left text-sm text-secondary bg-invert-bg">
|
||||||
|
<th className="py-3 px-4 font-medium">Name</th>
|
||||||
|
<th className="py-3 px-4 font-medium">Status</th>
|
||||||
|
<th className="py-3 px-4 font-medium">
|
||||||
|
Review Date
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 font-medium text-right">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredPolicies().map((policy) => {
|
||||||
|
const isLinked = isPolicyLinked(policy.id);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={policy.id}
|
||||||
|
className="border-b hover:bg-invert-bg"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="font-medium">
|
||||||
|
{policy.name}
|
||||||
|
</div>
|
||||||
|
{policy.content && (
|
||||||
|
<div className="text-xs text-secondary line-clamp-1 mt-0.5">
|
||||||
|
{policy.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${
|
||||||
|
policy.status === "ACTIVE"
|
||||||
|
? "bg-success-bg text-success"
|
||||||
|
: "bg-secondary-bg text-secondary"
|
||||||
|
} inline-block`}
|
||||||
|
>
|
||||||
|
{policy.status}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
{policy.reviewDate
|
||||||
|
? new Date(
|
||||||
|
policy.reviewDate
|
||||||
|
).toLocaleDateString()
|
||||||
|
: "Not set"}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||||
|
{isLinked ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkPolicy(policy.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingPolicy}
|
||||||
|
className="text-xs h-7 text-danger border-danger-b hover:bg-h-danger-bg"
|
||||||
|
>
|
||||||
|
{isUnlinkingPolicy ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Unlink</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleLinkPolicy(policy.id)
|
||||||
|
}
|
||||||
|
disabled={isLinkingPolicy}
|
||||||
|
className="text-xs h-7 text-info border-info-b hover:bg-h-info-bg"
|
||||||
|
>
|
||||||
|
{isLinkingPolicy ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Link</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-4">
|
||||||
|
<Button onClick={() => setIsPolicyMappingDialogOpen(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Linked Policies List */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-xl font-medium text-secondary">Policies</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
onClick={handleOpenPolicyMappingDialog}
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
<span>Link Policies</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingPolicies ? (
|
||||||
|
<div className="flex items-center justify-center h-24">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-info" />
|
||||||
|
<span className="ml-2">Loading policies...</span>
|
||||||
|
</div>
|
||||||
|
) : linkedPoliciesData?.control?.policies?.edges &&
|
||||||
|
linkedPoliciesData.control.policies.edges.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto border rounded-md">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b text-left text-sm text-secondary bg-invert-bg">
|
||||||
|
<th className="py-3 px-4 font-medium">Name</th>
|
||||||
|
<th className="py-3 px-4 font-medium">Status</th>
|
||||||
|
<th className="py-3 px-4 font-medium">Review Date</th>
|
||||||
|
<th className="py-3 px-4 font-medium text-right">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{getLinkedPolicies().map((policy) => (
|
||||||
|
<tr
|
||||||
|
key={policy.id}
|
||||||
|
className="border-b hover:bg-invert-bg"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="font-medium">{policy.name}</div>
|
||||||
|
{policy.content && (
|
||||||
|
<div className="text-xs text-secondary line-clamp-1 mt-0.5">
|
||||||
|
{policy.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${
|
||||||
|
policy.status === "ACTIVE"
|
||||||
|
? "bg-success-bg text-success"
|
||||||
|
: "bg-secondary-bg text-secondary"
|
||||||
|
} inline-block`}
|
||||||
|
>
|
||||||
|
{policy.status}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
{policy.reviewDate
|
||||||
|
? new Date(policy.reviewDate).toLocaleDateString()
|
||||||
|
: "Not set"}
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/policies/${policy.id}`}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleUnlinkPolicy(policy.id)}
|
||||||
|
disabled={isUnlinkingPolicy}
|
||||||
|
className="text-xs h-7 text-danger border-danger-b hover:bg-h-danger-bg"
|
||||||
|
>
|
||||||
|
Unlink
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-secondary border rounded-md">
|
||||||
|
No policies linked to this control yet. Click "Link
|
||||||
|
Policies" to connect some.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<d614f25231ff53581f96021c5b52628a>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type CreateControlPolicyMappingInput = {
|
||||||
|
controlId: string;
|
||||||
|
policyId: string;
|
||||||
|
};
|
||||||
|
export type ControlCreatePolicyMappingMutation$variables = {
|
||||||
|
input: CreateControlPolicyMappingInput;
|
||||||
|
};
|
||||||
|
export type ControlCreatePolicyMappingMutation$data = {
|
||||||
|
readonly createControlPolicyMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlCreatePolicyMappingMutation = {
|
||||||
|
response: ControlCreatePolicyMappingMutation$data;
|
||||||
|
variables: ControlCreatePolicyMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "CreateControlPolicyMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createControlPolicyMapping",
|
||||||
|
"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": "ControlCreatePolicyMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlCreatePolicyMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "bde220ef49867cec6b77bffc06878258",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ControlCreatePolicyMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ControlCreatePolicyMappingMutation(\n $input: CreateControlPolicyMappingInput!\n) {\n createControlPolicyMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "8e6ef41146efb59340331e16ce0493d6";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<5988b98a6e2ae42a565e51fc13fa5c91>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DeleteControlPolicyMappingInput = {
|
||||||
|
controlId: string;
|
||||||
|
policyId: string;
|
||||||
|
};
|
||||||
|
export type ControlDeletePolicyMappingMutation$variables = {
|
||||||
|
input: DeleteControlPolicyMappingInput;
|
||||||
|
};
|
||||||
|
export type ControlDeletePolicyMappingMutation$data = {
|
||||||
|
readonly deleteControlPolicyMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlDeletePolicyMappingMutation = {
|
||||||
|
response: ControlDeletePolicyMappingMutation$data;
|
||||||
|
variables: ControlDeletePolicyMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "DeleteControlPolicyMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteControlPolicyMapping",
|
||||||
|
"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": "ControlDeletePolicyMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlDeletePolicyMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "3b84cd899eee67303695eae81d1ba453",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ControlDeletePolicyMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ControlDeletePolicyMappingMutation(\n $input: DeleteControlPolicyMappingInput!\n) {\n deleteControlPolicyMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "e706f52dac09ef3fcadeb588ccd0274d";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<9bddb18bf52f14881a85995793bdc1ca>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
||||||
|
export type ControlLinkedPoliciesQuery$variables = {
|
||||||
|
controlId: string;
|
||||||
|
};
|
||||||
|
export type ControlLinkedPoliciesQuery$data = {
|
||||||
|
readonly control: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly policies?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly content: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly reviewDate: string | null | undefined;
|
||||||
|
readonly status: PolicyStatus;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlLinkedPoliciesQuery = {
|
||||||
|
response: ControlLinkedPoliciesQuery$data;
|
||||||
|
variables: ControlLinkedPoliciesQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "controlId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "controlId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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": "PolicyEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Policy",
|
||||||
|
"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": "content",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "status",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "reviewDate",
|
||||||
|
"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": "ControlLinkedPoliciesQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "control",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "policies",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "PolicyConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Control__policies_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Control",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlLinkedPoliciesQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "control",
|
||||||
|
"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": "PolicyConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "policies",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": "policies(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Control__policies",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "policies"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Control",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "67f9bf1d5f255f5406e932abde540e08",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"control",
|
||||||
|
"policies"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "ControlLinkedPoliciesQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query ControlLinkedPoliciesQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n status\n reviewDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "3904c2344754177177fa7c16129dc894";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<cfe85b9fdabaf24f9a7f575eca8a6417>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
||||||
|
export type ControlOrganizationPoliciesQuery$variables = {
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type ControlOrganizationPoliciesQuery$data = {
|
||||||
|
readonly organization: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly policies?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly content: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly reviewDate: string | null | undefined;
|
||||||
|
readonly status: PolicyStatus;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlOrganizationPoliciesQuery = {
|
||||||
|
response: ControlOrganizationPoliciesQuery$data;
|
||||||
|
variables: ControlOrganizationPoliciesQuery$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": "PolicyEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Policy",
|
||||||
|
"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": "content",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "status",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "reviewDate",
|
||||||
|
"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": "ControlOrganizationPoliciesQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "policies",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "PolicyConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Organization__policies_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": "ControlOrganizationPoliciesQuery",
|
||||||
|
"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": "PolicyConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "policies",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": "policies(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Organization__policies",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "policies"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "3a3c91f3e18e8a4b66011aa583e262e4",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"organization",
|
||||||
|
"policies"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "ControlOrganizationPoliciesQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query ControlOrganizationPoliciesQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n status\n reviewDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "f4699d729e6df6192efd201047689204";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -43,7 +43,7 @@ func (cp ControlPolicy) Insert(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
controls_mitigations (
|
controls_policies (
|
||||||
control_id,
|
control_id,
|
||||||
policy_id,
|
policy_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
@@ -75,7 +75,7 @@ func (cp ControlPolicy) Delete(
|
|||||||
q := `
|
q := `
|
||||||
DELETE
|
DELETE
|
||||||
FROM
|
FROM
|
||||||
controls_mitigations
|
controls_policies
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND control_id = @control_id
|
AND control_id = @control_id
|
||||||
|
|||||||
@@ -266,7 +266,6 @@ WITH plcs AS (
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
tenant_id,
|
|
||||||
organization_id,
|
organization_id,
|
||||||
owner_id,
|
owner_id,
|
||||||
name,
|
name,
|
||||||
|
|||||||
Reference in New Issue
Block a user