@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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,13 +32,11 @@ 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 (
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -78,7 +80,7 @@ function CreatePeoplePageContent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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({});
|
||||||
|
|||||||
@@ -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>
|
||||||
<h2 className="text-xl font-semibold mb-2">{framework?.name}</h2>
|
</div>
|
||||||
|
<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({});
|
||||||
|
|||||||
@@ -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() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
first: ITEMS_PER_PAGE,
|
||||||
after: undefined,
|
after: undefined,
|
||||||
last: undefined,
|
last: undefined,
|
||||||
before: undefined,
|
before: undefined,
|
||||||
}, { fetchPolicy: 'network-only' });
|
},
|
||||||
|
{ 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"}
|
||||||
@@ -206,7 +228,10 @@ function PeopleListPageFallback() {
|
|||||||
<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,15 +243,18 @@ 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,
|
||||||
@@ -236,7 +264,17 @@ 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,
|
||||||
@@ -244,7 +282,7 @@ export default function PeopleListPage() {
|
|||||||
last,
|
last,
|
||||||
before,
|
before,
|
||||||
},
|
},
|
||||||
{ fetchPolicy: 'network-only' }
|
{ fetchPolicy: "network-only" },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,7 +296,11 @@ 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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({});
|
||||||
|
|||||||
@@ -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 === "next" && !pageInfo.hasNextPage) return;
|
||||||
if (direction === 'prev' && !pageInfo.hasPreviousPage) return;
|
if (direction === "prev" && !pageInfo.hasPreviousPage) return;
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
const params = direction === 'next'
|
const params =
|
||||||
|
direction === "next"
|
||||||
? { first: ITEMS_PER_PAGE, after: pageInfo.endCursor }
|
? { first: ITEMS_PER_PAGE, after: pageInfo.endCursor }
|
||||||
: { last: ITEMS_PER_PAGE, before: pageInfo.startCursor };
|
: { last: ITEMS_PER_PAGE, before: pageInfo.startCursor };
|
||||||
|
|
||||||
setSearchParams(prev => {
|
setSearchParams((prev) => {
|
||||||
if (direction === 'next') {
|
if (direction === "next") {
|
||||||
prev.set('after', pageInfo.endCursor!);
|
prev.set("after", pageInfo.endCursor!);
|
||||||
prev.delete('before');
|
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,
|
first: ITEMS_PER_PAGE,
|
||||||
after: undefined,
|
after: undefined,
|
||||||
last: undefined,
|
last: undefined,
|
||||||
before: undefined,
|
before: undefined,
|
||||||
}, { fetchPolicy: 'network-only' });
|
},
|
||||||
|
{ fetchPolicy: "network-only" },
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -219,7 +243,10 @@ 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}`}
|
||||||
@@ -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>
|
||||||
@@ -253,20 +285,27 @@ function VendorListContent({
|
|||||||
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,
|
first: ITEMS_PER_PAGE,
|
||||||
after: undefined,
|
after: undefined,
|
||||||
last: undefined,
|
last: undefined,
|
||||||
before: undefined,
|
before: undefined,
|
||||||
}, { fetchPolicy: 'network-only' });
|
},
|
||||||
|
{ 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,12 +357,13 @@ 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,
|
||||||
@@ -333,7 +373,17 @@ 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,
|
||||||
@@ -341,7 +391,7 @@ export default function VendorListPage() {
|
|||||||
last,
|
last,
|
||||||
before,
|
before,
|
||||||
},
|
},
|
||||||
{ fetchPolicy: 'network-only' }
|
{ fetchPolicy: "network-only" },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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! });
|
||||||
|
|||||||
Reference in New Issue
Block a user