@@ -54,7 +54,9 @@ function CreatePeoplePageContent({
|
||||
const environment = useRelayEnvironment();
|
||||
const data = usePreloadedQuery(createPeoplePageQuery, queryRef);
|
||||
const [createPeople, isCreatingPeople] = useMutation(createPeopleMutation);
|
||||
const [kind, setKind] = useState<'EMPLOYEE' | 'CONTRACTOR' | 'VENDOR'>('EMPLOYEE');
|
||||
const [kind, setKind] = useState<"EMPLOYEE" | "CONTRACTOR" | "VENDOR">(
|
||||
"EMPLOYEE",
|
||||
);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -64,8 +66,8 @@ function CreatePeoplePageContent({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: data.node.id,
|
||||
fullName: formData.get('fullName') as string,
|
||||
primaryEmailAddress: formData.get('primaryEmailAddress') as string,
|
||||
fullName: formData.get("fullName") as string,
|
||||
primaryEmailAddress: formData.get("primaryEmailAddress") as string,
|
||||
kind,
|
||||
},
|
||||
},
|
||||
@@ -77,8 +79,8 @@ function CreatePeoplePageContent({
|
||||
organization.invalidateRecord();
|
||||
}
|
||||
});
|
||||
|
||||
navigate('/peoples');
|
||||
|
||||
navigate("/peoples");
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -91,7 +93,9 @@ function CreatePeoplePageContent({
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Create People</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Create People
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add a new person to your organization.
|
||||
</p>
|
||||
@@ -145,7 +149,7 @@ function CreatePeoplePageContent({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate('/peoples')}
|
||||
onClick={() => navigate("/peoples")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -162,7 +166,9 @@ function CreatePeoplePageContent({
|
||||
}
|
||||
|
||||
export default function CreatePeoplePage() {
|
||||
const [queryRef, loadQuery] = useQueryLoader<CreatePeoplePageQueryType>(createPeoplePageQuery);
|
||||
const [queryRef, loadQuery] = useQueryLoader<CreatePeoplePageQueryType>(
|
||||
createPeoplePageQuery,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({});
|
||||
@@ -177,4 +183,4 @@ export default function CreatePeoplePage() {
|
||||
<CreatePeoplePageContent queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,4 @@ export function ErrorPage({
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,16 +46,19 @@ function FrameworkListPageContent({
|
||||
queryRef: PreloadedQuery<FrameworkListPageQueryType>;
|
||||
}) {
|
||||
const data = usePreloadedQuery(FrameworkListPageQuery, queryRef);
|
||||
const frameworks = data.node.frameworks?.edges.map(edge => edge?.node) ?? [];
|
||||
const frameworks =
|
||||
data.node.frameworks?.edges.map((edge) => edge?.node) ?? [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-semibold mb-2">Framework</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Compliance frameworks like SOC 2 and ISO 27001 provide guidelines to secure sensitive data, manage risks, and
|
||||
build trust. SOC 2 focuses on customer data protection via five Trust Principles, while ISO 27001 establishes
|
||||
a comprehensive Information Security Management System for global standards.
|
||||
Compliance frameworks like SOC 2 and ISO 27001 provide guidelines to
|
||||
secure sensitive data, manage risks, and build trust. SOC 2 focuses on
|
||||
customer data protection via five Trust Principles, while ISO 27001
|
||||
establishes a comprehensive Information Security Management System for
|
||||
global standards.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -70,9 +73,13 @@ function FrameworkListPageContent({
|
||||
</Badge>
|
||||
<div className="bg-green-500/10 w-24 h-24 rounded-full flex items-center justify-center mb-4">
|
||||
<Globe2 className="w-12 h-12 text-green-500" />
|
||||
<div className="absolute text-xs font-medium text-green-500">{framework?.name}</div>
|
||||
<div className="absolute text-xs font-medium text-green-500">
|
||||
{framework?.name}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">{framework?.name}</h2>
|
||||
<h2 className="text-xl font-semibold mb-2">
|
||||
{framework?.name}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{framework?.description}
|
||||
</p>
|
||||
@@ -118,7 +125,9 @@ function FrameworkListPageFallback() {
|
||||
}
|
||||
|
||||
export default function FrameworkListPage() {
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(FrameworkListPageQuery);
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
|
||||
FrameworkListPageQuery,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({});
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Progress } from "@/components/ui/progress";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
const FrameworkOverviewPageQuery = graphql`
|
||||
query FrameworkOverviewPageQuery($frameworkId: ID!) {
|
||||
node(id: $frameworkId) {
|
||||
@@ -53,21 +54,29 @@ function FrameworkOverviewPageContent({
|
||||
}) {
|
||||
const data = usePreloadedQuery(FrameworkOverviewPageQuery, queryRef);
|
||||
const framework = data.node;
|
||||
const controls = framework.controls?.edges.map(edge => edge?.node) ?? [];
|
||||
const controls = framework.controls?.edges.map((edge) => edge?.node) ?? [];
|
||||
|
||||
const controlsByCategory = {
|
||||
office: controls.filter(c => c?.name.toLowerCase().includes('office')),
|
||||
computer: controls.filter(c => c?.name.toLowerCase().includes('computer')),
|
||||
employees: controls.filter(c => c?.name.toLowerCase().includes('employee')),
|
||||
email: controls.filter(c => c?.name.toLowerCase().includes('email')),
|
||||
code: controls.filter(c => c?.name.toLowerCase().includes('code')),
|
||||
infrastructure: controls.filter(c => c?.name.toLowerCase().includes('infrastructure')),
|
||||
network: controls.filter(c => c?.name.toLowerCase().includes('network')),
|
||||
data: controls.filter(c => c?.name.toLowerCase().includes('data')),
|
||||
logging: controls.filter(c => c?.name.toLowerCase().includes('log')),
|
||||
incidents: controls.filter(c => c?.name.toLowerCase().includes('incident')),
|
||||
vendors: controls.filter(c => c?.name.toLowerCase().includes('vendor')),
|
||||
sharing: controls.filter(c => c?.name.toLowerCase().includes('share'))
|
||||
office: controls.filter((c) => c?.name.toLowerCase().includes("office")),
|
||||
computer: controls.filter((c) =>
|
||||
c?.name.toLowerCase().includes("computer"),
|
||||
),
|
||||
employees: controls.filter((c) =>
|
||||
c?.name.toLowerCase().includes("employee"),
|
||||
),
|
||||
email: controls.filter((c) => c?.name.toLowerCase().includes("email")),
|
||||
code: controls.filter((c) => c?.name.toLowerCase().includes("code")),
|
||||
infrastructure: controls.filter((c) =>
|
||||
c?.name.toLowerCase().includes("infrastructure"),
|
||||
),
|
||||
network: controls.filter((c) => c?.name.toLowerCase().includes("network")),
|
||||
data: controls.filter((c) => c?.name.toLowerCase().includes("data")),
|
||||
logging: controls.filter((c) => c?.name.toLowerCase().includes("log")),
|
||||
incidents: controls.filter((c) =>
|
||||
c?.name.toLowerCase().includes("incident"),
|
||||
),
|
||||
vendors: controls.filter((c) => c?.name.toLowerCase().includes("vendor")),
|
||||
sharing: controls.filter((c) => c?.name.toLowerCase().includes("share")),
|
||||
};
|
||||
|
||||
const controlCards = [
|
||||
@@ -75,19 +84,25 @@ function FrameworkOverviewPageContent({
|
||||
icon: <Building2 className="w-4 h-4" />,
|
||||
title: "Secure your offices and internet access",
|
||||
controls: controlsByCategory.office,
|
||||
completed: controlsByCategory.office.filter(c => c?.state === 'IMPLEMENTED').length,
|
||||
completed: controlsByCategory.office.filter(
|
||||
(c) => c?.state === "IMPLEMENTED",
|
||||
).length,
|
||||
total: controlsByCategory.office.length,
|
||||
},
|
||||
{
|
||||
icon: <Computer className="w-4 h-4" />,
|
||||
title: "Manage your computers",
|
||||
controls: controlsByCategory.computer,
|
||||
completed: controlsByCategory.computer.filter(c => c?.state === 'IMPLEMENTED').length,
|
||||
completed: controlsByCategory.computer.filter(
|
||||
(c) => c?.state === "IMPLEMENTED",
|
||||
).length,
|
||||
total: controlsByCategory.computer.length,
|
||||
},
|
||||
];
|
||||
|
||||
const totalImplemented = controls.filter(c => c?.state === 'IMPLEMENTED').length;
|
||||
const totalImplemented = controls.filter(
|
||||
(c) => c?.state === "IMPLEMENTED",
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
@@ -125,7 +140,9 @@ function FrameworkOverviewPageContent({
|
||||
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold">Controls</h2>
|
||||
<div className="text-primary">{totalImplemented} out of {controls.length} validated</div>
|
||||
<div className="text-primary">
|
||||
{totalImplemented} out of {controls.length} validated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@@ -191,7 +208,9 @@ function FrameworkOverviewPageFallback() {
|
||||
|
||||
export default function FrameworkOverviewPage() {
|
||||
const { frameworkId } = useParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(FrameworkOverviewPageQuery);
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(
|
||||
FrameworkOverviewPageQuery,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ frameworkId: frameworkId! });
|
||||
@@ -208,4 +227,3 @@ export default function FrameworkOverviewPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generat
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
const PeopleListPageQuery = graphql`
|
||||
query PeopleListPageQuery($first: Int, $after: CursorKey, $last: Int, $before: CursorKey) {
|
||||
query PeopleListPageQuery(
|
||||
$first: Int
|
||||
$after: CursorKey
|
||||
$last: Int
|
||||
$before: CursorKey
|
||||
) {
|
||||
node(id: "AZSfP_xAcAC5IAAAAAAltA") {
|
||||
id
|
||||
... on Organization {
|
||||
@@ -57,21 +62,26 @@ const deletePeopleMutation = graphql`
|
||||
function PeopleListPageContent({
|
||||
queryRef,
|
||||
onPageChange,
|
||||
loadQuery
|
||||
loadQuery,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<PeopleListPageQueryType>;
|
||||
onPageChange: (params: { first?: number; after?: string; last?: number; before?: string }) => void;
|
||||
onPageChange: (params: {
|
||||
first?: number;
|
||||
after?: string;
|
||||
last?: number;
|
||||
before?: string;
|
||||
}) => void;
|
||||
loadQuery: LoadQueryType;
|
||||
}) {
|
||||
const data = usePreloadedQuery(PeopleListPageQuery, queryRef);
|
||||
const peoples = data.node.peoples?.edges.map(edge => edge?.node) ?? [];
|
||||
const peoples = data.node.peoples?.edges.map((edge) => edge?.node) ?? [];
|
||||
const pageInfo = data.node.peoples?.pageInfo;
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [deletePeople] = useMutation(deletePeopleMutation);
|
||||
|
||||
const handlePageChange = (direction: 'prev' | 'next') => {
|
||||
const handlePageChange = (direction: "prev" | "next") => {
|
||||
startTransition(() => {
|
||||
if (direction === 'prev') {
|
||||
if (direction === "prev") {
|
||||
onPageChange({
|
||||
last: ITEMS_PER_PAGE,
|
||||
before: pageInfo?.startCursor,
|
||||
@@ -96,9 +106,7 @@ function PeopleListPageContent({
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link to="/peoples/create">
|
||||
Create People
|
||||
</Link>
|
||||
<Link to="/peoples/create">Create People</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,14 +121,21 @@ function PeopleListPageContent({
|
||||
<AvatarFallback>{person?.fullName?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{person?.fullName}</p>
|
||||
<p className="text-sm text-muted-foreground">{person?.primaryEmailAddress}</p>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{person?.fullName}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{person?.primaryEmailAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge variant="secondary" className="font-medium">
|
||||
{person?.kind === 'EMPLOYEE' ? 'Employee' :
|
||||
person?.kind === 'CONTRACTOR' ? 'Contractor' : 'Vendor'}
|
||||
{person?.kind === "EMPLOYEE"
|
||||
? "Employee"
|
||||
: person?.kind === "CONTRACTOR"
|
||||
? "Contractor"
|
||||
: "Vendor"}
|
||||
</Badge>
|
||||
<div className="flex gap-1">
|
||||
<CircleUser className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -136,20 +151,27 @@ function PeopleListPageContent({
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (window.confirm('Are you sure you want to delete this person?')) {
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete this person?",
|
||||
)
|
||||
) {
|
||||
deletePeople({
|
||||
variables: {
|
||||
input: {
|
||||
peopleId: person.id
|
||||
}
|
||||
peopleId: person.id,
|
||||
},
|
||||
},
|
||||
onCompleted() {
|
||||
loadQuery({
|
||||
first: ITEMS_PER_PAGE,
|
||||
after: undefined,
|
||||
last: undefined,
|
||||
before: undefined,
|
||||
}, { fetchPolicy: 'network-only' });
|
||||
loadQuery(
|
||||
{
|
||||
first: ITEMS_PER_PAGE,
|
||||
after: undefined,
|
||||
last: undefined,
|
||||
before: undefined,
|
||||
},
|
||||
{ fetchPolicy: "network-only" },
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -164,14 +186,14 @@ function PeopleListPageContent({
|
||||
<div className="flex gap-2 justify-end mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handlePageChange('prev')}
|
||||
onClick={() => handlePageChange("prev")}
|
||||
disabled={isPending || !pageInfo?.hasPreviousPage}
|
||||
>
|
||||
{isPending ? "Loading..." : "Previous"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handlePageChange('next')}
|
||||
onClick={() => handlePageChange("next")}
|
||||
disabled={isPending || !pageInfo?.hasNextPage}
|
||||
>
|
||||
{isPending ? "Loading..." : "Next"}
|
||||
@@ -190,7 +212,7 @@ function PeopleListPageFallback() {
|
||||
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[1,2,3].map((i) => (
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between p-4 rounded-lg border bg-card text-card-foreground shadow-sm"
|
||||
@@ -205,8 +227,11 @@ function PeopleListPageFallback() {
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-6 w-16 bg-muted animate-pulse rounded-full" />
|
||||
<div className="flex gap-1">
|
||||
{[1,2,3].map((j) => (
|
||||
<div key={j} className="h-4 w-4 bg-muted animate-pulse rounded" />
|
||||
{[1, 2, 3].map((j) => (
|
||||
<div
|
||||
key={j}
|
||||
className="h-4 w-4 bg-muted animate-pulse rounded"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-6 w-24 bg-muted animate-pulse rounded-full" />
|
||||
@@ -218,17 +243,20 @@ function PeopleListPageFallback() {
|
||||
);
|
||||
}
|
||||
|
||||
type LoadQueryType = ReturnType<typeof useQueryLoader<PeopleListPageQueryType>>[1];
|
||||
type LoadQueryType = ReturnType<
|
||||
typeof useQueryLoader<PeopleListPageQueryType>
|
||||
>[1];
|
||||
|
||||
export default function PeopleListPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<PeopleListPageQueryType>(PeopleListPageQuery);
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<PeopleListPageQueryType>(PeopleListPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
const after = searchParams.get('after');
|
||||
const before = searchParams.get('before');
|
||||
|
||||
loadQuery({
|
||||
const after = searchParams.get("after");
|
||||
const before = searchParams.get("before");
|
||||
|
||||
loadQuery({
|
||||
first: before ? undefined : ITEMS_PER_PAGE,
|
||||
after: after || undefined,
|
||||
last: before ? ITEMS_PER_PAGE : undefined,
|
||||
@@ -236,15 +264,25 @@ export default function PeopleListPage() {
|
||||
});
|
||||
}, [loadQuery, searchParams]);
|
||||
|
||||
const handlePageChange = ({ first, after, last, before }: { first?: number; after?: string; last?: number; before?: string }) => {
|
||||
const handlePageChange = ({
|
||||
first,
|
||||
after,
|
||||
last,
|
||||
before,
|
||||
}: {
|
||||
first?: number;
|
||||
after?: string;
|
||||
last?: number;
|
||||
before?: string;
|
||||
}) => {
|
||||
loadQuery(
|
||||
{
|
||||
{
|
||||
first,
|
||||
after,
|
||||
last,
|
||||
before,
|
||||
},
|
||||
{ fetchPolicy: 'network-only' }
|
||||
{ fetchPolicy: "network-only" },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -258,8 +296,12 @@ export default function PeopleListPage() {
|
||||
<title>People - Probo Console</title>
|
||||
</Helmet>
|
||||
<Suspense fallback={<PeopleListPageFallback />}>
|
||||
<PeopleListPageContent queryRef={queryRef} onPageChange={handlePageChange} loadQuery={loadQuery} />
|
||||
<PeopleListPageContent
|
||||
queryRef={queryRef}
|
||||
onPageChange={handlePageChange}
|
||||
loadQuery={loadQuery}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { Mail, Building2, Upload, MoreVertical } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { Suspense, useEffect } from "react"
|
||||
import { Mail, Building2, Upload, MoreVertical } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
} from "react-relay"
|
||||
import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__/SettingsPageQuery.graphql"
|
||||
} from "react-relay";
|
||||
import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__/SettingsPageQuery.graphql";
|
||||
|
||||
const settingsPageQuery = graphql`
|
||||
query SettingsPageQuery {
|
||||
@@ -23,7 +34,7 @@ const settingsPageQuery = graphql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
interface Member {
|
||||
id: string;
|
||||
@@ -46,13 +57,17 @@ function SettingsPageContent({
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-4xl font-medium tracking-tight">Settings</h1>
|
||||
<p className="text-lg text-muted-foreground">Manage your details and personal preferences here.</p>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Manage your details and personal preferences here.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>User & Organisation information</CardTitle>
|
||||
<CardDescription>Publish your trust page to the web</CardDescription>
|
||||
<CardDescription>
|
||||
Publish your trust page to the web
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
@@ -60,10 +75,14 @@ function SettingsPageContent({
|
||||
<div className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">This is your email to connect to Probo</span>
|
||||
<span className="text-muted-foreground">
|
||||
This is your email to connect to Probo
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-green-600">john.doe@example.com</span>
|
||||
<span className="text-sm text-green-600">
|
||||
john.doe@example.com
|
||||
</span>
|
||||
<Button variant="outline" size="sm">
|
||||
Edit
|
||||
</Button>
|
||||
@@ -76,7 +95,11 @@ function SettingsPageContent({
|
||||
<div className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
{organization.logoUrl ? (
|
||||
<img src={organization.logoUrl} alt="Logo" className="h-10 w-10 rounded-lg object-cover" />
|
||||
<img
|
||||
src={organization.logoUrl}
|
||||
alt="Logo"
|
||||
className="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg border">
|
||||
<Upload className="h-5 w-5 text-muted-foreground" />
|
||||
@@ -95,7 +118,9 @@ function SettingsPageContent({
|
||||
<div className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Set the name of the organization</span>
|
||||
<span className="text-muted-foreground">
|
||||
Set the name of the organization
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">{organization.name}</span>
|
||||
@@ -115,23 +140,37 @@ function SettingsPageContent({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Workspace members</CardTitle>
|
||||
<CardDescription>Manage who has privileged access to your workspace and their permissions.</CardDescription>
|
||||
<CardDescription>
|
||||
Manage who has privileged access to your workspace and their
|
||||
permissions.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{members.map((member) => (
|
||||
<div key={member.id} className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center justify-between rounded-lg border p-3 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarFallback>{member.fullName.charAt(0)}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{member.fullName.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{member.fullName}</span>
|
||||
<span className="text-sm text-muted-foreground">{member.primaryEmailAddress}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{member.fullName}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{member.primaryEmailAddress}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{member.role}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{member.role}
|
||||
</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
@@ -141,7 +180,9 @@ function SettingsPageContent({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Change role</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">Remove member</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
Remove member
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
@@ -152,7 +193,7 @@ function SettingsPageContent({
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsPageFallback() {
|
||||
@@ -172,7 +213,8 @@ function SettingsPageFallback() {
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [queryRef, loadQuery] = useQueryLoader<SettingsPageQueryType>(settingsPageQuery);
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<SettingsPageQueryType>(settingsPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({});
|
||||
@@ -192,4 +234,4 @@ export default function SettingsPage() {
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@ import { Helmet } from "react-helmet-async";
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
const vendorListPageQuery = graphql`
|
||||
query VendorListPageQuery($first: Int, $after: CursorKey, $last: Int, $before: CursorKey) {
|
||||
query VendorListPageQuery(
|
||||
$first: Int
|
||||
$after: CursorKey
|
||||
$last: Int
|
||||
$before: CursorKey
|
||||
) {
|
||||
node(id: "AZSfP_xAcAC5IAAAAAAltA") {
|
||||
id
|
||||
... on Organization {
|
||||
@@ -64,34 +69,45 @@ const deleteVendorMutation = graphql`
|
||||
|
||||
// TODO: Remove this once we have a real list of vendors
|
||||
const vendorsList = [
|
||||
{ id: '1', name: 'Amazon Web Services', createdAt: new Date().toISOString() },
|
||||
{ id: '2', name: 'Google Cloud Platform', createdAt: new Date().toISOString() },
|
||||
{ id: '3', name: 'Microsoft Azure', createdAt: new Date().toISOString() },
|
||||
{ id: '4', name: 'Salesforce', createdAt: new Date().toISOString() },
|
||||
{ id: '5', name: 'Slack', createdAt: new Date().toISOString() },
|
||||
{ id: '6', name: 'Zoom', createdAt: new Date().toISOString() },
|
||||
{ id: '7', name: 'Dropbox', createdAt: new Date().toISOString() },
|
||||
{ id: '8', name: 'Trello', createdAt: new Date().toISOString() },
|
||||
{ id: '9', name: 'Asana', createdAt: new Date().toISOString() },
|
||||
{ id: '10', name: 'Notion', createdAt: new Date().toISOString() },
|
||||
{ id: '11', name: 'GitHub', createdAt: new Date().toISOString() },
|
||||
{ id: '12', name: 'GitLab', createdAt: new Date().toISOString() },
|
||||
{ id: '13', name: 'Bitbucket', createdAt: new Date().toISOString() },
|
||||
{ id: '14', name: 'Docker', createdAt: new Date().toISOString() },
|
||||
{ id: '15', name: 'Kubernetes', createdAt: new Date().toISOString() },
|
||||
{ id: '16', name: 'Jenkins', createdAt: new Date().toISOString() },
|
||||
{ id: '17', name: 'CircleCI', createdAt: new Date().toISOString() },
|
||||
{ id: "1", name: "Amazon Web Services", createdAt: new Date().toISOString() },
|
||||
{
|
||||
id: "2",
|
||||
name: "Google Cloud Platform",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{ id: "3", name: "Microsoft Azure", createdAt: new Date().toISOString() },
|
||||
{ id: "4", name: "Salesforce", createdAt: new Date().toISOString() },
|
||||
{ id: "5", name: "Slack", createdAt: new Date().toISOString() },
|
||||
{ id: "6", name: "Zoom", createdAt: new Date().toISOString() },
|
||||
{ id: "7", name: "Dropbox", createdAt: new Date().toISOString() },
|
||||
{ id: "8", name: "Trello", createdAt: new Date().toISOString() },
|
||||
{ id: "9", name: "Asana", createdAt: new Date().toISOString() },
|
||||
{ id: "10", name: "Notion", createdAt: new Date().toISOString() },
|
||||
{ id: "11", name: "GitHub", createdAt: new Date().toISOString() },
|
||||
{ id: "12", name: "GitLab", createdAt: new Date().toISOString() },
|
||||
{ id: "13", name: "Bitbucket", createdAt: new Date().toISOString() },
|
||||
{ id: "14", name: "Docker", createdAt: new Date().toISOString() },
|
||||
{ id: "15", name: "Kubernetes", createdAt: new Date().toISOString() },
|
||||
{ id: "16", name: "Jenkins", createdAt: new Date().toISOString() },
|
||||
{ id: "17", name: "CircleCI", createdAt: new Date().toISOString() },
|
||||
];
|
||||
|
||||
type LoadQueryType = ReturnType<typeof useQueryLoader<VendorListPageQueryType>>[1];
|
||||
type LoadQueryType = ReturnType<
|
||||
typeof useQueryLoader<VendorListPageQueryType>
|
||||
>[1];
|
||||
|
||||
function VendorListContent({
|
||||
queryRef,
|
||||
onPageChange,
|
||||
loadQuery
|
||||
loadQuery,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<VendorListPageQueryType>;
|
||||
onPageChange: (params: { first?: number; after?: string; last?: number; before?: string }) => void;
|
||||
onPageChange: (params: {
|
||||
first?: number;
|
||||
after?: string;
|
||||
last?: number;
|
||||
before?: string;
|
||||
}) => void;
|
||||
loadQuery: LoadQueryType;
|
||||
}) {
|
||||
const data = usePreloadedQuery(vendorListPageQuery, queryRef);
|
||||
@@ -102,32 +118,33 @@ function VendorListContent({
|
||||
const [createVendor] = useMutation(createVendorMutation);
|
||||
const [deleteVendor] = useMutation(deleteVendorMutation);
|
||||
|
||||
const vendors = data.node?.vendors?.edges?.map(edge => edge?.node) ?? [];
|
||||
const vendors = data.node?.vendors?.edges?.map((edge) => edge?.node) ?? [];
|
||||
const pageInfo = data.node?.vendors?.pageInfo;
|
||||
|
||||
const fuse = new Fuse(vendorsList, {
|
||||
keys: ['name'],
|
||||
keys: ["name"],
|
||||
threshold: 0.3,
|
||||
});
|
||||
|
||||
const handlePageChange = (direction: 'next' | 'prev') => {
|
||||
const handlePageChange = (direction: "next" | "prev") => {
|
||||
if (!pageInfo) return;
|
||||
|
||||
if (direction === 'next' && !pageInfo.hasNextPage) return;
|
||||
if (direction === 'prev' && !pageInfo.hasPreviousPage) return;
|
||||
|
||||
startTransition(() => {
|
||||
const params = direction === 'next'
|
||||
? { first: ITEMS_PER_PAGE, after: pageInfo.endCursor }
|
||||
: { last: ITEMS_PER_PAGE, before: pageInfo.startCursor };
|
||||
|
||||
setSearchParams(prev => {
|
||||
if (direction === 'next') {
|
||||
prev.set('after', pageInfo.endCursor!);
|
||||
prev.delete('before');
|
||||
if (direction === "next" && !pageInfo.hasNextPage) return;
|
||||
if (direction === "prev" && !pageInfo.hasPreviousPage) return;
|
||||
|
||||
startTransition(() => {
|
||||
const params =
|
||||
direction === "next"
|
||||
? { first: ITEMS_PER_PAGE, after: pageInfo.endCursor }
|
||||
: { last: ITEMS_PER_PAGE, before: pageInfo.startCursor };
|
||||
|
||||
setSearchParams((prev) => {
|
||||
if (direction === "next") {
|
||||
prev.set("after", pageInfo.endCursor!);
|
||||
prev.delete("before");
|
||||
} else {
|
||||
prev.set('before', pageInfo.startCursor!);
|
||||
prev.delete('after');
|
||||
prev.set("before", pageInfo.startCursor!);
|
||||
prev.delete("after");
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
@@ -153,7 +170,9 @@ function VendorListContent({
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Add a vendor</p>
|
||||
<p className="text-sm text-muted-foreground">Search and add new vendors</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Search and add new vendors
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -169,7 +188,9 @@ function VendorListContent({
|
||||
if (value.trim() === "") {
|
||||
setFilteredVendors([]);
|
||||
} else {
|
||||
const results = fuse.search(value).map(result => result.item);
|
||||
const results = fuse
|
||||
.search(value)
|
||||
.map((result) => result.item);
|
||||
setFilteredVendors(results);
|
||||
}
|
||||
}}
|
||||
@@ -185,18 +206,21 @@ function VendorListContent({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: data.node.id,
|
||||
name: vendor.name
|
||||
}
|
||||
name: vendor.name,
|
||||
},
|
||||
},
|
||||
onCompleted(response: any) {
|
||||
setSearchTerm("");
|
||||
setFilteredVendors([]);
|
||||
loadQuery({
|
||||
first: ITEMS_PER_PAGE,
|
||||
after: undefined,
|
||||
last: undefined,
|
||||
before: undefined,
|
||||
}, { fetchPolicy: 'network-only' });
|
||||
loadQuery(
|
||||
{
|
||||
first: ITEMS_PER_PAGE,
|
||||
after: undefined,
|
||||
last: undefined,
|
||||
before: undefined,
|
||||
},
|
||||
{ fetchPolicy: "network-only" },
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -219,9 +243,12 @@ function VendorListContent({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{vendors.map((vendor) => (
|
||||
<div key={vendor?.id} className="block hover:bg-accent/50 transition-colors">
|
||||
<div
|
||||
key={vendor?.id}
|
||||
className="block hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border bg-card text-card-foreground shadow-sm">
|
||||
<Link
|
||||
<Link
|
||||
to={`/vendors/${vendor?.id}`}
|
||||
className="flex items-center gap-4 flex-1"
|
||||
>
|
||||
@@ -230,8 +257,13 @@ function VendorListContent({
|
||||
<AvatarFallback>{vendor?.name?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{vendor?.name}</p>
|
||||
<p className="text-sm text-muted-foreground">Vendor since {new Date(vendor?.createdAt).toLocaleDateString()}</p>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{vendor?.name}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Vendor since{" "}
|
||||
{new Date(vendor?.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -247,26 +279,33 @@ function VendorListContent({
|
||||
<Badge variant="outline" className="text-muted-foreground">
|
||||
Not assessed
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (window.confirm('Are you sure you want to delete this vendor?')) {
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete this vendor?",
|
||||
)
|
||||
) {
|
||||
deleteVendor({
|
||||
variables: {
|
||||
input: {
|
||||
vendorId: vendor.id
|
||||
}
|
||||
vendorId: vendor.id,
|
||||
},
|
||||
},
|
||||
onCompleted() {
|
||||
loadQuery({
|
||||
first: ITEMS_PER_PAGE,
|
||||
after: undefined,
|
||||
last: undefined,
|
||||
before: undefined,
|
||||
}, { fetchPolicy: 'network-only' });
|
||||
loadQuery(
|
||||
{
|
||||
first: ITEMS_PER_PAGE,
|
||||
after: undefined,
|
||||
last: undefined,
|
||||
before: undefined,
|
||||
},
|
||||
{ fetchPolicy: "network-only" },
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -282,14 +321,14 @@ function VendorListContent({
|
||||
<div className="flex gap-2 justify-end mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handlePageChange('prev')}
|
||||
onClick={() => handlePageChange("prev")}
|
||||
disabled={isPending || !pageInfo?.hasPreviousPage}
|
||||
>
|
||||
{isPending ? "Loading..." : "Previous"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handlePageChange('next')}
|
||||
onClick={() => handlePageChange("next")}
|
||||
disabled={isPending || !pageInfo?.hasNextPage}
|
||||
>
|
||||
{isPending ? "Loading..." : "Next"}
|
||||
@@ -318,14 +357,15 @@ function VendorListFallback() {
|
||||
|
||||
export default function VendorListPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<VendorListPageQueryType>(vendorListPageQuery);
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<VendorListPageQueryType>(vendorListPageQuery);
|
||||
|
||||
// Initialize with URL params
|
||||
useEffect(() => {
|
||||
const after = searchParams.get('after');
|
||||
const before = searchParams.get('before');
|
||||
|
||||
loadQuery({
|
||||
const after = searchParams.get("after");
|
||||
const before = searchParams.get("before");
|
||||
|
||||
loadQuery({
|
||||
first: before ? undefined : ITEMS_PER_PAGE,
|
||||
after: after || undefined,
|
||||
last: before ? ITEMS_PER_PAGE : undefined,
|
||||
@@ -333,15 +373,25 @@ export default function VendorListPage() {
|
||||
});
|
||||
}, [loadQuery, searchParams]);
|
||||
|
||||
const handlePageChange = ({ first, after, last, before }: { first?: number; after?: string; last?: number; before?: string }) => {
|
||||
const handlePageChange = ({
|
||||
first,
|
||||
after,
|
||||
last,
|
||||
before,
|
||||
}: {
|
||||
first?: number;
|
||||
after?: string;
|
||||
last?: number;
|
||||
before?: string;
|
||||
}) => {
|
||||
loadQuery(
|
||||
{
|
||||
{
|
||||
first,
|
||||
after,
|
||||
last,
|
||||
before,
|
||||
},
|
||||
{ fetchPolicy: 'network-only' }
|
||||
{ fetchPolicy: "network-only" },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -355,10 +405,10 @@ export default function VendorListPage() {
|
||||
<title>Vendors - Probo Console</title>
|
||||
</Helmet>
|
||||
<Suspense fallback={<VendorListFallback />}>
|
||||
<VendorListContent
|
||||
queryRef={queryRef}
|
||||
<VendorListContent
|
||||
queryRef={queryRef}
|
||||
onPageChange={handlePageChange}
|
||||
loadQuery={loadQuery}
|
||||
loadQuery={loadQuery}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { HelpCircle } from "lucide-react"
|
||||
import { graphql, PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay"
|
||||
import { Suspense, useEffect } from "react"
|
||||
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql"
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
} from "react-relay";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
|
||||
import { useParams } from "react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
const VendorOverviewPageQuery = graphql`
|
||||
@@ -22,7 +27,7 @@ const VendorOverviewPageQuery = graphql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
function VendorOverviewPageContent({
|
||||
queryRef,
|
||||
@@ -30,7 +35,7 @@ function VendorOverviewPageContent({
|
||||
queryRef: PreloadedQuery<VendorOverviewPageQueryType>;
|
||||
}) {
|
||||
const data = usePreloadedQuery(VendorOverviewPageQuery, queryRef);
|
||||
const [riskLevel, setRiskLevel] = useState("medium")
|
||||
const [riskLevel, setRiskLevel] = useState("medium");
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-6 lg:p-8">
|
||||
@@ -42,26 +47,39 @@ function VendorOverviewPageContent({
|
||||
alt="Google Cloud"
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
<h1 className="text-xl font-semibold text-gray-900">{data.node?.name}</h1>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
{data.node?.name}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-gray-600">Use this form to assess the risk of using Google Workspace as a vendor.</p>
|
||||
<p className="text-gray-600">
|
||||
Use this form to assess the risk of using Google Workspace as a
|
||||
vendor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">Asset Storage Type</h2>
|
||||
<p className="text-sm text-gray-500">Questions related to the type of assets stored by the vendor.</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Questions related to the type of assets stored by the vendor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Does this vendor store physical or digital assets?</Label>
|
||||
<Label className="text-sm">
|
||||
Does this vendor store physical or digital assets?
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">Digital</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Physical</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
Digital
|
||||
</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
|
||||
Physical
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +90,8 @@ function VendorOverviewPageContent({
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">Data Storage and Handling</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Questions related to the vendor's data storage and handling practices.
|
||||
Questions related to the vendor's data storage and handling
|
||||
practices.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -80,29 +99,43 @@ function VendorOverviewPageContent({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Does this vendor store production data?</Label>
|
||||
<Label className="text-sm">
|
||||
Does this vendor store production data?
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">No</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Yes</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
No
|
||||
</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Is the production data encrypted at rest?</Label>
|
||||
<Label className="text-sm">
|
||||
Is the production data encrypted at rest?
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">No</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Yes</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
No
|
||||
</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Who has access to the production data?</Label>
|
||||
<Label className="text-sm">
|
||||
Who has access to the production data?
|
||||
</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -141,9 +174,12 @@ function VendorOverviewPageContent({
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">Vendor Reliability and Continuity</h2>
|
||||
<h2 className="text-lg font-medium">
|
||||
Vendor Reliability and Continuity
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Evaluating the vendor's reliability and plans for business continuity.
|
||||
Evaluating the vendor's reliability and plans for business
|
||||
continuity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -151,7 +187,9 @@ function VendorOverviewPageContent({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Does the vendor have an uptime commitment?</Label>
|
||||
<Label className="text-sm">
|
||||
Does the vendor have an uptime commitment?
|
||||
</Label>
|
||||
</div>
|
||||
<RadioGroup defaultValue="greater" className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -160,7 +198,9 @@ function VendorOverviewPageContent({
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="between" id="between" />
|
||||
<Label htmlFor="between">Between 99% and 99.9% uptime</Label>
|
||||
<Label htmlFor="between">
|
||||
Between 99% and 99.9% uptime
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="less" id="less" />
|
||||
@@ -176,23 +216,37 @@ function VendorOverviewPageContent({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Does the vendor have a disaster recovery plan?</Label>
|
||||
<Label className="text-sm">
|
||||
Does the vendor have a disaster recovery plan?
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">No</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Yes</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
No
|
||||
</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Impact of a Data Breach or Outage</Label>
|
||||
<Label className="text-sm">
|
||||
Impact of a Data Breach or Outage
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">Low</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">Medium</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">High</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
Low
|
||||
</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
Medium
|
||||
</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
|
||||
High
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,18 +257,26 @@ function VendorOverviewPageContent({
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">Risk level</h2>
|
||||
<p className="text-sm text-gray-500">Based on the responses, the suggested risk level is medium.</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Based on the responses, the suggested risk level is medium.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">Low</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">Medium</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">High</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
Low
|
||||
</button>
|
||||
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
|
||||
Medium
|
||||
</button>
|
||||
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
|
||||
High
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function VendorOverviewPageFallback() {
|
||||
@@ -235,7 +297,9 @@ function VendorOverviewPageFallback() {
|
||||
|
||||
export default function VendorOverviewPage() {
|
||||
const { vendorId } = useParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(VendorOverviewPageQuery);
|
||||
const [queryRef, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(
|
||||
VendorOverviewPageQuery,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ vendorId: vendorId! });
|
||||
@@ -255,4 +319,4 @@ export default function VendorOverviewPage() {
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user