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 AuthLayout from "./layouts/AuthLayout";
|
||||||
import { RelayEnvironment } from "./RelayEnvironment";
|
import { RelayEnvironment } from "./RelayEnvironment";
|
||||||
import VisitorErrorBoundary from "./components/VisitorErrorBoundary";
|
import VisitorErrorBoundary from "./components/VisitorErrorBoundary";
|
||||||
|
import { FrameworkListPageSkeleton } from "./pages/FrameworkListPage";
|
||||||
|
|
||||||
posthog.init(process.env.POSTHOG_KEY!, {
|
posthog.init(process.env.POSTHOG_KEY!, {
|
||||||
api_host: process.env.POSTHOG_HOST,
|
api_host: process.env.POSTHOG_HOST,
|
||||||
@@ -145,7 +146,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="frameworks"
|
path="frameworks"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<FrameworkListPage />
|
<FrameworkListPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</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 { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
|
||||||
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
|
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
|
||||||
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.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
|
// Function to format ISO8601 duration to human-readable format
|
||||||
const formatDuration = (isoDuration: string): string => {
|
const formatDuration = (isoDuration: string): string => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
CreateControlPageCreateControlMutation,
|
CreateControlPageCreateControlMutation,
|
||||||
ControlImportance,
|
ControlImportance,
|
||||||
} from "./__generated__/CreateControlPageCreateControlMutation.graphql";
|
} from "./__generated__/CreateControlPageCreateControlMutation.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const createControlMutation = graphql`
|
const createControlMutation = graphql`
|
||||||
mutation CreateControlPageCreateControlMutation(
|
mutation CreateControlPageCreateControlMutation(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { HelpCircle } from "lucide-react";
|
import { HelpCircle } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql";
|
import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const createPeopleMutation = graphql`
|
const createPeopleMutation = graphql`
|
||||||
mutation CreatePeoplePageCreatePeopleMutation(
|
mutation CreatePeoplePageCreatePeopleMutation(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import PeopleSelector from "@/components/PeopleSelector";
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
|
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
|
||||||
import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql";
|
import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const CreatePolicyQuery = graphql`
|
const CreatePolicyQuery = graphql`
|
||||||
query CreatePolicyPageQuery($organizationId: ID!) {
|
query CreatePolicyPageQuery($organizationId: ID!) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Link, useParams } from "react-router";
|
import { Link, useParams } from "react-router";
|
||||||
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
|
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, Upload } from "lucide-react";
|
import { Plus, Upload } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -24,7 +23,7 @@ import {
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { FrameworkListPageImportFrameworkMutation as FrameworkListPageImportFrameworkMutationType } from "./__generated__/FrameworkListPageImportFrameworkMutation.graphql";
|
import { FrameworkListPageImportFrameworkMutation as FrameworkListPageImportFrameworkMutationType } from "./__generated__/FrameworkListPageImportFrameworkMutation.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const FrameworkListPageQuery = graphql`
|
const FrameworkListPageQuery = graphql`
|
||||||
query FrameworkListPageQuery($organizationId: ID!) {
|
query FrameworkListPageQuery($organizationId: ID!) {
|
||||||
@@ -191,111 +190,107 @@ function FrameworkListPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Frameworks"
|
||||||
<title>Frameworks - Probo</title>
|
description="Manage your compliance frameworks"
|
||||||
</Helmet>
|
actions={
|
||||||
<div className="container">
|
<div className="flex gap-4">
|
||||||
<PageHeader
|
<Dialog
|
||||||
className="mb-17"
|
open={isImportDialogOpen}
|
||||||
title="Frameworks"
|
onOpenChange={setIsImportDialogOpen}
|
||||||
description="Manage your compliance frameworks"
|
>
|
||||||
actions={
|
<DialogTrigger asChild>
|
||||||
<div className="flex gap-4">
|
<Button variant="outline">
|
||||||
<Dialog
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
open={isImportDialogOpen}
|
Import Framework
|
||||||
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>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</DialogTrigger>
|
||||||
}
|
<DialogContent>
|
||||||
/>
|
<DialogHeader>
|
||||||
<div className="space-y-6">
|
<DialogTitle>Import Framework</DialogTitle>
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
</DialogHeader>
|
||||||
{frameworks.map((framework) => {
|
<div className="space-y-4 py-4">
|
||||||
const validatedControls = framework.controls.edges.filter(
|
<div className="space-y-2">
|
||||||
(edge) => edge?.node?.state === "IMPLEMENTED"
|
<Label htmlFor="framework-file">Upload Framework File</Label>
|
||||||
).length;
|
<Input
|
||||||
const totalControls = framework.controls.edges.length;
|
id="framework-file"
|
||||||
|
type="file"
|
||||||
return (
|
ref={fileInputRef}
|
||||||
<Link
|
onChange={handleFileChange}
|
||||||
key={framework.id}
|
disabled={isUploading}
|
||||||
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
|
accept=".json"
|
||||||
>
|
|
||||||
<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>
|
<p className="text-sm text-muted-foreground">
|
||||||
);
|
Upload a JSON file containing your framework definition.
|
||||||
})}
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FrameworkListPageFallback() {
|
export function FrameworkListPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<PageTemplateSkeleton
|
||||||
<div>
|
title="Frameworks"
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
description="Manage your compliance frameworks"
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" />
|
actions={
|
||||||
</div>
|
<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">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{[1, 2].map((i) => (
|
{[1, 2].map((i) => (
|
||||||
<Card key={i} className="bg-card/50">
|
<Card key={i} className="bg-card/50">
|
||||||
@@ -310,7 +305,7 @@ function FrameworkListPageFallback() {
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,13 +321,8 @@ export default function FrameworkListPage() {
|
|||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
||||||
<Helmet>
|
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
||||||
<title>Frameworks - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<FrameworkListPageFallback />}>
|
|
||||||
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
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";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const FrameworkOverviewPageQuery = graphql`
|
const FrameworkOverviewPageQuery = graphql`
|
||||||
query FrameworkOverviewPageQuery($frameworkId: ID!) {
|
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 type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql";
|
||||||
import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
|
import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
|
||||||
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
|
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 25;
|
const ITEMS_PER_PAGE = 25;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./_
|
|||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const peopleOverviewPageQuery = graphql`
|
const peopleOverviewPageQuery = graphql`
|
||||||
query PeopleOverviewPageQuery($peopleId: ID!) {
|
query PeopleOverviewPageQuery($peopleId: ID!) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import type { PolicyListPageQuery as PolicyListPageQueryType } from "./__generated__/PolicyListPageQuery.graphql";
|
import type { PolicyListPageQuery as PolicyListPageQueryType } from "./__generated__/PolicyListPageQuery.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const PolicyListPageQuery = graphql`
|
const PolicyListPageQuery = graphql`
|
||||||
query PolicyListPageQuery($organizationId: ID!) {
|
query PolicyListPageQuery($organizationId: ID!) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOve
|
|||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import "../styles/policy-content.css";
|
import "../styles/policy-content.css";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const PolicyOverviewPageQuery = graphql`
|
const PolicyOverviewPageQuery = graphql`
|
||||||
query PolicyOverviewPageQuery($policyId: ID!) {
|
query PolicyOverviewPageQuery($policyId: ID!) {
|
||||||
@@ -157,6 +157,32 @@ function PolicyOverviewPageContent({
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
className="mb-17"
|
className="mb-17"
|
||||||
title={policy.name ?? ""}
|
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={
|
actions={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
@@ -188,30 +214,7 @@ function PolicyOverviewPageContent({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div className="lg:col-span-2">
|
<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 { SettingsPageUpdateOrganizationMutation as SettingsPageUpdateOrganizationMutationType } from "./__generated__/SettingsPageUpdateOrganizationMutation.graphql";
|
||||||
import type { SettingsPageInviteUserMutation as SettingsPageInviteUserMutationType } from "./__generated__/SettingsPageInviteUserMutation.graphql";
|
import type { SettingsPageInviteUserMutation as SettingsPageInviteUserMutationType } from "./__generated__/SettingsPageInviteUserMutation.graphql";
|
||||||
import type { SettingsPageRemoveUserMutation as SettingsPageRemoveUserMutationType } from "./__generated__/SettingsPageRemoveUserMutation.graphql";
|
import type { SettingsPageRemoveUserMutation as SettingsPageRemoveUserMutationType } from "./__generated__/SettingsPageRemoveUserMutation.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const settingsPageQuery = graphql`
|
const settingsPageQuery = graphql`
|
||||||
query SettingsPageQuery($organizationID: ID!) {
|
query SettingsPageQuery($organizationID: ID!) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type {
|
|||||||
ControlState,
|
ControlState,
|
||||||
ControlImportance,
|
ControlImportance,
|
||||||
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
|
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const updateControlMutation = graphql`
|
const updateControlMutation = graphql`
|
||||||
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
|
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { HelpCircle } from "lucide-react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { UpdateFrameworkPageUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkPageUpdateFrameworkMutation.graphql";
|
import { UpdateFrameworkPageUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkPageUpdateFrameworkMutation.graphql";
|
||||||
import { UpdateFrameworkPageQuery as UpdateFrameworkPageQueryType } from "./__generated__/UpdateFrameworkPageQuery.graphql";
|
import { UpdateFrameworkPageQuery as UpdateFrameworkPageQueryType } from "./__generated__/UpdateFrameworkPageQuery.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const updateFrameworkMutation = graphql`
|
const updateFrameworkMutation = graphql`
|
||||||
mutation UpdateFrameworkPageUpdateFrameworkMutation(
|
mutation UpdateFrameworkPageUpdateFrameworkMutation(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import PolicyEditor from "@/components/PolicyEditor";
|
|||||||
import PeopleSelector from "@/components/PeopleSelector";
|
import PeopleSelector from "@/components/PeopleSelector";
|
||||||
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
|
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
|
||||||
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
|
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const UpdatePolicyPageQuery = graphql`
|
const UpdatePolicyPageQuery = graphql`
|
||||||
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
|
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPa
|
|||||||
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
|
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
|
||||||
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
|
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 25;
|
const ITEMS_PER_PAGE = 25;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./_
|
|||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader } from "./PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
|
|
||||||
const vendorOverviewPageQuery = graphql`
|
const vendorOverviewPageQuery = graphql`
|
||||||
query VendorOverviewPageQuery($vendorId: ID!) {
|
query VendorOverviewPageQuery($vendorId: ID!) {
|
||||||
|
|||||||
Reference in New Issue
Block a user