@@ -13,6 +13,8 @@ import {
|
|||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
useQueryLoader,
|
useQueryLoader,
|
||||||
useMutation,
|
useMutation,
|
||||||
|
fetchQuery,
|
||||||
|
useRelayEnvironment,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -28,6 +30,7 @@ import {
|
|||||||
File as FileGeneric,
|
File as FileGeneric,
|
||||||
FileText,
|
FileText,
|
||||||
Image,
|
Image,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -74,7 +77,6 @@ const controlOverviewPageQuery = graphql`
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
fileUrl
|
|
||||||
mimeType
|
mimeType
|
||||||
filename
|
filename
|
||||||
size
|
size
|
||||||
@@ -153,6 +155,18 @@ const uploadEvidenceMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Add a GraphQL query to fetch the fileUrl for an evidence item
|
||||||
|
const getEvidenceFileUrlQuery = graphql`
|
||||||
|
query ControlOverviewPageGetEvidenceFileUrlQuery($evidenceId: ID!) {
|
||||||
|
node(id: $evidenceId) {
|
||||||
|
... on Evidence {
|
||||||
|
id
|
||||||
|
fileUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
function ControlOverviewPageContent({
|
function ControlOverviewPageContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
@@ -165,6 +179,7 @@ function ControlOverviewPageContent({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { organizationId, frameworkId, controlId } = useParams();
|
const { organizationId, frameworkId, controlId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const environment = useRelayEnvironment();
|
||||||
const [updateTaskState] =
|
const [updateTaskState] =
|
||||||
useMutation<ControlOverviewPageUpdateTaskStateMutationType>(
|
useMutation<ControlOverviewPageUpdateTaskStateMutationType>(
|
||||||
updateTaskStateMutation
|
updateTaskStateMutation
|
||||||
@@ -207,6 +222,16 @@ function ControlOverviewPageContent({
|
|||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
// Add state for the preview modal
|
||||||
|
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
||||||
|
const [previewEvidence, setPreviewEvidence] = useState<{
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
fileUrl?: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [isLoadingFileUrl, setIsLoadingFileUrl] = useState(false);
|
||||||
|
|
||||||
const tasks = data.control.tasks?.edges.map((edge) => edge.node) || [];
|
const tasks = data.control.tasks?.edges.map((edge) => edge.node) || [];
|
||||||
|
|
||||||
const getEvidenceConnectionId = useCallback(
|
const getEvidenceConnectionId = useCallback(
|
||||||
@@ -537,6 +562,54 @@ function ControlOverviewPageContent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Simplified preview handler that opens the modal and sets the evidence
|
||||||
|
const handlePreviewEvidence = (evidence: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
}) => {
|
||||||
|
// Just open the modal with the evidence info
|
||||||
|
setPreviewEvidence({
|
||||||
|
id: evidence.id,
|
||||||
|
filename: evidence.filename,
|
||||||
|
mimeType: evidence.mimeType,
|
||||||
|
});
|
||||||
|
setIsPreviewModalOpen(true);
|
||||||
|
|
||||||
|
// We'll fetch the fileUrl when the modal opens
|
||||||
|
setIsLoadingFileUrl(true);
|
||||||
|
|
||||||
|
// Use GraphQL query to fetch the fileUrl
|
||||||
|
fetchQuery(environment, getEvidenceFileUrlQuery, {
|
||||||
|
evidenceId: evidence.id,
|
||||||
|
})
|
||||||
|
.toPromise()
|
||||||
|
.then((response) => {
|
||||||
|
// Type assertion for the response
|
||||||
|
const data = response as { node?: { id: string; fileUrl?: string } };
|
||||||
|
|
||||||
|
if (data?.node?.fileUrl) {
|
||||||
|
setPreviewEvidence((prev) => {
|
||||||
|
if (!prev) return null;
|
||||||
|
return { ...prev, fileUrl: data.node!.fileUrl };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("File URL not available in response");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching file URL:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error fetching file URL",
|
||||||
|
description: error.message || "Could not load the file preview",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoadingFileUrl(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -829,27 +902,38 @@ function ControlOverviewPageContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{evidence.mimeType.startsWith("image/") && (
|
{evidence.mimeType.startsWith("image/") ? (
|
||||||
<a
|
<button
|
||||||
href={evidence.fileUrl}
|
onClick={() =>
|
||||||
target="_blank"
|
handlePreviewEvidence(evidence)
|
||||||
rel="noopener noreferrer"
|
}
|
||||||
className="p-1 rounded-full hover:bg-gray-100"
|
className="p-1 rounded-full hover:bg-gray-100"
|
||||||
title="Preview"
|
title="Preview Image"
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4 text-gray-600" />
|
<Eye className="w-4 h-4 text-gray-600" />
|
||||||
</a>
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePreviewEvidence(evidence);
|
||||||
|
}}
|
||||||
|
className="p-1 rounded-full hover:bg-gray-100"
|
||||||
|
title="View File"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
<a
|
<button
|
||||||
href={evidence.fileUrl}
|
onClick={(e) => {
|
||||||
target="_blank"
|
e.preventDefault();
|
||||||
rel="noopener noreferrer"
|
handlePreviewEvidence(evidence);
|
||||||
|
}}
|
||||||
className="p-1 rounded-full hover:bg-gray-100"
|
className="p-1 rounded-full hover:bg-gray-100"
|
||||||
title="Download"
|
title="Download"
|
||||||
download
|
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4 text-gray-600" />
|
<Download className="w-4 h-4 text-gray-600" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -942,6 +1026,85 @@ function ControlOverviewPageContent({
|
|||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Add the Preview Modal */}
|
||||||
|
<Dialog open={isPreviewModalOpen} onOpenChange={setIsPreviewModalOpen}>
|
||||||
|
<DialogContent className="sm:max-w-4xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center justify-between">
|
||||||
|
<span>{previewEvidence?.filename}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsPreviewModalOpen(false)}
|
||||||
|
className="rounded-full p-1 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[300px] bg-gray-50 rounded-md p-4">
|
||||||
|
{isLoadingFileUrl ? (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||||
|
<p className="text-gray-500">Loading preview...</p>
|
||||||
|
</div>
|
||||||
|
) : previewEvidence?.fileUrl ? (
|
||||||
|
previewEvidence.mimeType.startsWith("image/") ? (
|
||||||
|
<img
|
||||||
|
src={previewEvidence.fileUrl}
|
||||||
|
alt={previewEvidence.filename}
|
||||||
|
className="max-h-[70vh] object-contain"
|
||||||
|
/>
|
||||||
|
) : previewEvidence.mimeType.includes("pdf") ? (
|
||||||
|
<iframe
|
||||||
|
src={previewEvidence.fileUrl}
|
||||||
|
className="w-full h-[70vh]"
|
||||||
|
title={previewEvidence.filename}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<FileGeneric className="w-16 h-16 text-gray-400" />
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Preview not available for this file type
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (previewEvidence?.fileUrl) {
|
||||||
|
window.open(previewEvidence.fileUrl, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download File
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<FileGeneric className="w-12 h-12 text-gray-400" />
|
||||||
|
<p className="text-gray-500">Failed to load preview</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsPreviewModalOpen(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
{previewEvidence?.fileUrl && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (previewEvidence?.fileUrl) {
|
||||||
|
window.open(previewEvidence.fileUrl, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
134
apps/console/src/pages/__generated__/ControlOverviewPageGetEvidenceFileUrlQuery.graphql.ts
generated
Normal file
134
apps/console/src/pages/__generated__/ControlOverviewPageGetEvidenceFileUrlQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<4cf4f895bdcd936c74cb70f562210df7>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type ControlOverviewPageGetEvidenceFileUrlQuery$variables = {
|
||||||
|
evidenceId: string;
|
||||||
|
};
|
||||||
|
export type ControlOverviewPageGetEvidenceFileUrlQuery$data = {
|
||||||
|
readonly node: {
|
||||||
|
readonly fileUrl?: string;
|
||||||
|
readonly id?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlOverviewPageGetEvidenceFileUrlQuery = {
|
||||||
|
response: ControlOverviewPageGetEvidenceFileUrlQuery$data;
|
||||||
|
variables: ControlOverviewPageGetEvidenceFileUrlQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "evidenceId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "evidenceId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "fileUrl",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"type": "Evidence",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
||||||
|
"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*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"type": "Evidence",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "debb5fda7cec88257a50998c78ee1e33",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query ControlOverviewPageGetEvidenceFileUrlQuery(\n $evidenceId: ID!\n) {\n node(id: $evidenceId) {\n __typename\n ... on Evidence {\n id\n fileUrl\n }\n id\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "a97d9390a7415ff87916e268458f5ead";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<005ee5181eeb4c0865e7e76dcb6b3aac>>
|
* @generated SignedSource<<3c761f476fc4dcbc2f175eefa625bb28>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -31,8 +31,7 @@ export type ControlOverviewPageQuery$data = {
|
|||||||
readonly __id: string;
|
readonly __id: string;
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly createdAt: string;
|
readonly createdAt: any;
|
||||||
readonly fileUrl: string;
|
|
||||||
readonly filename: string;
|
readonly filename: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly mimeType: string;
|
readonly mimeType: string;
|
||||||
@@ -173,13 +172,6 @@ v11 = [
|
|||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v2/*: any*/),
|
(v2/*: any*/),
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "fileUrl",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -421,7 +413,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "0747630c87fd59b419e5264fcede33bf",
|
"cacheID": "4364cf9b985fb746dde62c1e94063390",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -444,11 +436,11 @@ return {
|
|||||||
},
|
},
|
||||||
"name": "ControlOverviewPageQuery",
|
"name": "ControlOverviewPageQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n evidences(first: 50) {\n edges {\n node {\n id\n fileUrl\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "0fc29eb31e33b025168ac8972109f853";
|
(node as any).hash = "b91e90b278ccedc6b44d3a3c53593e73";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<f1cdd20d14ca9fefe868019e5aa43741>>
|
* @generated SignedSource<<09af19b5a7657e04606a42962db9db2c>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -23,7 +23,7 @@ export type ControlOverviewPageUploadEvidenceMutation$data = {
|
|||||||
readonly uploadEvidence: {
|
readonly uploadEvidence: {
|
||||||
readonly evidenceEdge: {
|
readonly evidenceEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly createdAt: string;
|
readonly createdAt: any;
|
||||||
readonly fileUrl: string;
|
readonly fileUrl: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly mimeType: string;
|
readonly mimeType: string;
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ type EvidenceEdge {
|
|||||||
|
|
||||||
type Evidence implements Node {
|
type Evidence implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
fileUrl: String!
|
fileUrl: String! @goField(forceResolver: true)
|
||||||
mimeType: String!
|
mimeType: String!
|
||||||
size: Int!
|
size: Int!
|
||||||
state: EvidenceState!
|
state: EvidenceState!
|
||||||
|
|||||||
@@ -382,6 +382,8 @@ type ControlResolver interface {
|
|||||||
Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error)
|
Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error)
|
||||||
}
|
}
|
||||||
type EvidenceResolver interface {
|
type EvidenceResolver interface {
|
||||||
|
FileURL(ctx context.Context, obj *types.Evidence) (string, error)
|
||||||
|
|
||||||
StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error)
|
StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error)
|
||||||
}
|
}
|
||||||
type FrameworkResolver interface {
|
type FrameworkResolver interface {
|
||||||
@@ -2188,7 +2190,7 @@ type EvidenceEdge {
|
|||||||
|
|
||||||
type Evidence implements Node {
|
type Evidence implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
fileUrl: String!
|
fileUrl: String! @goField(forceResolver: true)
|
||||||
mimeType: String!
|
mimeType: String!
|
||||||
size: Int!
|
size: Int!
|
||||||
state: EvidenceState!
|
state: EvidenceState!
|
||||||
@@ -5184,7 +5186,7 @@ func (ec *executionContext) _Evidence_fileUrl(ctx context.Context, field graphql
|
|||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
ctx = rctx // use context from middleware stack in children
|
ctx = rctx // use context from middleware stack in children
|
||||||
return obj.FileURL, nil
|
return ec.resolvers.Evidence().FileURL(rctx, obj)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
@@ -5205,8 +5207,8 @@ func (ec *executionContext) fieldContext_Evidence_fileUrl(_ context.Context, fie
|
|||||||
fc = &graphql.FieldContext{
|
fc = &graphql.FieldContext{
|
||||||
Object: "Evidence",
|
Object: "Evidence",
|
||||||
Field: field,
|
Field: field,
|
||||||
IsMethod: false,
|
IsMethod: true,
|
||||||
IsResolver: false,
|
IsResolver: true,
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
return nil, errors.New("field of type String does not have child fields")
|
return nil, errors.New("field of type String does not have child fields")
|
||||||
},
|
},
|
||||||
@@ -14061,10 +14063,36 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
|
|||||||
atomic.AddUint32(&out.Invalids, 1)
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
}
|
}
|
||||||
case "fileUrl":
|
case "fileUrl":
|
||||||
out.Values[i] = ec._Evidence_fileUrl(ctx, field, obj)
|
field := field
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
atomic.AddUint32(&out.Invalids, 1)
|
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
||||||
|
res = ec._Evidence_fileUrl(ctx, field, obj)
|
||||||
|
if res == graphql.Null {
|
||||||
|
atomic.AddUint32(&fs.Invalids, 1)
|
||||||
}
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Deferrable != nil {
|
||||||
|
dfs, ok := deferred[field.Deferrable.Label]
|
||||||
|
di := 0
|
||||||
|
if ok {
|
||||||
|
dfs.AddField(field)
|
||||||
|
di = len(dfs.Values) - 1
|
||||||
|
} else {
|
||||||
|
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
|
||||||
|
deferred[field.Deferrable.Label] = dfs
|
||||||
|
}
|
||||||
|
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
|
||||||
|
return innerFunc(ctx, dfs)
|
||||||
|
})
|
||||||
|
|
||||||
|
// don't run the out.Concurrently() call below
|
||||||
|
out.Values[i] = graphql.Null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||||
case "mimeType":
|
case "mimeType":
|
||||||
out.Values[i] = ec._Evidence_mimeType(ctx, field, obj)
|
out.Values[i] = ec._Evidence_mimeType(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package console_v1
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/api/console/v1/schema"
|
"github.com/getprobo/probo/pkg/api/console/v1/schema"
|
||||||
"github.com/getprobo/probo/pkg/api/console/v1/types"
|
"github.com/getprobo/probo/pkg/api/console/v1/types"
|
||||||
@@ -41,6 +42,16 @@ func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *
|
|||||||
return types.NewTaskConnection(page), nil
|
return types.NewTaskConnection(page), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileURL is the resolver for the fileUrl field.
|
||||||
|
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (string, error) {
|
||||||
|
fileURL, err := r.proboSvc.GetEvidenceFileURL(ctx, obj.ID, 15*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot generate file URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *fileURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StateTransisions is the resolver for the stateTransisions field.
|
// StateTransisions is the resolver for the stateTransisions field.
|
||||||
func (r *evidenceResolver) StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error) {
|
func (r *evidenceResolver) StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|||||||
52
pkg/probo/get_evidence_file_url.go
Normal file
52
pkg/probo/get_evidence_file_url.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package probo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s Service) GetEvidenceFileURL(
|
||||||
|
ctx context.Context,
|
||||||
|
evidenceID gid.GID,
|
||||||
|
expiresIn time.Duration,
|
||||||
|
) (*string, error) {
|
||||||
|
evidence, err := s.GetEvidence(ctx, evidenceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get evidence: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
presignClient := s3.NewPresignClient(s.s3)
|
||||||
|
|
||||||
|
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(evidence.ObjectKey),
|
||||||
|
ResponseContentType: aws.String(evidence.MimeType),
|
||||||
|
ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", evidence.Filename)),
|
||||||
|
}, func(opts *s3.PresignOptions) {
|
||||||
|
opts.Expires = expiresIn
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot presign GetObject request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &presignedReq.URL, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user