Add PageTemplate with skeletons and apply to Framework list
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import ConsoleLayout from "./layouts/ConsoleLayout";
|
||||
import AuthLayout from "./layouts/AuthLayout";
|
||||
import { RelayEnvironment } from "./RelayEnvironment";
|
||||
import VisitorErrorBoundary from "./components/VisitorErrorBoundary";
|
||||
import { FrameworkListPageSkeleton } from "./pages/FrameworkListPage";
|
||||
|
||||
posthog.init(process.env.POSTHOG_KEY!, {
|
||||
api_host: process.env.POSTHOG_HOST,
|
||||
@@ -145,7 +146,7 @@ function App() {
|
||||
<Route
|
||||
path="frameworks"
|
||||
element={
|
||||
<Suspense>
|
||||
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
|
||||
26
apps/console/src/components/PageContainer.tsx
Normal file
26
apps/console/src/components/PageContainer.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
export interface PageContainerProps {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const PageContainer: FC<PageContainerProps> = ({ children, title }) => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>{`${title} - Probo Console`}</Helmet>
|
||||
<div className="container">{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageContainerSkeleton: FC<{
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
}> = ({ children, title }) =>
|
||||
title ? (
|
||||
<PageContainer title={title}>{children}</PageContainer>
|
||||
) : (
|
||||
<div className="container">{children}</div>
|
||||
);
|
||||
108
apps/console/src/components/PageHeader.tsx
Normal file
108
apps/console/src/components/PageHeader.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
export interface PageHeaderProps {
|
||||
title: ReactNode;
|
||||
actions?: ReactNode;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
export function PageHeaderShell({
|
||||
className,
|
||||
title,
|
||||
actions,
|
||||
description,
|
||||
}: PageHeaderProps & { className?: string }) {
|
||||
return (
|
||||
<div className={cn("space-y-6", className)}>
|
||||
{actions ? (
|
||||
<div className="flex items-center justify-between">
|
||||
{title}
|
||||
{actions}
|
||||
</div>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
{description}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
className,
|
||||
title,
|
||||
actions,
|
||||
description,
|
||||
}: PageHeaderProps & { className?: string }) {
|
||||
return (
|
||||
<PageHeaderShell
|
||||
className={className}
|
||||
title={<PageHeading>{title}</PageHeading>}
|
||||
description={<PageDescription>{description}</PageDescription>}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageHeading({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return <h1 className={cn("text-3xl font-medium", className)}>{children}</h1>;
|
||||
}
|
||||
|
||||
const PageHeadingSkeleton: FC = () => {
|
||||
return (
|
||||
<div className="py-[3px]">
|
||||
<div className="bg-muted animate rounded-lg w-60 h-7.5" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function PageDescription({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<p className={cn("text-md text-muted-foreground", className)}>{children}</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageDescriptionSkeleton() {
|
||||
return (
|
||||
<div className="py-[5px]">
|
||||
<div className="bg-muted animate rounded-md w-100 h-4.5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const PageHeaderSkeleton: FC<{
|
||||
className?: string;
|
||||
title?: string;
|
||||
actions?: ReactNode;
|
||||
description?: ReactNode;
|
||||
withDescription?: boolean;
|
||||
}> = ({ className, title, actions, description, withDescription = false }) => {
|
||||
return (
|
||||
<PageHeaderShell
|
||||
className={className}
|
||||
title={
|
||||
title ? <PageHeading>{title}</PageHeading> : <PageHeadingSkeleton />
|
||||
}
|
||||
description={
|
||||
description ? (
|
||||
<PageDescription>{description}</PageDescription>
|
||||
) : withDescription ? (
|
||||
<PageDescriptionSkeleton />
|
||||
) : null
|
||||
}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
47
apps/console/src/components/PageTemplate.tsx
Normal file
47
apps/console/src/components/PageTemplate.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import {
|
||||
PageContainer,
|
||||
PageContainerProps,
|
||||
PageContainerSkeleton,
|
||||
} from "./PageContainer";
|
||||
import { PageHeader, PageHeaderProps, PageHeaderSkeleton } from "./PageHeader";
|
||||
|
||||
export const PageTemplate: FC<PageContainerProps & PageHeaderProps> = ({
|
||||
actions,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<PageContainer title={title}>
|
||||
<PageHeader
|
||||
className="mb-17"
|
||||
title={title}
|
||||
description={description}
|
||||
actions={actions}
|
||||
/>
|
||||
{children}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageTemplateSkeleton: FC<{
|
||||
title?: string;
|
||||
description?: ReactNode;
|
||||
withDescription?: boolean;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
}> = ({ actions, children, description, withDescription = false, title }) => {
|
||||
return (
|
||||
<PageContainerSkeleton title={title}>
|
||||
<PageHeaderSkeleton
|
||||
className="mb-17"
|
||||
title={title}
|
||||
description={description}
|
||||
withDescription={withDescription}
|
||||
actions={actions}
|
||||
/>
|
||||
{children}
|
||||
</PageContainerSkeleton>
|
||||
);
|
||||
};
|
||||
@@ -75,7 +75,7 @@ import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssign
|
||||
import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
|
||||
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
|
||||
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
// Function to format ISO8601 duration to human-readable format
|
||||
const formatDuration = (isoDuration: string): string => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
CreateControlPageCreateControlMutation,
|
||||
ControlImportance,
|
||||
} from "./__generated__/CreateControlPageCreateControlMutation.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const createControlMutation = graphql`
|
||||
mutation CreateControlPageCreateControlMutation(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useToast } from "@/hooks/use-toast";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const createPeopleMutation = graphql`
|
||||
mutation CreatePeoplePageCreatePeopleMutation(
|
||||
|
||||
@@ -21,7 +21,7 @@ import PeopleSelector from "@/components/PeopleSelector";
|
||||
import { Suspense } from "react";
|
||||
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
|
||||
import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const CreatePolicyQuery = graphql`
|
||||
query CreatePolicyPageQuery($organizationId: ID!) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Link, useParams } from "react-router";
|
||||
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FrameworkListPageImportFrameworkMutation as FrameworkListPageImportFrameworkMutationType } from "./__generated__/FrameworkListPageImportFrameworkMutation.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||
|
||||
const FrameworkListPageQuery = graphql`
|
||||
query FrameworkListPageQuery($organizationId: ID!) {
|
||||
@@ -191,111 +190,107 @@ function FrameworkListPageContent({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Frameworks - Probo</title>
|
||||
</Helmet>
|
||||
<div className="container">
|
||||
<PageHeader
|
||||
className="mb-17"
|
||||
title="Frameworks"
|
||||
description="Manage your compliance frameworks"
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Dialog
|
||||
open={isImportDialogOpen}
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Import Framework
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Framework</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="framework-file">
|
||||
Upload Framework File
|
||||
</Label>
|
||||
<Input
|
||||
id="framework-file"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
accept=".json"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upload a JSON file containing your framework definition.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button asChild>
|
||||
<Link to={`/organizations/${organizationId}/frameworks/create`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Framework
|
||||
</Link>
|
||||
<PageTemplate
|
||||
title="Frameworks"
|
||||
description="Manage your compliance frameworks"
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Dialog
|
||||
open={isImportDialogOpen}
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Import Framework
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||
{frameworks.map((framework) => {
|
||||
const validatedControls = framework.controls.edges.filter(
|
||||
(edge) => edge?.node?.state === "IMPLEMENTED"
|
||||
).length;
|
||||
const totalControls = framework.controls.edges.length;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={framework.id}
|
||||
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
|
||||
>
|
||||
<FrameworkCard
|
||||
title={framework.name}
|
||||
description={framework.description}
|
||||
icon={
|
||||
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-lg font-semibold text-blue-900">
|
||||
{framework.name.split(" ")[0]}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
status={
|
||||
validatedControls === totalControls
|
||||
? "Compliant"
|
||||
: undefined
|
||||
}
|
||||
progress={
|
||||
validatedControls === totalControls
|
||||
? "All controls validated"
|
||||
: `${validatedControls}/${totalControls} Controls validated`
|
||||
}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Framework</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="framework-file">Upload Framework File</Label>
|
||||
<Input
|
||||
id="framework-file"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
accept=".json"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upload a JSON file containing your framework definition.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button asChild>
|
||||
<Link to={`/organizations/${organizationId}/frameworks/create`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Framework
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||
{frameworks.map((framework) => {
|
||||
const validatedControls = framework.controls.edges.filter(
|
||||
(edge) => edge?.node?.state === "IMPLEMENTED"
|
||||
).length;
|
||||
const totalControls = framework.controls.edges.length;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={framework.id}
|
||||
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
|
||||
>
|
||||
<FrameworkCard
|
||||
title={framework.name}
|
||||
description={framework.description}
|
||||
icon={
|
||||
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-lg font-semibold text-blue-900">
|
||||
{framework.name.split(" ")[0]}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
status={
|
||||
validatedControls === totalControls
|
||||
? "Compliant"
|
||||
: undefined
|
||||
}
|
||||
progress={
|
||||
validatedControls === totalControls
|
||||
? "All controls validated"
|
||||
: `${validatedControls}/${totalControls} Controls validated`
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
function FrameworkListPageFallback() {
|
||||
export function FrameworkListPageSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
||||
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" />
|
||||
</div>
|
||||
<PageTemplateSkeleton
|
||||
title="Frameworks"
|
||||
description="Manage your compliance frameworks"
|
||||
actions={
|
||||
<div className="flex gap-4 w-1/3">
|
||||
<div className="bg-muted animate-pulse h-9 w-1/2 rounded-lg" />
|
||||
<div className="bg-muted animate-pulse h-9 w-1/2 rounded-lg" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{[1, 2].map((i) => (
|
||||
<Card key={i} className="bg-card/50">
|
||||
@@ -310,7 +305,7 @@ function FrameworkListPageFallback() {
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PageTemplateSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -326,13 +321,8 @@ export default function FrameworkListPage() {
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Frameworks - Probo Console</title>
|
||||
</Helmet>
|
||||
<Suspense fallback={<FrameworkListPageFallback />}>
|
||||
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
||||
</Suspense>
|
||||
</>
|
||||
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
||||
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const FrameworkOverviewPageQuery = graphql`
|
||||
query FrameworkOverviewPageQuery($frameworkId: ID!) {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function PageHeader({
|
||||
className,
|
||||
children,
|
||||
title,
|
||||
actions,
|
||||
description,
|
||||
}: {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
title: string;
|
||||
actions?: ReactNode;
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("space-y-6", className)}>
|
||||
{actions ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<PageHeading title={title} />
|
||||
{actions}
|
||||
</div>
|
||||
) : (
|
||||
<PageHeading title={title} />
|
||||
)}
|
||||
{description && <PageDescription>{description}</PageDescription>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageHeading({
|
||||
title,
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
title: string;
|
||||
}) {
|
||||
return <h1 className={cn("text-3xl font-medium", className)}>{title}</h1>;
|
||||
}
|
||||
|
||||
export function PageDescription({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<p className={cn("text-lg text-muted-foreground", className)}>{children}</p>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generat
|
||||
import type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql";
|
||||
import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
|
||||
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const ITEMS_PER_PAGE = 25;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./_
|
||||
import { useParams } from "react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const peopleOverviewPageQuery = graphql`
|
||||
query PeopleOverviewPageQuery($peopleId: ID!) {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { format } from "date-fns";
|
||||
import type { PolicyListPageQuery as PolicyListPageQueryType } from "./__generated__/PolicyListPageQuery.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const PolicyListPageQuery = graphql`
|
||||
query PolicyListPageQuery($organizationId: ID!) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOve
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import "../styles/policy-content.css";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const PolicyOverviewPageQuery = graphql`
|
||||
query PolicyOverviewPageQuery($policyId: ID!) {
|
||||
@@ -157,6 +157,32 @@ function PolicyOverviewPageContent({
|
||||
<PageHeader
|
||||
className="mb-17"
|
||||
title={policy.name ?? ""}
|
||||
description={
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Badge
|
||||
variant={policy.status === "ACTIVE" ? "default" : "outline"}
|
||||
>
|
||||
{policy.status === "ACTIVE" ? "Active" : "Draft"}
|
||||
</Badge>
|
||||
{policy.owner && (
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-3 w-3" />
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/people/${policy.owner.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{policy.owner.fullName}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{policy.reviewDate && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>Review: {formatDate(policy.reviewDate)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
@@ -188,30 +214,7 @@ function PolicyOverviewPageContent({
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Badge variant={policy.status === "ACTIVE" ? "default" : "outline"}>
|
||||
{policy.status === "ACTIVE" ? "Active" : "Draft"}
|
||||
</Badge>
|
||||
{policy.owner && (
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-3 w-3" />
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/people/${policy.owner.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{policy.owner.fullName}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{policy.reviewDate && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>Review: {formatDate(policy.reviewDate)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
|
||||
@@ -39,7 +39,7 @@ import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__
|
||||
import type { SettingsPageUpdateOrganizationMutation as SettingsPageUpdateOrganizationMutationType } from "./__generated__/SettingsPageUpdateOrganizationMutation.graphql";
|
||||
import type { SettingsPageInviteUserMutation as SettingsPageInviteUserMutationType } from "./__generated__/SettingsPageInviteUserMutation.graphql";
|
||||
import type { SettingsPageRemoveUserMutation as SettingsPageRemoveUserMutationType } from "./__generated__/SettingsPageRemoveUserMutation.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const settingsPageQuery = graphql`
|
||||
query SettingsPageQuery($organizationID: ID!) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
ControlState,
|
||||
ControlImportance,
|
||||
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const updateControlMutation = graphql`
|
||||
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { HelpCircle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { UpdateFrameworkPageUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkPageUpdateFrameworkMutation.graphql";
|
||||
import { UpdateFrameworkPageQuery as UpdateFrameworkPageQueryType } from "./__generated__/UpdateFrameworkPageQuery.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const updateFrameworkMutation = graphql`
|
||||
mutation UpdateFrameworkPageUpdateFrameworkMutation(
|
||||
|
||||
@@ -20,7 +20,7 @@ import PolicyEditor from "@/components/PolicyEditor";
|
||||
import PeopleSelector from "@/components/PeopleSelector";
|
||||
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
|
||||
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const UpdatePolicyPageQuery = graphql`
|
||||
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPa
|
||||
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
|
||||
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const ITEMS_PER_PAGE = 25;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./_
|
||||
import { useParams } from "react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
const vendorOverviewPageQuery = graphql`
|
||||
query VendorOverviewPageQuery($vendorId: ID!) {
|
||||
|
||||
Reference in New Issue
Block a user