diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 281f51297..b89b5a546 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -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() { + }> diff --git a/apps/console/src/components/PageContainer.tsx b/apps/console/src/components/PageContainer.tsx new file mode 100644 index 000000000..0b98a420c --- /dev/null +++ b/apps/console/src/components/PageContainer.tsx @@ -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 = ({ children, title }) => { + return ( + <> + {`${title} - Probo Console`} +
{children}
+ + ); +}; + +export const PageContainerSkeleton: FC<{ + title?: string; + children: ReactNode; +}> = ({ children, title }) => + title ? ( + {children} + ) : ( +
{children}
+ ); diff --git a/apps/console/src/components/PageHeader.tsx b/apps/console/src/components/PageHeader.tsx new file mode 100644 index 000000000..fed913cba --- /dev/null +++ b/apps/console/src/components/PageHeader.tsx @@ -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 ( +
+ {actions ? ( +
+ {title} + {actions} +
+ ) : ( + title + )} + {description} +
+ ); +} + +export function PageHeader({ + className, + title, + actions, + description, +}: PageHeaderProps & { className?: string }) { + return ( + {title}} + description={{description}} + actions={actions} + /> + ); +} + +export function PageHeading({ + children, + className, +}: { + className?: string; + children: ReactNode; +}) { + return

{children}

; +} + +const PageHeadingSkeleton: FC = () => { + return ( +
+
+
+ ); +}; + +export function PageDescription({ + children, + className, +}: { + className?: string; + children: ReactNode; +}) { + return ( +

{children}

+ ); +} + +export function PageDescriptionSkeleton() { + return ( +
+
+
+ ); +} + +export const PageHeaderSkeleton: FC<{ + className?: string; + title?: string; + actions?: ReactNode; + description?: ReactNode; + withDescription?: boolean; +}> = ({ className, title, actions, description, withDescription = false }) => { + return ( + {title} : + } + description={ + description ? ( + {description} + ) : withDescription ? ( + + ) : null + } + actions={actions} + /> + ); +}; diff --git a/apps/console/src/components/PageTemplate.tsx b/apps/console/src/components/PageTemplate.tsx new file mode 100644 index 000000000..bf2afd44c --- /dev/null +++ b/apps/console/src/components/PageTemplate.tsx @@ -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 = ({ + actions, + title, + description, + children, +}) => { + return ( + + + {children} + + ); +}; + +export const PageTemplateSkeleton: FC<{ + title?: string; + description?: ReactNode; + withDescription?: boolean; + actions?: ReactNode; + children: ReactNode; +}> = ({ actions, children, description, withDescription = false, title }) => { + return ( + + + {children} + + ); +}; diff --git a/apps/console/src/pages/ControlOverviewPage.tsx b/apps/console/src/pages/ControlOverviewPage.tsx index f9c1b8b51..e8909bd2c 100644 --- a/apps/console/src/pages/ControlOverviewPage.tsx +++ b/apps/console/src/pages/ControlOverviewPage.tsx @@ -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 => { diff --git a/apps/console/src/pages/CreateControlPage.tsx b/apps/console/src/pages/CreateControlPage.tsx index ecb97370f..ed4e95343 100644 --- a/apps/console/src/pages/CreateControlPage.tsx +++ b/apps/console/src/pages/CreateControlPage.tsx @@ -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( diff --git a/apps/console/src/pages/CreatePeoplePage.tsx b/apps/console/src/pages/CreatePeoplePage.tsx index 055105c52..ea2569a99 100644 --- a/apps/console/src/pages/CreatePeoplePage.tsx +++ b/apps/console/src/pages/CreatePeoplePage.tsx @@ -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( diff --git a/apps/console/src/pages/CreatePolicyPage.tsx b/apps/console/src/pages/CreatePolicyPage.tsx index 858c5a0c0..1b274a69a 100644 --- a/apps/console/src/pages/CreatePolicyPage.tsx +++ b/apps/console/src/pages/CreatePolicyPage.tsx @@ -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!) { diff --git a/apps/console/src/pages/FrameworkListPage.tsx b/apps/console/src/pages/FrameworkListPage.tsx index e381ebb29..360a39852 100644 --- a/apps/console/src/pages/FrameworkListPage.tsx +++ b/apps/console/src/pages/FrameworkListPage.tsx @@ -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 ( - <> - - Frameworks - Probo - -
- - - - - - - - Import Framework - -
-
- - -

- Upload a JSON file containing your framework definition. -

-
-
-
-
- -
- } - /> -
-
- {frameworks.map((framework) => { - const validatedControls = framework.controls.edges.filter( - (edge) => edge?.node?.state === "IMPLEMENTED" - ).length; - const totalControls = framework.controls.edges.length; - - return ( - - - - {framework.name.split(" ")[0]} - -
- } - status={ - validatedControls === totalControls - ? "Compliant" - : undefined - } - progress={ - validatedControls === totalControls - ? "All controls validated" - : `${validatedControls}/${totalControls} Controls validated` - } + + + + Import Framework + +
+
+ + - - ); - })} -
+

+ Upload a JSON file containing your framework definition. +

+
+
+ + + +
+ } + > +
+
+ {frameworks.map((framework) => { + const validatedControls = framework.controls.edges.filter( + (edge) => edge?.node?.state === "IMPLEMENTED" + ).length; + const totalControls = framework.controls.edges.length; + + return ( + + + + {framework.name.split(" ")[0]} + +
+ } + status={ + validatedControls === totalControls + ? "Compliant" + : undefined + } + progress={ + validatedControls === totalControls + ? "All controls validated" + : `${validatedControls}/${totalControls} Controls validated` + } + /> + + ); + })}
- + ); } -function FrameworkListPageFallback() { +export function FrameworkListPageSkeleton() { return ( -
-
-
-
-
+ +
+
+
+ } + >
{[1, 2].map((i) => ( @@ -310,7 +305,7 @@ function FrameworkListPageFallback() { ))}
-
+
); } @@ -326,13 +321,8 @@ export default function FrameworkListPage() { }, [loadQuery, organizationId]); return ( - <> - - Frameworks - Probo Console - - }> - {queryRef && } - - + }> + {queryRef && } + ); } diff --git a/apps/console/src/pages/FrameworkOverviewPage.tsx b/apps/console/src/pages/FrameworkOverviewPage.tsx index f8257757b..160ac08e5 100644 --- a/apps/console/src/pages/FrameworkOverviewPage.tsx +++ b/apps/console/src/pages/FrameworkOverviewPage.tsx @@ -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!) { diff --git a/apps/console/src/pages/PageHeader.tsx b/apps/console/src/pages/PageHeader.tsx deleted file mode 100644 index c077f7fde..000000000 --- a/apps/console/src/pages/PageHeader.tsx +++ /dev/null @@ -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 ( -
- {actions ? ( -
- - {actions} -
- ) : ( - - )} - {description && {description}} - {children} -
- ); -} - -export function PageHeading({ - title, - className, -}: { - className?: string; - title: string; -}) { - return

{title}

; -} - -export function PageDescription({ - children, - className, -}: { - className?: string; - children: ReactNode; -}) { - return ( -

{children}

- ); -} diff --git a/apps/console/src/pages/PeopleListPage.tsx b/apps/console/src/pages/PeopleListPage.tsx index 51cbd8087..223321f0e 100644 --- a/apps/console/src/pages/PeopleListPage.tsx +++ b/apps/console/src/pages/PeopleListPage.tsx @@ -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; diff --git a/apps/console/src/pages/PeopleOverviewPage.tsx b/apps/console/src/pages/PeopleOverviewPage.tsx index d6e80a201..132e5d516 100644 --- a/apps/console/src/pages/PeopleOverviewPage.tsx +++ b/apps/console/src/pages/PeopleOverviewPage.tsx @@ -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!) { diff --git a/apps/console/src/pages/PolicyListPage.tsx b/apps/console/src/pages/PolicyListPage.tsx index cc7cd6761..8ae1bef5d 100644 --- a/apps/console/src/pages/PolicyListPage.tsx +++ b/apps/console/src/pages/PolicyListPage.tsx @@ -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!) { diff --git a/apps/console/src/pages/PolicyOverviewPage.tsx b/apps/console/src/pages/PolicyOverviewPage.tsx index 6fd35838d..a5bf197e3 100644 --- a/apps/console/src/pages/PolicyOverviewPage.tsx +++ b/apps/console/src/pages/PolicyOverviewPage.tsx @@ -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({ + + {policy.status === "ACTIVE" ? "Active" : "Draft"} + + {policy.owner && ( +
+ + + {policy.owner.fullName} + +
+ )} + {policy.reviewDate && ( +
+ + Review: {formatDate(policy.reviewDate)} +
+ )} +
+ } actions={
} - > -
- - {policy.status === "ACTIVE" ? "Active" : "Draft"} - - {policy.owner && ( -
- - - {policy.owner.fullName} - -
- )} - {policy.reviewDate && ( -
- - Review: {formatDate(policy.reviewDate)} -
- )} -
- + />
diff --git a/apps/console/src/pages/SettingsPage.tsx b/apps/console/src/pages/SettingsPage.tsx index c164d8021..4f630a442 100644 --- a/apps/console/src/pages/SettingsPage.tsx +++ b/apps/console/src/pages/SettingsPage.tsx @@ -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!) { diff --git a/apps/console/src/pages/UpdateControlPage.tsx b/apps/console/src/pages/UpdateControlPage.tsx index e0775eba2..b5bd638f5 100644 --- a/apps/console/src/pages/UpdateControlPage.tsx +++ b/apps/console/src/pages/UpdateControlPage.tsx @@ -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!) { diff --git a/apps/console/src/pages/UpdateFrameworkPage.tsx b/apps/console/src/pages/UpdateFrameworkPage.tsx index dbf9c4f3a..b9312e237 100644 --- a/apps/console/src/pages/UpdateFrameworkPage.tsx +++ b/apps/console/src/pages/UpdateFrameworkPage.tsx @@ -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( diff --git a/apps/console/src/pages/UpdatePolicyPage.tsx b/apps/console/src/pages/UpdatePolicyPage.tsx index f65aa3c99..69189f9b2 100644 --- a/apps/console/src/pages/UpdatePolicyPage.tsx +++ b/apps/console/src/pages/UpdatePolicyPage.tsx @@ -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!) { diff --git a/apps/console/src/pages/VendorListPage.tsx b/apps/console/src/pages/VendorListPage.tsx index f409347f7..d86398062 100644 --- a/apps/console/src/pages/VendorListPage.tsx +++ b/apps/console/src/pages/VendorListPage.tsx @@ -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; diff --git a/apps/console/src/pages/VendorOverviewPage.tsx b/apps/console/src/pages/VendorOverviewPage.tsx index 73956d7c2..0c9ad4023 100644 --- a/apps/console/src/pages/VendorOverviewPage.tsx +++ b/apps/console/src/pages/VendorOverviewPage.tsx @@ -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!) {