Add business and data impact on list

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-04-23 11:23:28 -07:00
parent b65524d60b
commit 80ad4f643a
8 changed files with 387 additions and 80 deletions

View File

@@ -23,6 +23,12 @@ import { ListVendorViewPaginationQuery } from "./__generated__/ListVendorViewPag
import { ListVendorView_vendors$key } from "./__generated__/ListVendorView_vendors.graphql";
import { ListVendorViewQuery } from "./__generated__/ListVendorViewQuery.graphql";
import { ListVendorViewDeleteVendorMutation } from "./__generated__/ListVendorViewDeleteVendorMutation.graphql";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface VendorData {
name: string;
@@ -86,6 +92,17 @@ const vendorListFragment = graphql`
websiteUrl
createdAt
updatedAt
riskAssessments(first: 1, orderBy: { direction: DESC, field: ASSESSED_AT }) {
edges {
node {
id
assessedAt
expiresAt
dataSensitivity
businessImpact
}
}
}
}
}
pageInfo {
@@ -292,20 +309,42 @@ function ListVendorContent({
switch (riskLevel) {
case "CRITICAL":
return "bg-[#FFEFEF] text-[#CD2B31]";
case "SIGNIFICANT":
case "HIGH":
return "bg-[#FFF1E7] text-[#BD4B00]";
default:
case "MEDIUM":
return "bg-[#FFF8E7] text-[#B54708]";
case "LOW":
return "bg-[#EEFADC] text-[#5D770D]";
case "NONE":
return "bg-[#E6F4FF] text-[#0B4F71]";
default:
return "bg-gray-100 text-gray-600";
}
};
// Mock risk status for demo (since the data doesn't have this field)
const getRiskStatus = (vendorId: string) => {
// Simplistic way to assign different risk levels for demo
const hash = vendorId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
if (hash % 3 === 0) return "CRITICAL";
if (hash % 3 === 1) return "SIGNIFICANT";
return "GENERAL";
// Get risk assessment status
const getRiskAssessmentStatus = (vendor: any) => {
const latestAssessment = vendor.riskAssessments?.edges[0]?.node;
if (!latestAssessment) {
return {
status: "NEEDED",
dataSensitivity: null,
businessImpact: null,
isExpired: false
};
}
const now = new Date();
const expiresAt = new Date(latestAssessment.expiresAt);
const isExpired = expiresAt < now;
return {
status: isExpired ? "EXPIRED" : "VALID",
dataSensitivity: latestAssessment.dataSensitivity,
businessImpact: latestAssessment.businessImpact,
isExpired
};
};
return (
@@ -477,7 +516,7 @@ function ListVendorContent({
</th>
<th className="py-3 px-4 text-left text-xs font-semibold text-gray-500">
<div className="flex items-center gap-2">
Risk
Data Risk
<svg
width="8"
height="8"
@@ -492,7 +531,7 @@ function ListVendorContent({
</th>
<th className="py-3 px-4 text-left text-xs font-semibold text-gray-500">
<div className="flex items-center gap-2">
Compliance status
Business Risk
<svg
width="8"
height="8"
@@ -510,7 +549,7 @@ function ListVendorContent({
</thead>
<tbody>
{vendors.map((vendor) => {
const riskStatus = getRiskStatus(vendor.id);
const riskStatus = getRiskAssessmentStatus(vendor);
return (
<tr
key={vendor.id}
@@ -540,64 +579,78 @@ function ListVendorContent({
{formatDate(vendor.updatedAt)}
</td>
<td className="py-4 px-4">
<Badge className={`${getRiskBadgeStyle(riskStatus)} font-medium text-xs px-2 py-1 rounded-md`}>
{riskStatus === "CRITICAL"
? "Critical"
: riskStatus === "SIGNIFICANT"
? "Medium"
: "General"}
</Badge>
{riskStatus.status === "NEEDED" ? (
<Badge className="bg-gray-100 text-gray-600 font-medium text-xs px-2 py-1 rounded-md">
Assessment needed
</Badge>
) : riskStatus.status === "EXPIRED" ? (
<Badge className="bg-[#FFEFEF] text-[#CD2B31] font-medium text-xs px-2 py-1 rounded-md">
Assessment expired
</Badge>
) : (
<Badge className={`${getRiskBadgeStyle(riskStatus.dataSensitivity)} font-medium text-xs px-2 py-1 rounded-md`}>
{riskStatus.dataSensitivity || "NONE"}
</Badge>
)}
</td>
<td className="py-4 px-4">
<Badge
className={`${
riskStatus === "CRITICAL"
? "bg-[#FFEFEF] text-[#CD2B31]"
: riskStatus === "SIGNIFICANT"
? "bg-[#FFF1E7] text-[#BD4B00]"
: "bg-[#EEFADC] text-[#5D770D]"
} font-medium text-xs px-2 py-1 rounded-md`}
>
{riskStatus === "CRITICAL"
? "Late"
: riskStatus === "SIGNIFICANT"
? "In Progress"
: "Completed"}
</Badge>
{riskStatus.status === "NEEDED" ? (
<Badge className="bg-gray-100 text-gray-600 font-medium text-xs px-2 py-1 rounded-md">
Assessment needed
</Badge>
) : riskStatus.status === "EXPIRED" ? (
<Badge className="bg-[#FFEFEF] text-[#CD2B31] font-medium text-xs px-2 py-1 rounded-md">
Assessment expired
</Badge>
) : (
<Badge className={`${getRiskBadgeStyle(riskStatus.businessImpact)} font-medium text-xs px-2 py-1 rounded-md`}>
{riskStatus.businessImpact || "LOW"}
</Badge>
)}
</td>
<td className="py-4 px-4 text-right">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={(e) => {
e.preventDefault();
// Handle menu click - could be expanded into a dropdown menu
if (
window.confirm(
"Are you sure you want to delete this vendor?"
)
) {
deleteVendor({
variables: {
connections: [vendorsConnection.vendors.__id],
input: {
vendorId: vendor.id,
},
},
onCompleted() {
toast({
title: "Vendor deleted",
description:
"The vendor has been deleted successfully",
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-red-600"
onClick={(e) => {
e.preventDefault();
if (
window.confirm(
"Are you sure you want to delete this vendor?"
)
) {
deleteVendor({
variables: {
connections: [vendorsConnection.vendors.__id],
input: {
vendorId: vendor.id,
},
},
onCompleted() {
toast({
title: "Vendor deleted",
description:
"The vendor has been deleted successfully",
});
},
});
},
});
}
}}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
}
}}
>
Delete vendor
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
);

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<2d5b613cbbbd051c4c1f1561f55bf644>>
* @generated SignedSource<<53f7cbf9132d69012d6e13909a807576>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -231,6 +231,82 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "ASSESSED_AT"
}
}
],
"concreteType": "VendorRiskAssessmentConnection",
"kind": "LinkedField",
"name": "riskAssessments",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessmentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessment",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v11/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "assessedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataSensitivity",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "businessImpact",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})"
},
(v10/*: any*/)
],
"storageKey": null
@@ -320,16 +396,16 @@ return {
]
},
"params": {
"cacheID": "b1055c67d0940ecd9189ec80410a8add",
"cacheID": "bbb819bae323711ced24c6d5ab842906",
"id": null,
"metadata": {},
"name": "ListVendorViewPaginationQuery",
"operationKind": "query",
"text": "query ListVendorViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...ListVendorView_vendors_pbnwq\n id\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
"text": "query ListVendorViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...ListVendorView_vendors_pbnwq\n id\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n createdAt\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "3908e5fd1af7e0cb367a480fc9fc437e";
(node as any).hash = "eb39ac51abe77196431f4f20ac0010a9";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<4d1328e7df631fabe3c8b7dccdf5ffb0>>
* @generated SignedSource<<500e7888f7d464b89781689be541b2c5>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -233,6 +233,82 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "ASSESSED_AT"
}
}
],
"concreteType": "VendorRiskAssessmentConnection",
"kind": "LinkedField",
"name": "riskAssessments",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessmentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessment",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v6/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "assessedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataSensitivity",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "businessImpact",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})"
},
(v11/*: any*/)
],
"storageKey": null
@@ -322,12 +398,12 @@ return {
]
},
"params": {
"cacheID": "f4269cea33bcb3665cfc49bc350cf426",
"cacheID": "7a3b85e5ebe050f67283192623ccd695",
"id": null,
"metadata": {},
"name": "ListVendorViewQuery",
"operationKind": "query",
"text": "query ListVendorViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...ListVendorView_vendors_pbnwq\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
"text": "query ListVendorViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...ListVendorView_vendors_pbnwq\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n createdAt\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e3d4dae05baff068e5f61916fb436e10>>
* @generated SignedSource<<c99ab9d208e0554aff738243c0115a04>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,6 +9,8 @@
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type BusinessImpact = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM";
export type DataSensitivity = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM" | "NONE";
import { FragmentRefs } from "relay-runtime";
export type ListVendorView_vendors$data = {
readonly id: string;
@@ -20,6 +22,17 @@ export type ListVendorView_vendors$data = {
readonly description: string | null | undefined;
readonly id: string;
readonly name: string;
readonly riskAssessments: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly assessedAt: string;
readonly businessImpact: BusinessImpact;
readonly dataSensitivity: DataSensitivity;
readonly expiresAt: string;
readonly id: string;
};
}>;
};
readonly updatedAt: string;
readonly websiteUrl: string | null | undefined;
};
@@ -175,6 +188,82 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "ASSESSED_AT"
}
}
],
"concreteType": "VendorRiskAssessmentConnection",
"kind": "LinkedField",
"name": "riskAssessments",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessmentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessment",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "assessedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataSensitivity",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "businessImpact",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "riskAssessments(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"ASSESSED_AT\"})"
},
{
"alias": null,
"args": null,
@@ -256,6 +345,6 @@ return {
};
})();
(node as any).hash = "3908e5fd1af7e0cb367a480fc9fc437e";
(node as any).hash = "eb39ac51abe77196431f4f20ac0010a9";
export default node;

View File

@@ -52,6 +52,8 @@ func (v VendorRiskAssessment) CursorKey(orderBy VendorRiskAssessmentOrderField)
return page.NewCursorKey(v.ID, v.CreatedAt)
case VendorRiskAssessmentOrderFieldExpiresAt:
return page.NewCursorKey(v.ID, v.ExpiresAt)
case VendorRiskAssessmentOrderFieldAssessedAt:
return page.NewCursorKey(v.ID, v.AssessedAt)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))

View File

@@ -19,8 +19,9 @@ type (
)
const (
VendorRiskAssessmentOrderFieldCreatedAt VendorRiskAssessmentOrderField = "CREATED_AT"
VendorRiskAssessmentOrderFieldExpiresAt VendorRiskAssessmentOrderField = "EXPIRES_AT"
VendorRiskAssessmentOrderFieldCreatedAt VendorRiskAssessmentOrderField = "CREATED_AT"
VendorRiskAssessmentOrderFieldExpiresAt VendorRiskAssessmentOrderField = "EXPIRES_AT"
VendorRiskAssessmentOrderFieldAssessedAt VendorRiskAssessmentOrderField = "ASSESSED_AT"
)
func (p VendorRiskAssessmentOrderField) Column() string {

View File

@@ -1479,6 +1479,10 @@ enum VendorRiskAssessmentOrderField
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorRiskAssessmentOrderFieldExpiresAt"
)
ASSESSED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorRiskAssessmentOrderFieldAssessedAt"
)
}
input CreateVendorRiskAssessmentInput {

View File

@@ -4863,6 +4863,10 @@ enum VendorRiskAssessmentOrderField
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorRiskAssessmentOrderFieldExpiresAt"
)
ASSESSED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorRiskAssessmentOrderFieldAssessedAt"
)
}
input CreateVendorRiskAssessmentInput {
@@ -37075,12 +37079,14 @@ func (ec *executionContext) marshalNVendorRiskAssessmentOrderField2githubᚗcom
var (
unmarshalNVendorRiskAssessmentOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐVendorRiskAssessmentOrderField = map[string]coredata.VendorRiskAssessmentOrderField{
"CREATED_AT": coredata.VendorRiskAssessmentOrderFieldCreatedAt,
"EXPIRES_AT": coredata.VendorRiskAssessmentOrderFieldExpiresAt,
"CREATED_AT": coredata.VendorRiskAssessmentOrderFieldCreatedAt,
"EXPIRES_AT": coredata.VendorRiskAssessmentOrderFieldExpiresAt,
"ASSESSED_AT": coredata.VendorRiskAssessmentOrderFieldAssessedAt,
}
marshalNVendorRiskAssessmentOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐVendorRiskAssessmentOrderField = map[coredata.VendorRiskAssessmentOrderField]string{
coredata.VendorRiskAssessmentOrderFieldCreatedAt: "CREATED_AT",
coredata.VendorRiskAssessmentOrderFieldExpiresAt: "EXPIRES_AT",
coredata.VendorRiskAssessmentOrderFieldCreatedAt: "CREATED_AT",
coredata.VendorRiskAssessmentOrderFieldExpiresAt: "EXPIRES_AT",
coredata.VendorRiskAssessmentOrderFieldAssessedAt: "ASSESSED_AT",
}
)