@@ -26,7 +26,9 @@ const NotFoundPage = lazy(() => import("./pages/NotFoundPage"));
|
||||
const PeopleListPage = lazy(() => import("./pages/PeopleListPage"));
|
||||
const VendorListPage = lazy(() => import("./pages/VendorListPage"));
|
||||
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 SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
||||
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();
|
||||
return <ErrorBoundary key={location.pathname}>{children}</ErrorBoundary>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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",
|
||||
@@ -20,8 +20,8 @@ const badgeVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
@@ -30,7 +30,7 @@ export interface BadgeProps
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -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<
|
||||
HTMLDivElement,
|
||||
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
@@ -12,7 +12,7 @@ const Checkbox = React.forwardRef<
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -22,7 +22,7 @@ const Checkbox = React.forwardRef<
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
@@ -18,7 +18,7 @@ const Label = React.forwardRef<
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
@@ -11,7 +11,7 @@ const Progress = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -20,7 +20,7 @@ const Progress = React.forwardRef<
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { Circle } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
@@ -14,9 +14,9 @@ const RadioGroup = React.forwardRef<
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
@@ -27,7 +27,7 @@ const RadioGroupItem = React.forwardRef<
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -35,8 +35,8 @@ const RadioGroupItem = React.forwardRef<
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
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<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
@@ -18,15 +18,15 @@ const SelectTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
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",
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
@@ -48,15 +48,15 @@ const SelectContent = React.forwardRef<
|
||||
className={cn(
|
||||
"p-1",
|
||||
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}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
@@ -66,7 +66,7 @@ const SelectItem = React.forwardRef<
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -78,8 +78,8 @@ const SelectItem = React.forwardRef<
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
@@ -88,4 +88,4 @@ export {
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
|
||||
export default function ConsoleLayout() {
|
||||
const location = useLocation();
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||
const pathSegments = location.pathname.split("/").filter(Boolean);
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
@@ -32,15 +32,13 @@ export default function ConsoleLayout() {
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to="/">
|
||||
Home
|
||||
</Link>
|
||||
<Link to="/">Home</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{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;
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment key={path}>
|
||||
<BreadcrumbSeparator />
|
||||
@@ -52,7 +50,8 @@ export default function ConsoleLayout() {
|
||||
) : (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={path}>
|
||||
{segment.charAt(0).toUpperCase() + segment.slice(1)}
|
||||
{segment.charAt(0).toUpperCase() +
|
||||
segment.slice(1)}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
|
||||
@@ -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