Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-12 17:09:39 -08:00
parent 31e65065ab
commit 1b25601a01
17 changed files with 549 additions and 306 deletions

View File

@@ -26,7 +26,9 @@ const NotFoundPage = lazy(() => import("./pages/NotFoundPage"));
const PeopleListPage = lazy(() => import("./pages/PeopleListPage")); const PeopleListPage = lazy(() => import("./pages/PeopleListPage"));
const VendorListPage = lazy(() => import("./pages/VendorListPage")); const VendorListPage = lazy(() => import("./pages/VendorListPage"));
const FrameworkListPage = lazy(() => import("./pages/FrameworkListPage")); const FrameworkListPage = lazy(() => import("./pages/FrameworkListPage"));
const FrameworkOverviewPage = lazy(() => import("./pages/FrameworkOverviewPage")); const FrameworkOverviewPage = lazy(
() => import("./pages/FrameworkOverviewPage"),
);
const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage")); const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage")); const SettingsPage = lazy(() => import("./pages/SettingsPage"));
const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage")); const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage"));
@@ -147,7 +149,11 @@ function App() {
); );
} }
function ErrorBoundaryWithLocation({ children }: { children: React.ReactNode }) { function ErrorBoundaryWithLocation({
children,
}: {
children: React.ReactNode;
}) {
const location = useLocation(); const location = useLocation();
return <ErrorBoundary key={location.pathname}>{children}</ErrorBoundary>; return <ErrorBoundary key={location.pathname}>{children}</ErrorBoundary>;
} }

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -20,8 +20,8 @@ const badgeVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <div className={cn(badgeVariants({ variant }), className)} {...props} />
) );
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Card = React.forwardRef< const Card = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-xl border bg-card text-card-foreground shadow", "rounded-xl border bg-card text-card-foreground shadow",
className className,
)} )}
{...props} {...props}
/> />
)) ));
Card.displayName = "Card" Card.displayName = "Card";
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)} className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} {...props}
/> />
)) ));
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
className={cn("font-semibold leading-none tracking-tight", className)} className={cn("font-semibold leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ));
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef< const CardContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)) ));
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)} className={cn("flex items-center p-6 pt-0", className)}
{...props} {...props}
/> />
)) ));
CardFooter.displayName = "CardFooter" CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react" import { Check } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -12,7 +12,7 @@ const Checkbox = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className className,
)} )}
{...props} {...props}
> >
@@ -22,7 +22,7 @@ const Checkbox = React.forwardRef<
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
)) ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox } export { Checkbox };

View File

@@ -1,12 +1,12 @@
import * as React from "react" import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
) );
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
@@ -18,7 +18,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)} className={cn(labelVariants(), className)}
{...props} {...props}
/> />
)) ));
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName;
export { Label } export { Label };

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
@@ -11,7 +11,7 @@ const Progress = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20", "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className className,
)} )}
{...props} {...props}
> >
@@ -20,7 +20,7 @@ const Progress = React.forwardRef<
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
)) ));
Progress.displayName = ProgressPrimitive.Root.displayName Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress } export { Progress };

View File

@@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react" import { Circle } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef< const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>, React.ElementRef<typeof RadioGroupPrimitive.Root>,
@@ -14,9 +14,9 @@ const RadioGroup = React.forwardRef<
{...props} {...props}
ref={ref} ref={ref}
/> />
) );
}) });
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef< const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>, React.ElementRef<typeof RadioGroupPrimitive.Item>,
@@ -27,7 +27,7 @@ const RadioGroupItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -35,8 +35,8 @@ const RadioGroupItem = React.forwardRef<
<Circle className="h-2.5 w-2.5 fill-current text-current" /> <Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item> </RadioGroupPrimitive.Item>
) );
}) });
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem } export { RadioGroup, RadioGroupItem };

View File

@@ -1,14 +1,14 @@
import * as React from "react" import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react" import { Check, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -18,15 +18,15 @@ const SelectTrigger = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
@@ -39,7 +39,7 @@ const SelectContent = React.forwardRef<
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className,
)} )}
position={position} position={position}
{...props} {...props}
@@ -48,15 +48,15 @@ const SelectContent = React.forwardRef<
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)} )}
> >
{children} {children}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
)) ));
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
@@ -66,7 +66,7 @@ const SelectItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -78,8 +78,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)) ));
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName;
export { export {
Select, Select,
@@ -88,4 +88,4 @@ export {
SelectTrigger, SelectTrigger,
SelectContent, SelectContent,
SelectItem, SelectItem,
} };

View File

@@ -18,7 +18,7 @@ import {
export default function ConsoleLayout() { export default function ConsoleLayout() {
const location = useLocation(); const location = useLocation();
const pathSegments = location.pathname.split('/').filter(Boolean); const pathSegments = location.pathname.split("/").filter(Boolean);
return ( return (
<SidebarProvider> <SidebarProvider>
@@ -32,15 +32,13 @@ export default function ConsoleLayout() {
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to="/"> <Link to="/">Home</Link>
Home
</Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
{pathSegments.map((segment, index) => { {pathSegments.map((segment, index) => {
const path = `/${pathSegments.slice(0, index + 1).join('/')}`; const path = `/${pathSegments.slice(0, index + 1).join("/")}`;
const isLast = index === pathSegments.length - 1; const isLast = index === pathSegments.length - 1;
return ( return (
<React.Fragment key={path}> <React.Fragment key={path}>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
@@ -52,7 +50,8 @@ export default function ConsoleLayout() {
) : ( ) : (
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link to={path}> <Link to={path}>
{segment.charAt(0).toUpperCase() + segment.slice(1)} {segment.charAt(0).toUpperCase() +
segment.slice(1)}
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
)} )}

View File

@@ -54,7 +54,9 @@ function CreatePeoplePageContent({
const environment = useRelayEnvironment(); const environment = useRelayEnvironment();
const data = usePreloadedQuery(createPeoplePageQuery, queryRef); const data = usePreloadedQuery(createPeoplePageQuery, queryRef);
const [createPeople, isCreatingPeople] = useMutation(createPeopleMutation); 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>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@@ -64,8 +66,8 @@ function CreatePeoplePageContent({
variables: { variables: {
input: { input: {
organizationId: data.node.id, organizationId: data.node.id,
fullName: formData.get('fullName') as string, fullName: formData.get("fullName") as string,
primaryEmailAddress: formData.get('primaryEmailAddress') as string, primaryEmailAddress: formData.get("primaryEmailAddress") as string,
kind, kind,
}, },
}, },
@@ -77,8 +79,8 @@ function CreatePeoplePageContent({
organization.invalidateRecord(); organization.invalidateRecord();
} }
}); });
navigate('/peoples'); navigate("/peoples");
}, },
}); });
}; };
@@ -91,7 +93,9 @@ function CreatePeoplePageContent({
<div className="space-y-6"> <div className="space-y-6">
<div> <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"> <p className="text-sm text-muted-foreground">
Add a new person to your organization. Add a new person to your organization.
</p> </p>
@@ -145,7 +149,7 @@ function CreatePeoplePageContent({
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => navigate('/peoples')} onClick={() => navigate("/peoples")}
> >
Cancel Cancel
</Button> </Button>
@@ -162,7 +166,9 @@ function CreatePeoplePageContent({
} }
export default function CreatePeoplePage() { export default function CreatePeoplePage() {
const [queryRef, loadQuery] = useQueryLoader<CreatePeoplePageQueryType>(createPeoplePageQuery); const [queryRef, loadQuery] = useQueryLoader<CreatePeoplePageQueryType>(
createPeoplePageQuery,
);
useEffect(() => { useEffect(() => {
loadQuery({}); loadQuery({});
@@ -177,4 +183,4 @@ export default function CreatePeoplePage() {
<CreatePeoplePageContent queryRef={queryRef} /> <CreatePeoplePageContent queryRef={queryRef} />
</Suspense> </Suspense>
); );
} }

View File

@@ -32,4 +32,4 @@ export function ErrorPage({
</Link> </Link>
</div> </div>
); );
} }

View File

@@ -46,16 +46,19 @@ function FrameworkListPageContent({
queryRef: PreloadedQuery<FrameworkListPageQueryType>; queryRef: PreloadedQuery<FrameworkListPageQueryType>;
}) { }) {
const data = usePreloadedQuery(FrameworkListPageQuery, queryRef); 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 ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background p-6">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-2xl font-semibold mb-2">Framework</h1> <h1 className="text-2xl font-semibold mb-2">Framework</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Compliance frameworks like SOC 2 and ISO 27001 provide guidelines to secure sensitive data, manage risks, and Compliance frameworks like SOC 2 and ISO 27001 provide guidelines to
build trust. SOC 2 focuses on customer data protection via five Trust Principles, while ISO 27001 establishes secure sensitive data, manage risks, and build trust. SOC 2 focuses on
a comprehensive Information Security Management System for global standards. customer data protection via five Trust Principles, while ISO 27001
establishes a comprehensive Information Security Management System for
global standards.
</p> </p>
</div> </div>
@@ -70,9 +73,13 @@ function FrameworkListPageContent({
</Badge> </Badge>
<div className="bg-green-500/10 w-24 h-24 rounded-full flex items-center justify-center mb-4"> <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" /> <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> </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"> <p className="text-sm text-muted-foreground">
{framework?.description} {framework?.description}
</p> </p>
@@ -118,7 +125,9 @@ function FrameworkListPageFallback() {
} }
export default function FrameworkListPage() { export default function FrameworkListPage() {
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(FrameworkListPageQuery); const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
FrameworkListPageQuery,
);
useEffect(() => { useEffect(() => {
loadQuery({}); loadQuery({});

View File

@@ -24,6 +24,7 @@ import { Progress } from "@/components/ui/progress";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql"; import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
const FrameworkOverviewPageQuery = graphql` const FrameworkOverviewPageQuery = graphql`
query FrameworkOverviewPageQuery($frameworkId: ID!) { query FrameworkOverviewPageQuery($frameworkId: ID!) {
node(id: $frameworkId) { node(id: $frameworkId) {
@@ -53,21 +54,29 @@ function FrameworkOverviewPageContent({
}) { }) {
const data = usePreloadedQuery(FrameworkOverviewPageQuery, queryRef); const data = usePreloadedQuery(FrameworkOverviewPageQuery, queryRef);
const framework = data.node; const framework = data.node;
const controls = framework.controls?.edges.map(edge => edge?.node) ?? []; const controls = framework.controls?.edges.map((edge) => edge?.node) ?? [];
const controlsByCategory = { const controlsByCategory = {
office: controls.filter(c => c?.name.toLowerCase().includes('office')), office: controls.filter((c) => c?.name.toLowerCase().includes("office")),
computer: controls.filter(c => c?.name.toLowerCase().includes('computer')), computer: controls.filter((c) =>
employees: controls.filter(c => c?.name.toLowerCase().includes('employee')), c?.name.toLowerCase().includes("computer"),
email: controls.filter(c => c?.name.toLowerCase().includes('email')), ),
code: controls.filter(c => c?.name.toLowerCase().includes('code')), employees: controls.filter((c) =>
infrastructure: controls.filter(c => c?.name.toLowerCase().includes('infrastructure')), c?.name.toLowerCase().includes("employee"),
network: controls.filter(c => c?.name.toLowerCase().includes('network')), ),
data: controls.filter(c => c?.name.toLowerCase().includes('data')), email: controls.filter((c) => c?.name.toLowerCase().includes("email")),
logging: controls.filter(c => c?.name.toLowerCase().includes('log')), code: controls.filter((c) => c?.name.toLowerCase().includes("code")),
incidents: controls.filter(c => c?.name.toLowerCase().includes('incident')), infrastructure: controls.filter((c) =>
vendors: controls.filter(c => c?.name.toLowerCase().includes('vendor')), c?.name.toLowerCase().includes("infrastructure"),
sharing: controls.filter(c => c?.name.toLowerCase().includes('share')) ),
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 = [ const controlCards = [
@@ -75,19 +84,25 @@ function FrameworkOverviewPageContent({
icon: <Building2 className="w-4 h-4" />, icon: <Building2 className="w-4 h-4" />,
title: "Secure your offices and internet access", title: "Secure your offices and internet access",
controls: controlsByCategory.office, 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, total: controlsByCategory.office.length,
}, },
{ {
icon: <Computer className="w-4 h-4" />, icon: <Computer className="w-4 h-4" />,
title: "Manage your computers", title: "Manage your computers",
controls: controlsByCategory.computer, 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, total: controlsByCategory.computer.length,
}, },
]; ];
const totalImplemented = controls.filter(c => c?.state === 'IMPLEMENTED').length; const totalImplemented = controls.filter(
(c) => c?.state === "IMPLEMENTED",
).length;
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background p-6">
@@ -125,7 +140,9 @@ function FrameworkOverviewPageContent({
<div className="mb-6 flex justify-between items-center"> <div className="mb-6 flex justify-between items-center">
<h2 className="text-xl font-semibold">Controls</h2> <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>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <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() { export default function FrameworkOverviewPage() {
const { frameworkId } = useParams(); const { frameworkId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(FrameworkOverviewPageQuery); const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(
FrameworkOverviewPageQuery,
);
useEffect(() => { useEffect(() => {
loadQuery({ frameworkId: frameworkId! }); loadQuery({ frameworkId: frameworkId! });
@@ -208,4 +227,3 @@ export default function FrameworkOverviewPage() {
</> </>
); );
} }

View File

@@ -18,7 +18,12 @@ import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generat
const ITEMS_PER_PAGE = 20; const ITEMS_PER_PAGE = 20;
const PeopleListPageQuery = graphql` 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") { node(id: "AZSfP_xAcAC5IAAAAAAltA") {
id id
... on Organization { ... on Organization {
@@ -57,21 +62,26 @@ const deletePeopleMutation = graphql`
function PeopleListPageContent({ function PeopleListPageContent({
queryRef, queryRef,
onPageChange, onPageChange,
loadQuery loadQuery,
}: { }: {
queryRef: PreloadedQuery<PeopleListPageQueryType>; 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; loadQuery: LoadQueryType;
}) { }) {
const data = usePreloadedQuery(PeopleListPageQuery, queryRef); 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 pageInfo = data.node.peoples?.pageInfo;
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [deletePeople] = useMutation(deletePeopleMutation); const [deletePeople] = useMutation(deletePeopleMutation);
const handlePageChange = (direction: 'prev' | 'next') => { const handlePageChange = (direction: "prev" | "next") => {
startTransition(() => { startTransition(() => {
if (direction === 'prev') { if (direction === "prev") {
onPageChange({ onPageChange({
last: ITEMS_PER_PAGE, last: ITEMS_PER_PAGE,
before: pageInfo?.startCursor, before: pageInfo?.startCursor,
@@ -96,9 +106,7 @@ function PeopleListPageContent({
</p> </p>
</div> </div>
<Button asChild> <Button asChild>
<Link to="/peoples/create"> <Link to="/peoples/create">Create People</Link>
Create People
</Link>
</Button> </Button>
</div> </div>
</div> </div>
@@ -113,14 +121,21 @@ function PeopleListPageContent({
<AvatarFallback>{person?.fullName?.[0]}</AvatarFallback> <AvatarFallback>{person?.fullName?.[0]}</AvatarFallback>
</Avatar> </Avatar>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-medium leading-none">{person?.fullName}</p> <p className="text-sm font-medium leading-none">
<p className="text-sm text-muted-foreground">{person?.primaryEmailAddress}</p> {person?.fullName}
</p>
<p className="text-sm text-muted-foreground">
{person?.primaryEmailAddress}
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Badge variant="secondary" className="font-medium"> <Badge variant="secondary" className="font-medium">
{person?.kind === 'EMPLOYEE' ? 'Employee' : {person?.kind === "EMPLOYEE"
person?.kind === 'CONTRACTOR' ? 'Contractor' : 'Vendor'} ? "Employee"
: person?.kind === "CONTRACTOR"
? "Contractor"
: "Vendor"}
</Badge> </Badge>
<div className="flex gap-1"> <div className="flex gap-1">
<CircleUser className="h-4 w-4 text-muted-foreground" /> <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" className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); 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({ deletePeople({
variables: { variables: {
input: { input: {
peopleId: person.id peopleId: person.id,
} },
}, },
onCompleted() { onCompleted() {
loadQuery({ loadQuery(
first: ITEMS_PER_PAGE, {
after: undefined, first: ITEMS_PER_PAGE,
last: undefined, after: undefined,
before: undefined, last: undefined,
}, { fetchPolicy: 'network-only' }); before: undefined,
},
{ fetchPolicy: "network-only" },
);
}, },
}); });
} }
@@ -164,14 +186,14 @@ function PeopleListPageContent({
<div className="flex gap-2 justify-end mt-4"> <div className="flex gap-2 justify-end mt-4">
<Button <Button
variant="outline" variant="outline"
onClick={() => handlePageChange('prev')} onClick={() => handlePageChange("prev")}
disabled={isPending || !pageInfo?.hasPreviousPage} disabled={isPending || !pageInfo?.hasPreviousPage}
> >
{isPending ? "Loading..." : "Previous"} {isPending ? "Loading..." : "Previous"}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => handlePageChange('next')} onClick={() => handlePageChange("next")}
disabled={isPending || !pageInfo?.hasNextPage} disabled={isPending || !pageInfo?.hasNextPage}
> >
{isPending ? "Loading..." : "Next"} {isPending ? "Loading..." : "Next"}
@@ -190,7 +212,7 @@ function PeopleListPageFallback() {
<div className="h-4 w-96 bg-muted animate-pulse rounded" /> <div className="h-4 w-96 bg-muted animate-pulse rounded" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{[1,2,3].map((i) => ( {[1, 2, 3].map((i) => (
<div <div
key={i} key={i}
className="flex items-center justify-between p-4 rounded-lg border bg-card text-card-foreground shadow-sm" 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="flex items-center gap-4">
<div className="h-6 w-16 bg-muted animate-pulse rounded-full" /> <div className="h-6 w-16 bg-muted animate-pulse rounded-full" />
<div className="flex gap-1"> <div className="flex gap-1">
{[1,2,3].map((j) => ( {[1, 2, 3].map((j) => (
<div key={j} className="h-4 w-4 bg-muted animate-pulse rounded" /> <div
key={j}
className="h-4 w-4 bg-muted animate-pulse rounded"
/>
))} ))}
</div> </div>
<div className="h-6 w-24 bg-muted animate-pulse rounded-full" /> <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() { export default function PeopleListPage() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [queryRef, loadQuery] = useQueryLoader<PeopleListPageQueryType>(PeopleListPageQuery); const [queryRef, loadQuery] =
useQueryLoader<PeopleListPageQueryType>(PeopleListPageQuery);
useEffect(() => { useEffect(() => {
const after = searchParams.get('after'); const after = searchParams.get("after");
const before = searchParams.get('before'); const before = searchParams.get("before");
loadQuery({ loadQuery({
first: before ? undefined : ITEMS_PER_PAGE, first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined, after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined, last: before ? ITEMS_PER_PAGE : undefined,
@@ -236,15 +264,25 @@ export default function PeopleListPage() {
}); });
}, [loadQuery, searchParams]); }, [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( loadQuery(
{ {
first, first,
after, after,
last, last,
before, before,
}, },
{ fetchPolicy: 'network-only' } { fetchPolicy: "network-only" },
); );
}; };
@@ -258,8 +296,12 @@ export default function PeopleListPage() {
<title>People - Probo Console</title> <title>People - Probo Console</title>
</Helmet> </Helmet>
<Suspense fallback={<PeopleListPageFallback />}> <Suspense fallback={<PeopleListPageFallback />}>
<PeopleListPageContent queryRef={queryRef} onPageChange={handlePageChange} loadQuery={loadQuery} /> <PeopleListPageContent
queryRef={queryRef}
onPageChange={handlePageChange}
loadQuery={loadQuery}
/>
</Suspense> </Suspense>
</> </>
); );
} }

View File

@@ -1,17 +1,28 @@
import { Mail, Building2, Upload, MoreVertical } from "lucide-react" import { Mail, Building2, Upload, MoreVertical } from "lucide-react";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import {
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" Card,
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" CardContent,
import { Helmet } from "react-helmet-async" CardHeader,
import { Suspense, useEffect } from "react" 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 { import {
graphql, graphql,
PreloadedQuery, PreloadedQuery,
usePreloadedQuery, usePreloadedQuery,
useQueryLoader, useQueryLoader,
} from "react-relay" } from "react-relay";
import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__/SettingsPageQuery.graphql" import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__/SettingsPageQuery.graphql";
const settingsPageQuery = graphql` const settingsPageQuery = graphql`
query SettingsPageQuery { query SettingsPageQuery {
@@ -23,7 +34,7 @@ const settingsPageQuery = graphql`
} }
} }
} }
` `;
interface Member { interface Member {
id: string; id: string;
@@ -46,13 +57,17 @@ function SettingsPageContent({
<div className="container mx-auto p-6 space-y-6"> <div className="container mx-auto p-6 space-y-6">
<div> <div>
<h1 className="text-4xl font-medium tracking-tight">Settings</h1> <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> </div>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>User & Organisation information</CardTitle> <CardTitle>User & Organisation information</CardTitle>
<CardDescription>Publish your trust page to the web</CardDescription> <CardDescription>
Publish your trust page to the web
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="space-y-2"> <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 justify-between rounded-lg border p-3 shadow-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Mail className="h-5 w-5 text-muted-foreground" /> <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>
<div className="flex items-center gap-2"> <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"> <Button variant="outline" size="sm">
Edit Edit
</Button> </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 justify-between rounded-lg border p-3 shadow-sm">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{organization.logoUrl ? ( {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"> <div className="flex h-10 w-10 items-center justify-center rounded-lg border">
<Upload className="h-5 w-5 text-muted-foreground" /> <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 justify-between rounded-lg border p-3 shadow-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-muted-foreground" /> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm">{organization.name}</span> <span className="text-sm">{organization.name}</span>
@@ -115,23 +140,37 @@ function SettingsPageContent({
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Workspace members</CardTitle> <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> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{members.map((member) => ( {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"> <div className="flex items-center gap-3">
<Avatar> <Avatar>
<AvatarFallback>{member.fullName.charAt(0)}</AvatarFallback> <AvatarFallback>
{member.fullName.charAt(0)}
</AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium">{member.fullName}</span> <span className="text-sm font-medium">
<span className="text-sm text-muted-foreground">{member.primaryEmailAddress}</span> {member.fullName}
</span>
<span className="text-sm text-muted-foreground">
{member.primaryEmailAddress}
</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <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> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
@@ -141,7 +180,9 @@ function SettingsPageContent({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem>Change role</DropdownMenuItem> <DropdownMenuItem>Change role</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">Remove member</DropdownMenuItem> <DropdownMenuItem className="text-red-600">
Remove member
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
@@ -152,7 +193,7 @@ function SettingsPageContent({
</Card> </Card>
</div> </div>
</> </>
) );
} }
function SettingsPageFallback() { function SettingsPageFallback() {
@@ -172,7 +213,8 @@ function SettingsPageFallback() {
} }
export default function SettingsPage() { export default function SettingsPage() {
const [queryRef, loadQuery] = useQueryLoader<SettingsPageQueryType>(settingsPageQuery); const [queryRef, loadQuery] =
useQueryLoader<SettingsPageQueryType>(settingsPageQuery);
useEffect(() => { useEffect(() => {
loadQuery({}); loadQuery({});
@@ -192,4 +234,4 @@ export default function SettingsPage() {
</Suspense> </Suspense>
</> </>
); );
} }

View File

@@ -19,7 +19,12 @@ import { Helmet } from "react-helmet-async";
const ITEMS_PER_PAGE = 20; const ITEMS_PER_PAGE = 20;
const vendorListPageQuery = graphql` 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") { node(id: "AZSfP_xAcAC5IAAAAAAltA") {
id id
... on Organization { ... on Organization {
@@ -64,34 +69,45 @@ const deleteVendorMutation = graphql`
// TODO: Remove this once we have a real list of vendors // TODO: Remove this once we have a real list of vendors
const vendorsList = [ const vendorsList = [
{ id: '1', name: 'Amazon Web Services', 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: "2",
{ id: '4', name: 'Salesforce', createdAt: new Date().toISOString() }, name: "Google Cloud Platform",
{ id: '5', name: 'Slack', createdAt: new Date().toISOString() }, createdAt: new Date().toISOString(),
{ id: '6', name: 'Zoom', createdAt: new Date().toISOString() }, },
{ id: '7', name: 'Dropbox', createdAt: new Date().toISOString() }, { id: "3", name: "Microsoft Azure", createdAt: new Date().toISOString() },
{ id: '8', name: 'Trello', createdAt: new Date().toISOString() }, { id: "4", name: "Salesforce", createdAt: new Date().toISOString() },
{ id: '9', name: 'Asana', createdAt: new Date().toISOString() }, { id: "5", name: "Slack", createdAt: new Date().toISOString() },
{ id: '10', name: 'Notion', createdAt: new Date().toISOString() }, { id: "6", name: "Zoom", createdAt: new Date().toISOString() },
{ id: '11', name: 'GitHub', createdAt: new Date().toISOString() }, { id: "7", name: "Dropbox", createdAt: new Date().toISOString() },
{ id: '12', name: 'GitLab', createdAt: new Date().toISOString() }, { id: "8", name: "Trello", createdAt: new Date().toISOString() },
{ id: '13', name: 'Bitbucket', createdAt: new Date().toISOString() }, { id: "9", name: "Asana", createdAt: new Date().toISOString() },
{ id: '14', name: 'Docker', createdAt: new Date().toISOString() }, { id: "10", name: "Notion", createdAt: new Date().toISOString() },
{ id: '15', name: 'Kubernetes', createdAt: new Date().toISOString() }, { id: "11", name: "GitHub", createdAt: new Date().toISOString() },
{ id: '16', name: 'Jenkins', createdAt: new Date().toISOString() }, { id: "12", name: "GitLab", createdAt: new Date().toISOString() },
{ id: '17', name: 'CircleCI', 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({ function VendorListContent({
queryRef, queryRef,
onPageChange, onPageChange,
loadQuery loadQuery,
}: { }: {
queryRef: PreloadedQuery<VendorListPageQueryType>; 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; loadQuery: LoadQueryType;
}) { }) {
const data = usePreloadedQuery(vendorListPageQuery, queryRef); const data = usePreloadedQuery(vendorListPageQuery, queryRef);
@@ -102,32 +118,33 @@ function VendorListContent({
const [createVendor] = useMutation(createVendorMutation); const [createVendor] = useMutation(createVendorMutation);
const [deleteVendor] = useMutation(deleteVendorMutation); 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 pageInfo = data.node?.vendors?.pageInfo;
const fuse = new Fuse(vendorsList, { const fuse = new Fuse(vendorsList, {
keys: ['name'], keys: ["name"],
threshold: 0.3, threshold: 0.3,
}); });
const handlePageChange = (direction: 'next' | 'prev') => { const handlePageChange = (direction: "next" | "prev") => {
if (!pageInfo) return; 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" && !pageInfo.hasNextPage) return;
if (direction === 'next') { if (direction === "prev" && !pageInfo.hasPreviousPage) return;
prev.set('after', pageInfo.endCursor!);
prev.delete('before'); 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 { } else {
prev.set('before', pageInfo.startCursor!); prev.set("before", pageInfo.startCursor!);
prev.delete('after'); prev.delete("after");
} }
return prev; return prev;
}); });
@@ -153,7 +170,9 @@ function VendorListContent({
</Avatar> </Avatar>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-medium leading-none">Add a vendor</p> <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> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -169,7 +188,9 @@ function VendorListContent({
if (value.trim() === "") { if (value.trim() === "") {
setFilteredVendors([]); setFilteredVendors([]);
} else { } else {
const results = fuse.search(value).map(result => result.item); const results = fuse
.search(value)
.map((result) => result.item);
setFilteredVendors(results); setFilteredVendors(results);
} }
}} }}
@@ -185,18 +206,21 @@ function VendorListContent({
variables: { variables: {
input: { input: {
organizationId: data.node.id, organizationId: data.node.id,
name: vendor.name name: vendor.name,
} },
}, },
onCompleted(response: any) { onCompleted(response: any) {
setSearchTerm(""); setSearchTerm("");
setFilteredVendors([]); setFilteredVendors([]);
loadQuery({ loadQuery(
first: ITEMS_PER_PAGE, {
after: undefined, first: ITEMS_PER_PAGE,
last: undefined, after: undefined,
before: undefined, last: undefined,
}, { fetchPolicy: 'network-only' }); before: undefined,
},
{ fetchPolicy: "network-only" },
);
}, },
}); });
}} }}
@@ -219,9 +243,12 @@ function VendorListContent({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{vendors.map((vendor) => ( {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"> <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}`} to={`/vendors/${vendor?.id}`}
className="flex items-center gap-4 flex-1" className="flex items-center gap-4 flex-1"
> >
@@ -230,8 +257,13 @@ function VendorListContent({
<AvatarFallback>{vendor?.name?.[0]}</AvatarFallback> <AvatarFallback>{vendor?.name?.[0]}</AvatarFallback>
</Avatar> </Avatar>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-medium leading-none">{vendor?.name}</p> <p className="text-sm font-medium leading-none">
<p className="text-sm text-muted-foreground">Vendor since {new Date(vendor?.createdAt).toLocaleDateString()}</p> {vendor?.name}
</p>
<p className="text-sm text-muted-foreground">
Vendor since{" "}
{new Date(vendor?.createdAt).toLocaleDateString()}
</p>
</div> </div>
</div> </div>
</Link> </Link>
@@ -247,26 +279,33 @@ function VendorListContent({
<Badge variant="outline" className="text-muted-foreground"> <Badge variant="outline" className="text-muted-foreground">
Not assessed Not assessed
</Badge> </Badge>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10" className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); 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({ deleteVendor({
variables: { variables: {
input: { input: {
vendorId: vendor.id vendorId: vendor.id,
} },
}, },
onCompleted() { onCompleted() {
loadQuery({ loadQuery(
first: ITEMS_PER_PAGE, {
after: undefined, first: ITEMS_PER_PAGE,
last: undefined, after: undefined,
before: undefined, last: undefined,
}, { fetchPolicy: 'network-only' }); before: undefined,
},
{ fetchPolicy: "network-only" },
);
}, },
}); });
} }
@@ -282,14 +321,14 @@ function VendorListContent({
<div className="flex gap-2 justify-end mt-4"> <div className="flex gap-2 justify-end mt-4">
<Button <Button
variant="outline" variant="outline"
onClick={() => handlePageChange('prev')} onClick={() => handlePageChange("prev")}
disabled={isPending || !pageInfo?.hasPreviousPage} disabled={isPending || !pageInfo?.hasPreviousPage}
> >
{isPending ? "Loading..." : "Previous"} {isPending ? "Loading..." : "Previous"}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => handlePageChange('next')} onClick={() => handlePageChange("next")}
disabled={isPending || !pageInfo?.hasNextPage} disabled={isPending || !pageInfo?.hasNextPage}
> >
{isPending ? "Loading..." : "Next"} {isPending ? "Loading..." : "Next"}
@@ -318,14 +357,15 @@ function VendorListFallback() {
export default function VendorListPage() { export default function VendorListPage() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [queryRef, loadQuery] = useQueryLoader<VendorListPageQueryType>(vendorListPageQuery); const [queryRef, loadQuery] =
useQueryLoader<VendorListPageQueryType>(vendorListPageQuery);
// Initialize with URL params // Initialize with URL params
useEffect(() => { useEffect(() => {
const after = searchParams.get('after'); const after = searchParams.get("after");
const before = searchParams.get('before'); const before = searchParams.get("before");
loadQuery({ loadQuery({
first: before ? undefined : ITEMS_PER_PAGE, first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined, after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined, last: before ? ITEMS_PER_PAGE : undefined,
@@ -333,15 +373,25 @@ export default function VendorListPage() {
}); });
}, [loadQuery, searchParams]); }, [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( loadQuery(
{ {
first, first,
after, after,
last, last,
before, before,
}, },
{ fetchPolicy: 'network-only' } { fetchPolicy: "network-only" },
); );
}; };
@@ -355,10 +405,10 @@ export default function VendorListPage() {
<title>Vendors - Probo Console</title> <title>Vendors - Probo Console</title>
</Helmet> </Helmet>
<Suspense fallback={<VendorListFallback />}> <Suspense fallback={<VendorListFallback />}>
<VendorListContent <VendorListContent
queryRef={queryRef} queryRef={queryRef}
onPageChange={handlePageChange} onPageChange={handlePageChange}
loadQuery={loadQuery} loadQuery={loadQuery}
/> />
</Suspense> </Suspense>
</> </>

View File

@@ -1,14 +1,19 @@
"use client" "use client";
import { useState } from "react" import { useState } from "react";
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { HelpCircle } from "lucide-react" import { HelpCircle } from "lucide-react";
import { graphql, PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay" import {
import { Suspense, useEffect } from "react" graphql,
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.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 { useParams } from "react-router";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
const VendorOverviewPageQuery = graphql` const VendorOverviewPageQuery = graphql`
@@ -22,7 +27,7 @@ const VendorOverviewPageQuery = graphql`
} }
} }
} }
` `;
function VendorOverviewPageContent({ function VendorOverviewPageContent({
queryRef, queryRef,
@@ -30,7 +35,7 @@ function VendorOverviewPageContent({
queryRef: PreloadedQuery<VendorOverviewPageQueryType>; queryRef: PreloadedQuery<VendorOverviewPageQueryType>;
}) { }) {
const data = usePreloadedQuery(VendorOverviewPageQuery, queryRef); const data = usePreloadedQuery(VendorOverviewPageQuery, queryRef);
const [riskLevel, setRiskLevel] = useState("medium") const [riskLevel, setRiskLevel] = useState("medium");
return ( return (
<div className="space-y-6 p-4 md:p-6 lg:p-8"> <div className="space-y-6 p-4 md:p-6 lg:p-8">
@@ -42,26 +47,39 @@ function VendorOverviewPageContent({
alt="Google Cloud" alt="Google Cloud"
className="h-8 w-8" 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> </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> </div>
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<h2 className="text-lg font-medium">Asset Storage Type</h2> <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>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" /> <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>
<div className="flex gap-2"> <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-gray-100 px-4 py-1 text-sm text-gray-900">
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Physical</button> Digital
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Physical
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -72,7 +90,8 @@ function VendorOverviewPageContent({
<div className="space-y-2"> <div className="space-y-2">
<h2 className="text-lg font-medium">Data Storage and Handling</h2> <h2 className="text-lg font-medium">Data Storage and Handling</h2>
<p className="text-sm text-gray-500"> <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> </p>
</div> </div>
@@ -80,29 +99,43 @@ function VendorOverviewPageContent({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" /> <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>
<div className="flex gap-2"> <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-gray-100 px-4 py-1 text-sm text-gray-900">
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Yes</button> No
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Yes
</button>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" /> <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>
<div className="flex gap-2"> <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-gray-100 px-4 py-1 text-sm text-gray-900">
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Yes</button> No
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Yes
</button>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" /> <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>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -141,9 +174,12 @@ function VendorOverviewPageContent({
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <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"> <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> </p>
</div> </div>
@@ -151,7 +187,9 @@ function VendorOverviewPageContent({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" /> <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> </div>
<RadioGroup defaultValue="greater" className="space-y-2"> <RadioGroup defaultValue="greater" className="space-y-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -160,7 +198,9 @@ function VendorOverviewPageContent({
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupItem value="between" id="between" /> <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>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupItem value="less" id="less" /> <RadioGroupItem value="less" id="less" />
@@ -176,23 +216,37 @@ function VendorOverviewPageContent({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" /> <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>
<div className="flex gap-2"> <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-gray-100 px-4 py-1 text-sm text-gray-900">
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">Yes</button> No
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Yes
</button>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" /> <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>
<div className="flex gap-2"> <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">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">Medium</button> Low
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">High</button> </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> </div>
</div> </div>
@@ -203,18 +257,26 @@ function VendorOverviewPageContent({
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<h2 className="text-lg font-medium">Risk level</h2> <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>
<div className="flex gap-2"> <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">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">Medium</button> Low
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">High</button> </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> </div>
</Card> </Card>
</div> </div>
</div> </div>
) );
} }
function VendorOverviewPageFallback() { function VendorOverviewPageFallback() {
@@ -235,7 +297,9 @@ function VendorOverviewPageFallback() {
export default function VendorOverviewPage() { export default function VendorOverviewPage() {
const { vendorId } = useParams(); const { vendorId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(VendorOverviewPageQuery); const [queryRef, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(
VendorOverviewPageQuery,
);
useEffect(() => { useEffect(() => {
loadQuery({ vendorId: vendorId! }); loadQuery({ vendorId: vendorId! });
@@ -255,4 +319,4 @@ export default function VendorOverviewPage() {
</Suspense> </Suspense>
</> </>
); );
} }