Add all missing page skeletons and use them as fallback
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -13,6 +13,23 @@ 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";
|
import { FrameworkListPageSkeleton } from "./pages/FrameworkListPage";
|
||||||
|
import { ControlOverviewPageSkeleton } from "./pages/ControlOverviewPage";
|
||||||
|
import { CreateControlPageSkeleton } from "./pages/CreateControlPage";
|
||||||
|
import { UpdateFrameworkPageSkeleton } from "./pages/UpdateFrameworkPage";
|
||||||
|
import { UpdateControlPageSkeleton } from "./pages/UpdateControlPage";
|
||||||
|
import { CreateFrameworkPageSkeleton } from "./pages/CreateFrameworkPage";
|
||||||
|
import { CreateOrganizationPageSkeleton } from "./pages/CreateOrganizationPage";
|
||||||
|
import { CreatePeoplePageSkeleton } from "./pages/CreatePeoplePage";
|
||||||
|
import { CreatePolicyPageSkeleton } from "./pages/CreatePolicyPage";
|
||||||
|
import { FrameworkOverviewPageSkeleton } from "./pages/FrameworkOverviewPage";
|
||||||
|
import { PeopleListPageSkeleton } from "./pages/PeopleListPage";
|
||||||
|
import { PeopleOverviewPageSkeleton } from "./pages/PeopleOverviewPage";
|
||||||
|
import { PolicyListPageSkeleton } from "./pages/PolicyListPage";
|
||||||
|
import { PolicyOverviewPageSkeleton } from "./pages/PolicyOverviewPage";
|
||||||
|
import { UpdatePolicyPageSkeleton } from "./pages/UpdatePolicyPage";
|
||||||
|
import { VendorListPageSkeleton } from "./pages/VendorListPage";
|
||||||
|
import { VendorOverviewPageSkeleton } from "./pages/VendorOverviewPage";
|
||||||
|
import { SettingsPageSkeleton } from "./pages/SettingsPage";
|
||||||
|
|
||||||
posthog.init(process.env.POSTHOG_KEY!, {
|
posthog.init(process.env.POSTHOG_KEY!, {
|
||||||
api_host: process.env.POSTHOG_HOST,
|
api_host: process.env.POSTHOG_HOST,
|
||||||
@@ -85,7 +102,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="create"
|
path="create"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<CreateOrganizationPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<CreateOrganizationPage />
|
<CreateOrganizationPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -106,7 +123,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="people"
|
path="people"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<PeopleListPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<PeopleListPage />
|
<PeopleListPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -116,7 +133,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="people/create"
|
path="people/create"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<CreatePeoplePageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<CreatePeoplePage />
|
<CreatePeoplePage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -126,7 +143,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="people/:peopleId"
|
path="people/:peopleId"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<PeopleOverviewPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<PeopleOverviewPage />
|
<PeopleOverviewPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -136,7 +153,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="vendors"
|
path="vendors"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<VendorListPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<VendorListPage />
|
<VendorListPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -156,7 +173,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="frameworks/create"
|
path="frameworks/create"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<CreateFrameworkPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<CreateFrameworkPage />
|
<CreateFrameworkPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -166,7 +183,9 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId"
|
path="frameworks/:frameworkId"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense
|
||||||
|
fallback={<FrameworkOverviewPageSkeleton />}
|
||||||
|
>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<FrameworkOverviewPage />
|
<FrameworkOverviewPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -176,7 +195,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/update"
|
path="frameworks/:frameworkId/update"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<UpdateFrameworkPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<UpdateFrameworkPage />
|
<UpdateFrameworkPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -186,7 +205,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/controls/create"
|
path="frameworks/:frameworkId/controls/create"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<CreateControlPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<CreateControlPage />
|
<CreateControlPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -196,7 +215,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/controls/:controlId"
|
path="frameworks/:frameworkId/controls/:controlId"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<ControlOverviewPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<ControlOverviewPage />
|
<ControlOverviewPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -206,7 +225,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/controls/:controlId/update"
|
path="frameworks/:frameworkId/controls/:controlId/update"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<UpdateControlPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<UpdateControlPage />
|
<UpdateControlPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -216,7 +235,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="vendors/:vendorId"
|
path="vendors/:vendorId"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<VendorOverviewPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<VendorOverviewPage />
|
<VendorOverviewPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -227,7 +246,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="policies"
|
path="policies"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<PolicyListPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<PolicyListPage />
|
<PolicyListPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -237,7 +256,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="policies/create"
|
path="policies/create"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<CreatePolicyPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<CreatePolicyPage />
|
<CreatePolicyPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -247,7 +266,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="policies/:policyId"
|
path="policies/:policyId"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<PolicyOverviewPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<PolicyOverviewPage />
|
<PolicyOverviewPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -257,7 +276,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="policies/:policyId/update"
|
path="policies/:policyId/update"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<UpdatePolicyPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<UpdatePolicyPage />
|
<UpdatePolicyPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
@@ -267,7 +286,7 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="settings"
|
path="settings"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense fallback={<SettingsPageSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<SettingsPage />
|
<SettingsPage />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export function PageHeading({
|
|||||||
|
|
||||||
const PageHeadingSkeleton: FC = () => {
|
const PageHeadingSkeleton: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="py-[3px]">
|
<div className="py-[3px] w-2/5">
|
||||||
<div className="bg-muted animate rounded-lg w-60 h-7.5" />
|
<div className="bg-muted animate rounded-lg h-7.5" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
import { Suspense, useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -21,7 +20,7 @@ import {
|
|||||||
CreateControlPageCreateControlMutation,
|
CreateControlPageCreateControlMutation,
|
||||||
ControlImportance,
|
ControlImportance,
|
||||||
} from "./__generated__/CreateControlPageCreateControlMutation.graphql";
|
} from "./__generated__/CreateControlPageCreateControlMutation.graphql";
|
||||||
import { PageHeader } from "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const createControlMutation = graphql`
|
const createControlMutation = graphql`
|
||||||
mutation CreateControlPageCreateControlMutation(
|
mutation CreateControlPageCreateControlMutation(
|
||||||
@@ -182,90 +181,93 @@ function CreateControlPageContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Create Control"
|
||||||
<title>Create Control - Probo</title>
|
description="Create a new control for your framework"
|
||||||
</Helmet>
|
>
|
||||||
<div className="container">
|
<Card className="max-w-2xl">
|
||||||
<PageHeader
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
className="mb-17"
|
<EditableField
|
||||||
title="Create Control"
|
label="Name"
|
||||||
description="Create a new control for your framework"
|
value={formData.name}
|
||||||
/>
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="max-w-2xl">
|
<EditableField
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
label="Category"
|
||||||
<EditableField
|
value={formData.category}
|
||||||
label="Name"
|
onChange={(value) => handleFieldChange("category", value)}
|
||||||
value={formData.name}
|
required
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
helpText="The category this control belongs to"
|
||||||
required
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Category"
|
label="Description"
|
||||||
value={formData.category}
|
value={formData.description}
|
||||||
onChange={(value) => handleFieldChange("category", value)}
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
required
|
required
|
||||||
helpText="The category this control belongs to"
|
multiline
|
||||||
/>
|
helpText="Provide a detailed description of the control"
|
||||||
|
/>
|
||||||
|
|
||||||
<EditableField
|
<div className="space-y-2">
|
||||||
label="Description"
|
<Label htmlFor="importance" className="text-sm font-medium">
|
||||||
value={formData.description}
|
Importance
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
</Label>
|
||||||
required
|
<Select
|
||||||
multiline
|
value={formData.importance}
|
||||||
helpText="Provide a detailed description of the control"
|
onValueChange={(value) =>
|
||||||
/>
|
handleFieldChange("importance", value as ControlImportance)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select importance" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
||||||
|
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
||||||
|
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex justify-end gap-3">
|
||||||
<Label htmlFor="importance" className="text-sm font-medium">
|
<Button
|
||||||
Importance
|
type="button"
|
||||||
</Label>
|
variant="outline"
|
||||||
<Select
|
onClick={() =>
|
||||||
value={formData.importance}
|
navigate(
|
||||||
onValueChange={(value) =>
|
`/organizations/${organizationId}/frameworks/${frameworkId}`
|
||||||
handleFieldChange("importance", value as ControlImportance)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
Cancel
|
||||||
<SelectValue placeholder="Select importance" />
|
</Button>
|
||||||
</SelectTrigger>
|
<Button type="submit" disabled={isInFlight}>
|
||||||
<SelectContent>
|
{isInFlight ? "Creating..." : "Create Control"}
|
||||||
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
</Button>
|
||||||
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
</div>
|
||||||
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
</form>
|
||||||
</SelectContent>
|
</Card>
|
||||||
</Select>
|
</PageTemplate>
|
||||||
</div>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
export function CreateControlPageSkeleton() {
|
||||||
<Button
|
return (
|
||||||
type="button"
|
<PageTemplateSkeleton
|
||||||
variant="outline"
|
title="Create Control"
|
||||||
onClick={() =>
|
description="Create a new control for your framework"
|
||||||
navigate(
|
>
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}`
|
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
||||||
)
|
</PageTemplateSkeleton>
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight}>
|
|
||||||
{isInFlight ? "Creating..." : "Create Control"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateControlPage() {
|
export default function CreateControlPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<CreateControlPageSkeleton />}>
|
||||||
<CreateControlPageContent />
|
<CreateControlPageContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Suspense, useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -11,6 +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 { CreateFrameworkPageCreateFrameworkMutation } from "./__generated__/CreateFrameworkPageCreateFrameworkMutation.graphql";
|
import { CreateFrameworkPageCreateFrameworkMutation } from "./__generated__/CreateFrameworkPageCreateFrameworkMutation.graphql";
|
||||||
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const createFrameworkMutation = graphql`
|
const createFrameworkMutation = graphql`
|
||||||
mutation CreateFrameworkPageCreateFrameworkMutation(
|
mutation CreateFrameworkPageCreateFrameworkMutation(
|
||||||
@@ -69,7 +69,7 @@ function EditableField({
|
|||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full resize-none",
|
"w-full resize-none",
|
||||||
required && !value && "border-red-500",
|
required && !value && "border-red-500"
|
||||||
)}
|
)}
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
rows={4}
|
rows={4}
|
||||||
@@ -101,7 +101,7 @@ function CreateFrameworkPageContent() {
|
|||||||
|
|
||||||
const [commit, isInFlight] =
|
const [commit, isInFlight] =
|
||||||
useMutation<CreateFrameworkPageCreateFrameworkMutation>(
|
useMutation<CreateFrameworkPageCreateFrameworkMutation>(
|
||||||
createFrameworkMutation,
|
createFrameworkMutation
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
||||||
@@ -125,7 +125,7 @@ function CreateFrameworkPageContent() {
|
|||||||
|
|
||||||
const connectionId = ConnectionHandler.getConnectionID(
|
const connectionId = ConnectionHandler.getConnectionID(
|
||||||
organizationId!,
|
organizationId!,
|
||||||
"FrameworkListPage_frameworks",
|
"FrameworkListPage_frameworks"
|
||||||
);
|
);
|
||||||
|
|
||||||
commit({
|
commit({
|
||||||
@@ -153,7 +153,7 @@ function CreateFrameworkPageContent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
`/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`,
|
`/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
@@ -167,60 +167,62 @@ function CreateFrameworkPageContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Create Framework"
|
||||||
<title>Create Framework - Probo</title>
|
description="Create a new framework to organize your controls"
|
||||||
</Helmet>
|
>
|
||||||
<div className="container mx-auto py-6">
|
<Card className="max-w-2xl">
|
||||||
<div className="mb-6">
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
<h1 className="text-2xl font-bold">Create Framework</h1>
|
<EditableField
|
||||||
<p className="text-muted-foreground">
|
label="Name"
|
||||||
Create a new framework to organize your controls
|
value={formData.name}
|
||||||
</p>
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
</div>
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="max-w-2xl">
|
<EditableField
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
label="Description"
|
||||||
<EditableField
|
value={formData.description}
|
||||||
label="Name"
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
value={formData.name}
|
required
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
multiline
|
||||||
required
|
helpText="Provide a detailed description of the framework"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<div className="flex justify-end gap-3">
|
||||||
label="Description"
|
<Button
|
||||||
value={formData.description}
|
type="button"
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
variant="outline"
|
||||||
required
|
onClick={() =>
|
||||||
multiline
|
navigate(`/organizations/${organizationId}/frameworks`)
|
||||||
helpText="Provide a detailed description of the framework"
|
}
|
||||||
/>
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isInFlight}>
|
||||||
|
{isInFlight ? "Creating..." : "Create Framework"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
export function CreateFrameworkPageSkeleton() {
|
||||||
<Button
|
return (
|
||||||
type="button"
|
<PageTemplateSkeleton
|
||||||
variant="outline"
|
title="Create Framework"
|
||||||
onClick={() =>
|
description="Create a new framework to organize your controls"
|
||||||
navigate(`/organizations/${organizationId}/frameworks`)
|
>
|
||||||
}
|
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
||||||
>
|
</PageTemplateSkeleton>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight}>
|
|
||||||
{isInFlight ? "Creating..." : "Create Framework"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateFrameworkPage() {
|
export default function CreateFrameworkPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<CreateFrameworkPageSkeleton />}>
|
||||||
<CreateFrameworkPageContent />
|
<CreateFrameworkPageContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
useLazyLoadQuery,
|
useLazyLoadQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -21,6 +20,7 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { HelpCircle } from "lucide-react";
|
import { HelpCircle } from "lucide-react";
|
||||||
import { CreateOrganizationPageCreateOrganizationMutation } from "./__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql";
|
import { CreateOrganizationPageCreateOrganizationMutation } from "./__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql";
|
||||||
import { CreateOrganizationPageViewerQuery } from "./__generated__/CreateOrganizationPageViewerQuery.graphql";
|
import { CreateOrganizationPageViewerQuery } from "./__generated__/CreateOrganizationPageViewerQuery.graphql";
|
||||||
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const createOrganizationMutation = graphql`
|
const createOrganizationMutation = graphql`
|
||||||
mutation CreateOrganizationPageCreateOrganizationMutation(
|
mutation CreateOrganizationPageCreateOrganizationMutation(
|
||||||
@@ -81,16 +81,27 @@ function EditableField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CreateOrganizationPageSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton
|
||||||
|
title="Create Organization"
|
||||||
|
description="Create a new organization to manage your compliance and security needs."
|
||||||
|
>
|
||||||
|
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateOrganizationPage() {
|
export default function CreateOrganizationPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>(
|
const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>(
|
||||||
viewerQuery,
|
viewerQuery,
|
||||||
{},
|
{}
|
||||||
);
|
);
|
||||||
const [createOrganization] =
|
const [createOrganization] =
|
||||||
useMutation<CreateOrganizationPageCreateOrganizationMutation>(
|
useMutation<CreateOrganizationPageCreateOrganizationMutation>(
|
||||||
createOrganizationMutation,
|
createOrganizationMutation
|
||||||
);
|
);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -114,7 +125,7 @@ export default function CreateOrganizationPage() {
|
|||||||
connections: [
|
connections: [
|
||||||
ConnectionHandler.getConnectionID(
|
ConnectionHandler.getConnectionID(
|
||||||
data.viewer.id,
|
data.viewer.id,
|
||||||
"OrganizationSwitcher_organizations",
|
"OrganizationSwitcher_organizations"
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -138,46 +149,33 @@ export default function CreateOrganizationPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Create Organization"
|
||||||
<title>Create Organization - Probo</title>
|
description="Create a new organization to manage your compliance and security needs."
|
||||||
</Helmet>
|
>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Organization Details</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the basic information about your organization.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<EditableField
|
||||||
|
label="Organization Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
helpText="The name of your organization as it will appear throughout the platform."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<Button type="submit" className="w-full">
|
||||||
<div>
|
Create Organization
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
</Button>
|
||||||
Create Organization
|
</CardContent>
|
||||||
</h1>
|
</Card>
|
||||||
<p className="text-muted-foreground">
|
</form>
|
||||||
Create a new organization to manage your compliance and security
|
</PageTemplate>
|
||||||
needs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Organization Details</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Enter the basic information about your organization.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<EditableField
|
|
||||||
label="Organization Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
|
||||||
helpText="The name of your organization as it will appear throughout the platform."
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
Create Organization
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Suspense, useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -10,7 +9,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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const createPeopleMutation = graphql`
|
const createPeopleMutation = graphql`
|
||||||
mutation CreatePeoplePageCreatePeopleMutation(
|
mutation CreatePeoplePageCreatePeopleMutation(
|
||||||
@@ -150,177 +149,166 @@ function CreatePeoplePageContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Create Person"
|
||||||
<title>Create Person - Probo Console</title>
|
description="Add a new person interacting with organization"
|
||||||
</Helmet>
|
>
|
||||||
<div className="container">
|
<form onSubmit={handleSubmit}>
|
||||||
<PageHeader
|
<div className="max-w-2xl space-y-6">
|
||||||
className="mb-17"
|
<EditableField
|
||||||
title="Add a Person"
|
label="Full Name"
|
||||||
description="Add a new person interacting with organization"
|
value={formData.fullName}
|
||||||
/>
|
onChange={(value) => handleFieldChange("fullName", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<EditableField
|
||||||
<div className="max-w-4xl space-y-6">
|
label="Primary Email"
|
||||||
<EditableField
|
value={formData.primaryEmailAddress}
|
||||||
label="Full Name"
|
type="email"
|
||||||
value={formData.fullName}
|
onChange={(value) =>
|
||||||
onChange={(value) => handleFieldChange("fullName", value)}
|
handleFieldChange("primaryEmailAddress", value)
|
||||||
required
|
}
|
||||||
/>
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<EditableField
|
<div className="space-y-2">
|
||||||
label="Primary Email"
|
<div className="flex items-center gap-2">
|
||||||
value={formData.primaryEmailAddress}
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
type="email"
|
<Label className="text-sm">Additional Email Addresses</Label>
|
||||||
onChange={(value) =>
|
|
||||||
handleFieldChange("primaryEmailAddress", value)
|
|
||||||
}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
|
||||||
<Label className="text-sm">Additional Email Addresses</Label>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formData.additionalEmailAddresses.map((email, index) => (
|
|
||||||
<div key={index} className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newEmails = [
|
|
||||||
...formData.additionalEmailAddresses,
|
|
||||||
];
|
|
||||||
newEmails[index] = e.target.value;
|
|
||||||
handleFieldChange(
|
|
||||||
"additionalEmailAddresses",
|
|
||||||
newEmails
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const newEmails =
|
|
||||||
formData.additionalEmailAddresses.filter(
|
|
||||||
(_, i) => i !== index
|
|
||||||
);
|
|
||||||
handleFieldChange(
|
|
||||||
"additionalEmailAddresses",
|
|
||||||
newEmails
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
handleFieldChange("additionalEmailAddresses", [
|
|
||||||
...formData.additionalEmailAddresses,
|
|
||||||
"",
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Email
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.additionalEmailAddresses.map((email, index) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newEmails = [...formData.additionalEmailAddresses];
|
||||||
|
newEmails[index] = e.target.value;
|
||||||
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const newEmails =
|
||||||
|
formData.additionalEmailAddresses.filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
handleFieldChange("additionalEmailAddresses", [
|
||||||
|
...formData.additionalEmailAddresses,
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-lg font-medium">Additional Information</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Additional details about the person
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
<div className="flex items-center gap-2">
|
||||||
Additional Information
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
</h2>
|
<Label className="text-sm">Kind</Label>
|
||||||
<p className="text-sm text-gray-500">
|
</div>
|
||||||
Additional details about the person
|
<div className="flex gap-2">
|
||||||
</p>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
||||||
<div className="space-y-4">
|
className={cn(
|
||||||
<div className="space-y-2">
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
<div className="flex items-center gap-2">
|
formData.kind === "EMPLOYEE"
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
|
||||||
<Label className="text-sm">Kind</Label>
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
</div>
|
)}
|
||||||
<div className="flex gap-2">
|
>
|
||||||
<button
|
Employee
|
||||||
type="button"
|
</button>
|
||||||
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
<button
|
||||||
className={cn(
|
type="button"
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
||||||
formData.kind === "EMPLOYEE"
|
className={cn(
|
||||||
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
formData.kind === "CONTRACTOR"
|
||||||
)}
|
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
|
||||||
>
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
Employee
|
)}
|
||||||
</button>
|
>
|
||||||
<button
|
Contractor
|
||||||
type="button"
|
</button>
|
||||||
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
<button
|
||||||
className={cn(
|
type="button"
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
onClick={() =>
|
||||||
formData.kind === "CONTRACTOR"
|
handleFieldChange("kind", "SERVICE_ACCOUNT")
|
||||||
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
|
}
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
className={cn(
|
||||||
)}
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
>
|
formData.kind === "SERVICE_ACCOUNT"
|
||||||
Contractor
|
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||||
</button>
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
<button
|
)}
|
||||||
type="button"
|
>
|
||||||
onClick={() =>
|
Service Account
|
||||||
handleFieldChange("kind", "SERVICE_ACCOUNT")
|
</button>
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.kind === "SERVICE_ACCOUNT"
|
|
||||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Service Account
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
</div>
|
||||||
<Button
|
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||||
type="button"
|
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
|
||||||
variant="outline"
|
Cancel
|
||||||
onClick={() => navigate(-1)}
|
</Button>
|
||||||
>
|
<Button
|
||||||
Cancel
|
type="submit"
|
||||||
</Button>
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
<Button
|
>
|
||||||
type="submit"
|
Create Person
|
||||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
</Button>
|
||||||
>
|
</div>
|
||||||
Create Person
|
</form>
|
||||||
</Button>
|
</PageTemplate>
|
||||||
</div>
|
);
|
||||||
</form>
|
}
|
||||||
</div>
|
|
||||||
</>
|
export function CreatePeoplePageSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton
|
||||||
|
title="Create Person"
|
||||||
|
description="Add a new person interacting with organization"
|
||||||
|
>
|
||||||
|
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
||||||
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreatePeoplePage() {
|
export default function CreatePeoplePage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={<CreatePeoplePageSkeleton />}>
|
||||||
<CreatePeoplePageContent />
|
<CreatePeoplePageContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
PreloadedQuery,
|
PreloadedQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -21,7 +20,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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const CreatePolicyQuery = graphql`
|
const CreatePolicyQuery = graphql`
|
||||||
query CreatePolicyPageQuery($organizationId: ID!) {
|
query CreatePolicyPageQuery($organizationId: ID!) {
|
||||||
@@ -156,136 +155,119 @@ function CreatePolicyForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Create Policy"
|
||||||
<title>Create Policy - Probo Console</title>
|
description="Create a new policy for your organization"
|
||||||
</Helmet>
|
>
|
||||||
<div className="container">
|
<form onSubmit={handleSubmit}>
|
||||||
<PageHeader
|
<div className="grid gap-6">
|
||||||
className="mb-17"
|
<Card>
|
||||||
title="Create Policy"
|
<CardHeader>
|
||||||
description="Create a new policy for your organization"
|
<CardTitle>Policy Information</CardTitle>
|
||||||
/>
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Policy Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder="Enter policy name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<div className="space-y-2">
|
||||||
<div className="grid gap-6">
|
<Label htmlFor="content">Policy Content</Label>
|
||||||
<Card>
|
<div className="min-h-[300px]">
|
||||||
<CardHeader>
|
<PolicyEditor
|
||||||
<CardTitle>Policy Information</CardTitle>
|
initialContent={content}
|
||||||
</CardHeader>
|
onChange={(html) => setContent(html)}
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Policy Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
placeholder="Enter policy name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="content">Policy Content</Label>
|
<Label>Status</Label>
|
||||||
<div className="min-h-[300px]">
|
<RadioGroup
|
||||||
<PolicyEditor
|
value={status}
|
||||||
initialContent={content}
|
onValueChange={(value: "DRAFT" | "ACTIVE") =>
|
||||||
onChange={(html) => setContent(html)}
|
setStatus(value)
|
||||||
/>
|
}
|
||||||
|
className="flex space-x-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="DRAFT" id="draft" />
|
||||||
|
<Label htmlFor="draft" className="cursor-pointer">
|
||||||
|
Draft
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="ACTIVE" id="active" />
|
||||||
|
<Label htmlFor="active" className="cursor-pointer">
|
||||||
|
Active
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Status</Label>
|
<Label htmlFor="owner" className="flex items-center gap-2">
|
||||||
<RadioGroup
|
<User className="h-4 w-4" />
|
||||||
value={status}
|
Policy Owner
|
||||||
onValueChange={(value: "DRAFT" | "ACTIVE") =>
|
</Label>
|
||||||
setStatus(value)
|
<PeopleSelector
|
||||||
}
|
organizationRef={data.organization}
|
||||||
className="flex space-x-4"
|
selectedPersonId={ownerId}
|
||||||
>
|
onSelect={setOwnerId}
|
||||||
<div className="flex items-center space-x-2">
|
placeholder="Select policy owner"
|
||||||
<RadioGroupItem value="DRAFT" id="draft" />
|
required
|
||||||
<Label htmlFor="draft" className="cursor-pointer">
|
/>
|
||||||
Draft
|
</div>
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="ACTIVE" id="active" />
|
|
||||||
<Label htmlFor="active" className="cursor-pointer">
|
|
||||||
Active
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="owner" className="flex items-center gap-2">
|
<Label htmlFor="reviewDate" className="flex items-center gap-2">
|
||||||
<User className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
Policy Owner
|
Review Date
|
||||||
</Label>
|
</Label>
|
||||||
<PeopleSelector
|
<Input
|
||||||
organizationRef={data.organization}
|
id="reviewDate"
|
||||||
selectedPersonId={ownerId}
|
type="date"
|
||||||
onSelect={setOwnerId}
|
value={reviewDate}
|
||||||
placeholder="Select policy owner"
|
onChange={(e) => setReviewDate(e.target.value)}
|
||||||
required
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex justify-end gap-4">
|
||||||
<Label
|
<Button
|
||||||
htmlFor="reviewDate"
|
type="button"
|
||||||
className="flex items-center gap-2"
|
variant="outline"
|
||||||
>
|
onClick={() =>
|
||||||
<Calendar className="h-4 w-4" />
|
navigate(`/organizations/${organizationId}/policies`)
|
||||||
Review Date
|
}
|
||||||
</Label>
|
>
|
||||||
<Input
|
Cancel
|
||||||
id="reviewDate"
|
</Button>
|
||||||
type="date"
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
value={reviewDate}
|
{isSubmitting ? "Creating..." : "Create Policy"}
|
||||||
onChange={(e) => setReviewDate(e.target.value)}
|
</Button>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/organizations/${organizationId}/policies`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Creating..." : "Create Policy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreatePolicyPageFallback() {
|
export function CreatePolicyPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-6">
|
<PageTemplateSkeleton
|
||||||
<div className="flex items-center mb-6">
|
title="Create Policy"
|
||||||
<div className="mr-4">
|
description="Create a new policy for your organization"
|
||||||
<div className="h-12 w-12 bg-muted animate-pulse rounded-lg" />
|
>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded mb-2" />
|
|
||||||
<div className="h-4 w-64 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted animate-pulse rounded-lg h-[600px]" />
|
<div className="bg-muted animate-pulse rounded-lg h-[600px]" />
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,9 +282,13 @@ export default function CreatePolicyPage() {
|
|||||||
}
|
}
|
||||||
}, [organizationId, loadQuery]);
|
}, [organizationId, loadQuery]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <CreatePolicyPageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<CreatePolicyPageFallback />}>
|
<Suspense fallback={<CreatePolicyPageSkeleton />}>
|
||||||
{queryRef && <CreatePolicyForm queryRef={queryRef} />}
|
{<CreatePolicyForm queryRef={queryRef} />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,6 +320,10 @@ export default function FrameworkListPage() {
|
|||||||
loadQuery({ organizationId: organizationId! });
|
loadQuery({ organizationId: organizationId! });
|
||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <FrameworkListPageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
||||||
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
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 { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { PageHeader } from "@/components/PageHeader";
|
|
||||||
|
|
||||||
const FrameworkOverviewPageQuery = graphql`
|
const FrameworkOverviewPageQuery = graphql`
|
||||||
query FrameworkOverviewPageQuery($frameworkId: ID!) {
|
query FrameworkOverviewPageQuery($frameworkId: ID!) {
|
||||||
@@ -169,32 +168,29 @@ function FrameworkOverviewPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container space-y-6">
|
<PageTemplate
|
||||||
<PageHeader
|
title={framework.name ?? ""}
|
||||||
className="mb-10"
|
description={framework.description ?? ""}
|
||||||
title={framework.name ?? ""}
|
actions={
|
||||||
description={framework.description ?? ""}
|
<div className="flex gap-4">
|
||||||
actions={
|
<Button variant="outline" asChild>
|
||||||
<div className="flex gap-4">
|
<Link
|
||||||
<Button variant="outline" asChild>
|
to={`/organizations/${organizationId}/frameworks/${framework.id}/update`}
|
||||||
<Link
|
>
|
||||||
to={`/organizations/${organizationId}/frameworks/${framework.id}/update`}
|
Edit Framework
|
||||||
>
|
</Link>
|
||||||
Edit Framework
|
</Button>
|
||||||
</Link>
|
<Button asChild>
|
||||||
</Button>
|
<Link
|
||||||
<Button asChild>
|
to={`/organizations/${organizationId}/frameworks/${framework.id}/controls/create`}
|
||||||
<Link
|
>
|
||||||
to={`/organizations/${organizationId}/frameworks/${framework.id}/controls/create`}
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
>
|
Create Control
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
</Link>
|
||||||
Create Control
|
</Button>
|
||||||
</Link>
|
</div>
|
||||||
</Button>
|
}
|
||||||
</div>
|
>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
{categories.map((category) => {
|
{categories.map((category) => {
|
||||||
const isExpanded = expandedCategories.includes(category.id);
|
const isExpanded = expandedCategories.includes(category.id);
|
||||||
@@ -326,17 +322,13 @@ function FrameworkOverviewPageContent({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FrameworkOverviewPageFallback() {
|
export function FrameworkOverviewPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background p-6">
|
<PageTemplateSkeleton>
|
||||||
<div className="mb-8">
|
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-2" />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
<Card key={i}>
|
<Card key={i}>
|
||||||
@@ -351,7 +343,7 @@ function FrameworkOverviewPageFallback() {
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,14 +357,13 @@ export default function FrameworkOverviewPage() {
|
|||||||
loadQuery({ frameworkId: frameworkId! });
|
loadQuery({ frameworkId: frameworkId! });
|
||||||
}, [loadQuery, frameworkId]);
|
}, [loadQuery, frameworkId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <FrameworkOverviewPageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<FrameworkOverviewPageSkeleton />}>
|
||||||
<Helmet>
|
{queryRef && <FrameworkOverviewPageContent queryRef={queryRef} />}
|
||||||
<title>Framework Overview - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<FrameworkOverviewPageFallback />}>
|
|
||||||
{queryRef && <FrameworkOverviewPageContent queryRef={queryRef} />}
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { UserPlus, Trash2, ChevronRight } from "lucide-react";
|
import { UserPlus, Trash2, ChevronRight } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.graphql";
|
import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.graphql";
|
||||||
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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 25;
|
const ITEMS_PER_PAGE = 25;
|
||||||
|
|
||||||
@@ -174,31 +173,25 @@ function PeopleListContent({
|
|||||||
const pageInfo = peoplesConnection.peoples.pageInfo;
|
const pageInfo = peoplesConnection.peoples.pageInfo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="People"
|
||||||
<title>People - Probo</title>
|
description="Keep track of your company's workforce and their progress
|
||||||
</Helmet>
|
towards completing tasks assigned to them."
|
||||||
<div className="container space-y-6">
|
actions={
|
||||||
<PageHeader
|
<Button
|
||||||
className="mb-17"
|
asChild
|
||||||
title="Employees"
|
variant="outline"
|
||||||
description="Keep track of your company's workforce and their progress
|
style={{ borderRadius: "0.5rem" }}
|
||||||
towards completing tasks assigned to them."
|
className="gap-2"
|
||||||
actions={
|
>
|
||||||
<Button
|
<Link to={`/organizations/${organizationId}/people/create`}>
|
||||||
asChild
|
<UserPlus className="h-4 w-4" />
|
||||||
variant="outline"
|
Add a person
|
||||||
style={{ borderRadius: "0.5rem" }}
|
</Link>
|
||||||
className="gap-2"
|
</Button>
|
||||||
>
|
}
|
||||||
<Link to={`/organizations/${organizationId}/people/create`}>
|
>
|
||||||
<UserPlus className="h-4 w-4" />
|
<div className="space-y-6">
|
||||||
Add a person
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{peoples.map((person) => (
|
{peoples.map((person) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -296,27 +289,32 @@ function PeopleListContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeopleListPageFallback() {
|
export function PeopleListPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<PageTemplateSkeleton
|
||||||
<div>
|
title="People"
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
description="Keep track of your company's workforce and their progress towards completing tasks assigned to them."
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" />
|
actions={<div className="bg-muted animate-pulse h-9 w-1/6 rounded-lg" />}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="rounded-xl border bg-card p-4 space-y-2">
|
||||||
|
<div className="h-5 w-32 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-10 w-full bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-[72px] bg-muted animate-pulse rounded-xl"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border bg-card p-4 space-y-4">
|
</PageTemplateSkeleton>
|
||||||
<div className="h-5 w-32 bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-10 w-full bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="h-[72px] bg-muted animate-pulse rounded-xl" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,17 +339,12 @@ export default function PeopleListPage() {
|
|||||||
}, [loadQuery, organizationId, searchParams]);
|
}, [loadQuery, organizationId, searchParams]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <PeopleListPageFallback />;
|
return <PeopleListPageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<PeopleListPageSkeleton />}>
|
||||||
<Helmet>
|
<PeopleListContent queryRef={queryRef} />
|
||||||
<title>People - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<PeopleListPageFallback />}>
|
|
||||||
<PeopleListContent queryRef={queryRef} />
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ import {
|
|||||||
import { Suspense, useEffect, useState, useCallback } from "react";
|
import { Suspense, useEffect, useState, useCallback } from "react";
|
||||||
import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql";
|
import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader } from "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const peopleOverviewPageQuery = graphql`
|
const peopleOverviewPageQuery = graphql`
|
||||||
query PeopleOverviewPageQuery($peopleId: ID!) {
|
query PeopleOverviewPageQuery($peopleId: ID!) {
|
||||||
@@ -161,174 +160,160 @@ function PeopleOverviewPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate title={formData.fullName}>
|
||||||
<Helmet>
|
<div className="space-y-6">
|
||||||
<title>Person - Probo</title>
|
<div className="mx-auto max-w-4xl space-y-6">
|
||||||
</Helmet>
|
<EditableField
|
||||||
<div className="container">
|
label="Full Name"
|
||||||
<PageHeader className="mb-17" title={formData.fullName} />
|
value={formData.fullName}
|
||||||
<div className="space-y-6">
|
onChange={(value) => handleFieldChange("fullName", value)}
|
||||||
<div className="mx-auto max-w-4xl space-y-6">
|
/>
|
||||||
<EditableField
|
|
||||||
label="Full Name"
|
|
||||||
value={formData.fullName}
|
|
||||||
onChange={(value) => handleFieldChange("fullName", value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Primary Email"
|
label="Primary Email"
|
||||||
value={formData.primaryEmailAddress}
|
value={formData.primaryEmailAddress}
|
||||||
type="email"
|
type="email"
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleFieldChange("primaryEmailAddress", value)
|
handleFieldChange("primaryEmailAddress", value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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">Additional Email Addresses</Label>
|
<Label className="text-sm">Additional Email Addresses</Label>
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formData.additionalEmailAddresses.map((email, index) => (
|
|
||||||
<div key={index} className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newEmails = [
|
|
||||||
...formData.additionalEmailAddresses,
|
|
||||||
];
|
|
||||||
newEmails[index] = e.target.value;
|
|
||||||
handleFieldChange(
|
|
||||||
"additionalEmailAddresses",
|
|
||||||
newEmails
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const newEmails =
|
|
||||||
formData.additionalEmailAddresses.filter(
|
|
||||||
(_, i) => i !== index
|
|
||||||
);
|
|
||||||
handleFieldChange(
|
|
||||||
"additionalEmailAddresses",
|
|
||||||
newEmails
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
handleFieldChange("additionalEmailAddresses", [
|
|
||||||
...formData.additionalEmailAddresses,
|
|
||||||
"",
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Email
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.additionalEmailAddresses.map((email, index) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newEmails = [...formData.additionalEmailAddresses];
|
||||||
|
newEmails[index] = e.target.value;
|
||||||
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const newEmails =
|
||||||
|
formData.additionalEmailAddresses.filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
handleFieldChange("additionalEmailAddresses", [
|
||||||
|
...formData.additionalEmailAddresses,
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-lg font-medium">Additional Information</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Additional details about the person
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
<div className="flex items-center gap-2">
|
||||||
Additional Information
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
</h2>
|
<Label className="text-sm">Kind</Label>
|
||||||
<p className="text-sm text-gray-500">
|
</div>
|
||||||
Additional details about the person
|
<div className="flex gap-2">
|
||||||
</p>
|
<button
|
||||||
</div>
|
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
||||||
|
className={cn(
|
||||||
<div className="space-y-4">
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
<div className="space-y-2">
|
formData.kind === "EMPLOYEE"
|
||||||
<div className="flex items-center gap-2">
|
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
<Label className="text-sm">Kind</Label>
|
)}
|
||||||
</div>
|
>
|
||||||
<div className="flex gap-2">
|
Employee
|
||||||
<button
|
</button>
|
||||||
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
<button
|
||||||
className={cn(
|
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
className={cn(
|
||||||
formData.kind === "EMPLOYEE"
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
|
formData.kind === "CONTRACTOR"
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
|
||||||
)}
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
>
|
)}
|
||||||
Employee
|
>
|
||||||
</button>
|
Contractor
|
||||||
<button
|
</button>
|
||||||
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
<button
|
||||||
className={cn(
|
onClick={() =>
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
handleFieldChange("kind", "SERVICE_ACCOUNT")
|
||||||
formData.kind === "CONTRACTOR"
|
}
|
||||||
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
|
className={cn(
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
)}
|
formData.kind === "SERVICE_ACCOUNT"
|
||||||
>
|
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||||
Contractor
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
</button>
|
)}
|
||||||
<button
|
>
|
||||||
onClick={() =>
|
Service Account
|
||||||
handleFieldChange("kind", "SERVICE_ACCOUNT")
|
</button>
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.kind === "SERVICE_ACCOUNT"
|
|
||||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Service Account
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasChanges && (
|
|
||||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{hasChanges && (
|
||||||
|
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||||
|
<Button variant="outline" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeopleOverviewPageFallback() {
|
export function PeopleOverviewPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<PageTemplateSkeleton>
|
||||||
<div className="space-y-1">
|
<div className="space-y-6">
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
<div className="space-y-1">
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</PageTemplateSkeleton>
|
||||||
{[1, 2].map((i) => (
|
|
||||||
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,17 +328,12 @@ export default function PeopleOverviewPage() {
|
|||||||
}, [loadQuery, peopleId]);
|
}, [loadQuery, peopleId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <PeopleOverviewPageFallback />;
|
return <PeopleOverviewPageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<PeopleOverviewPageSkeleton />}>
|
||||||
<Helmet>
|
<PeopleOverviewPageContent queryRef={queryRef} />
|
||||||
<title>People Overview - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<PeopleOverviewPageFallback />}>
|
|
||||||
<PeopleOverviewPageContent queryRef={queryRef} />
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
import { Link, useParams } from "react-router";
|
import { Link, useParams } from "react-router";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -34,7 +33,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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const PolicyListPageQuery = graphql`
|
const PolicyListPageQuery = graphql`
|
||||||
query PolicyListPageQuery($organizationId: ID!) {
|
query PolicyListPageQuery($organizationId: ID!) {
|
||||||
@@ -220,153 +219,141 @@ function PolicyListPageContent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Policies"
|
||||||
<title>Policies - Probo</title>
|
description="Manage your organization's policies"
|
||||||
</Helmet>
|
actions={
|
||||||
<div className="container space-y-6">
|
<Button asChild>
|
||||||
<PageHeader
|
<Link to={`/organizations/${organizationId}/policies/create`}>
|
||||||
className="mb-17"
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
title="Policies"
|
Create Policy
|
||||||
description="Manage your organization's policies"
|
</Link>
|
||||||
actions={
|
</Button>
|
||||||
<Button asChild>
|
}
|
||||||
<Link to={`/organizations/${organizationId}/policies/create`}>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
{/* Search and filter controls */}
|
||||||
Create Policy
|
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||||
</Link>
|
<div className="relative flex-1">
|
||||||
</Button>
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
}
|
<Input
|
||||||
/>
|
placeholder="Search policies..."
|
||||||
|
className="pl-10"
|
||||||
{/* Search and filter controls */}
|
value={searchQuery}
|
||||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<div className="relative flex-1">
|
/>
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search policies..."
|
|
||||||
className="pl-10"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
<SelectValue placeholder="Filter by status" />
|
|
||||||
</div>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="ALL">All Statuses</SelectItem>
|
|
||||||
<SelectItem value="ACTIVE">Active</SelectItem>
|
|
||||||
<SelectItem value="DRAFT">Draft</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" className="flex items-center gap-2">
|
|
||||||
<ArrowUpDown className="h-4 w-4" />
|
|
||||||
Sort
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("name-asc")}>
|
|
||||||
Name (A-Z)
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("name-desc")}>
|
|
||||||
Name (Z-A)
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("updated-desc")}>
|
|
||||||
Recently Updated
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("updated-asc")}>
|
|
||||||
Oldest Updated
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("created-desc")}>
|
|
||||||
Recently Created
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("created-asc")}>
|
|
||||||
Oldest Created
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results summary */}
|
<div className="flex gap-2">
|
||||||
<div className="mb-4 text-sm text-muted-foreground">
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
Showing {filteredPolicies.length} of {policies.length} policies
|
<SelectTrigger className="w-[180px]">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
<SelectValue placeholder="Filter by status" />
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ALL">All Statuses</SelectItem>
|
||||||
|
<SelectItem value="ACTIVE">Active</SelectItem>
|
||||||
|
<SelectItem value="DRAFT">Draft</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
{/* Policy grid */}
|
<DropdownMenu>
|
||||||
<div className="space-y-6">
|
<DropdownMenuTrigger asChild>
|
||||||
{filteredPolicies.length > 0 ? (
|
<Button variant="outline" className="flex items-center gap-2">
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<ArrowUpDown className="h-4 w-4" />
|
||||||
{filteredPolicies.map((policy) => (
|
Sort
|
||||||
<Link
|
</Button>
|
||||||
key={policy.id}
|
</DropdownMenuTrigger>
|
||||||
to={`/organizations/${organizationId}/policies/${policy.id}`}
|
<DropdownMenuContent align="end">
|
||||||
className="group"
|
<DropdownMenuItem onClick={() => setSortBy("name-asc")}>
|
||||||
>
|
Name (A-Z)
|
||||||
<PolicyCard
|
</DropdownMenuItem>
|
||||||
title={policy.name}
|
<DropdownMenuItem onClick={() => setSortBy("name-desc")}>
|
||||||
content={policy.content}
|
Name (Z-A)
|
||||||
status={policy.status}
|
</DropdownMenuItem>
|
||||||
updatedAt={policy.updatedAt}
|
<DropdownMenuItem onClick={() => setSortBy("updated-desc")}>
|
||||||
/>
|
Recently Updated
|
||||||
</Link>
|
</DropdownMenuItem>
|
||||||
))}
|
<DropdownMenuItem onClick={() => setSortBy("updated-asc")}>
|
||||||
</div>
|
Oldest Updated
|
||||||
) : (
|
</DropdownMenuItem>
|
||||||
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
<DropdownMenuItem onClick={() => setSortBy("created-desc")}>
|
||||||
<FileText className="mx-auto h-8 w-8 text-muted-foreground mb-3" />
|
Recently Created
|
||||||
<h3 className="text-lg font-medium">No policies found</h3>
|
</DropdownMenuItem>
|
||||||
<p className="text-muted-foreground mb-4">
|
<DropdownMenuItem onClick={() => setSortBy("created-asc")}>
|
||||||
{searchQuery || statusFilter !== "ALL"
|
Oldest Created
|
||||||
? "Try adjusting your search or filters"
|
</DropdownMenuItem>
|
||||||
: "Create your first policy to get started"}
|
</DropdownMenuContent>
|
||||||
</p>
|
</DropdownMenu>
|
||||||
{searchQuery || statusFilter !== "ALL" ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setSearchQuery("");
|
|
||||||
setStatusFilter("ALL");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear filters
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button asChild>
|
|
||||||
<Link to={`/organizations/${organizationId}/policies/create`}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Create Policy
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{/* Results summary */}
|
||||||
|
<div className="mb-4 text-sm text-muted-foreground">
|
||||||
|
Showing {filteredPolicies.length} of {policies.length} policies
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Policy grid */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{filteredPolicies.length > 0 ? (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{filteredPolicies.map((policy) => (
|
||||||
|
<Link
|
||||||
|
key={policy.id}
|
||||||
|
to={`/organizations/${organizationId}/policies/${policy.id}`}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<PolicyCard
|
||||||
|
title={policy.name}
|
||||||
|
content={policy.content}
|
||||||
|
status={policy.status}
|
||||||
|
updatedAt={policy.updatedAt}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
||||||
|
<FileText className="mx-auto h-8 w-8 text-muted-foreground mb-3" />
|
||||||
|
<h3 className="text-lg font-medium">No policies found</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
{searchQuery || statusFilter !== "ALL"
|
||||||
|
? "Try adjusting your search or filters"
|
||||||
|
: "Create your first policy to get started"}
|
||||||
|
</p>
|
||||||
|
{searchQuery || statusFilter !== "ALL" ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setStatusFilter("ALL");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button asChild>
|
||||||
|
<Link to={`/organizations/${organizationId}/policies/create`}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Policy
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PolicyListPageFallback() {
|
export function PolicyListPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-6">
|
<PageTemplateSkeleton
|
||||||
<div className="flex justify-between items-center mb-6">
|
title="Policies"
|
||||||
<div>
|
description="Manage your organization's policies"
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
actions={<div className="bg-muted animate-pulse h-9 w-1/6 rounded-lg" />}
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-1" />
|
>
|
||||||
</div>
|
|
||||||
<div className="h-10 w-36 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search and filter controls skeleton */}
|
{/* Search and filter controls skeleton */}
|
||||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||||
<div className="flex-1 h-10 bg-muted animate-pulse rounded" />
|
<div className="flex-1 h-10 bg-muted animate-pulse rounded" />
|
||||||
@@ -407,7 +394,7 @@ function PolicyListPageFallback() {
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,14 +408,13 @@ export default function PolicyListPage() {
|
|||||||
loadQuery({ organizationId: organizationId! });
|
loadQuery({ organizationId: organizationId! });
|
||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <PolicyListPageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<PolicyListPageSkeleton />}>
|
||||||
<Helmet>
|
{<PolicyListPageContent queryRef={queryRef} />}
|
||||||
<title>Policies - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<PolicyListPageFallback />}>
|
|
||||||
{queryRef && <PolicyListPageContent queryRef={queryRef} />}
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import type { PolicyOverviewPageQuery as PolicyOverviewPageQueryType } from "./__generated__/PolicyOverviewPageQuery.graphql";
|
import type { PolicyOverviewPageQuery as PolicyOverviewPageQueryType } from "./__generated__/PolicyOverviewPageQuery.graphql";
|
||||||
import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOverviewPageDeleteMutation.graphql";
|
import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOverviewPageDeleteMutation.graphql";
|
||||||
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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const PolicyOverviewPageQuery = graphql`
|
const PolicyOverviewPageQuery = graphql`
|
||||||
query PolicyOverviewPageQuery($policyId: ID!) {
|
query PolicyOverviewPageQuery($policyId: ID!) {
|
||||||
@@ -149,276 +148,272 @@ function PolicyOverviewPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title={policy.name ?? ""}
|
||||||
<title>{policy.name} - Probo Console</title>
|
description={
|
||||||
</Helmet>
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
<div className="container">
|
<Badge variant={policy.status === "ACTIVE" ? "default" : "outline"}>
|
||||||
<PageHeader
|
{policy.status === "ACTIVE" ? "Active" : "Draft"}
|
||||||
className="mb-17"
|
</Badge>
|
||||||
title={policy.name ?? ""}
|
{policy.owner && (
|
||||||
description={
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
<User className="h-3 w-3" />
|
||||||
<Badge
|
<Link
|
||||||
variant={policy.status === "ACTIVE" ? "default" : "outline"}
|
to={`/organizations/${organizationId}/people/${policy.owner.id}`}
|
||||||
|
className="hover:underline"
|
||||||
>
|
>
|
||||||
{policy.status === "ACTIVE" ? "Active" : "Draft"}
|
{policy.owner.fullName}
|
||||||
</Badge>
|
</Link>
|
||||||
{policy.owner && (
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
)}
|
||||||
<User className="h-3 w-3" />
|
{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>
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
// Logic to download policy content as PDF or text
|
||||||
|
const element = document.createElement("a");
|
||||||
|
const file = new Blob([policy.content || ""], {
|
||||||
|
type: "text/plain",
|
||||||
|
});
|
||||||
|
element.href = URL.createObjectURL(file);
|
||||||
|
element.download = `${policy.name}.txt`;
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
document.body.removeChild(element);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<Card className="border shadow-sm">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="p-2 rounded-md bg-slate-100">
|
||||||
|
<Shield className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{policy.status && (
|
||||||
|
<Badge
|
||||||
|
className={`px-3 py-1 rounded-md font-medium ${
|
||||||
|
policy.status === "ACTIVE"
|
||||||
|
? "bg-green-100 text-green-700 hover:bg-green-200"
|
||||||
|
: policy.status === "DRAFT"
|
||||||
|
? "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
|
||||||
|
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{policy.status === "ACTIVE"
|
||||||
|
? "Active"
|
||||||
|
: policy.status === "DRAFT"
|
||||||
|
? "Draft"
|
||||||
|
: policy.status}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-3xl font-bold mb-3">{policy.name}</h1>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mb-6">
|
||||||
|
{getDescription(policy.content)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
defaultValue="content"
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<TabsList className="border-b w-full rounded-none bg-transparent p-0 h-auto">
|
||||||
|
<TabsTrigger
|
||||||
|
value="content"
|
||||||
|
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
|
||||||
|
activeTab === "content"
|
||||||
|
? "border-primary text-primary"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Policy Content
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="history"
|
||||||
|
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
|
||||||
|
activeTab === "history"
|
||||||
|
? "border-primary text-primary"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Version History
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="approvals"
|
||||||
|
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
|
||||||
|
activeTab === "approvals"
|
||||||
|
? "border-primary text-primary"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Approvals
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="content" className="pt-6">
|
||||||
|
<div className="prose prose-sm md:prose-base lg:prose-lg max-w-none">
|
||||||
|
<div
|
||||||
|
className="policy-content"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: policy.content || "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="history" className="pt-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Version history will be available soon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="approvals" className="pt-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Approval workflow will be available soon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<Card className="border shadow-sm mb-6">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-6">Policy Details</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Last Updated</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-medium">{formatDate(policy.updatedAt)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Review Due</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-medium">
|
||||||
|
{policy.reviewDate
|
||||||
|
? formatDate(policy.reviewDate)
|
||||||
|
: "Not set"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Owner</span>
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
to={`/organizations/${organizationId}/people/${policy.owner.id}`}
|
to={`/organizations/${organizationId}/people/${policy.owner?.id}`}
|
||||||
className="hover:underline"
|
className="hover:underline"
|
||||||
>
|
>
|
||||||
{policy.owner.fullName}
|
<p className="font-medium">
|
||||||
|
{policy.owner ? policy.owner.fullName : "Not assigned"}
|
||||||
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{policy.reviewDate && (
|
</CardContent>
|
||||||
<div className="flex items-center gap-1">
|
</Card>
|
||||||
<Calendar className="h-3 w-3" />
|
|
||||||
<span>Review: {formatDate(policy.reviewDate)}</span>
|
<Button asChild className="w-full">
|
||||||
</div>
|
<Link
|
||||||
)}
|
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
|
||||||
</div>
|
>
|
||||||
}
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
actions={
|
Edit Policy
|
||||||
<div className="flex gap-2">
|
</Link>
|
||||||
<Button variant="outline" size="sm" asChild>
|
</Button>
|
||||||
<Link
|
|
||||||
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
|
<Card className="border shadow-sm mt-6 bg-red-50">
|
||||||
>
|
<CardContent className="p-6">
|
||||||
<Edit className="h-4 w-4 mr-2" />
|
<h3 className="text-red-500 font-semibold mb-3">Danger Zone</h3>
|
||||||
Edit
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
</Link>
|
Permanently delete this policy and all of its data. This action
|
||||||
</Button>
|
cannot be undone.
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="destructive"
|
||||||
size="sm"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={handleDeletePolicy}
|
||||||
// Logic to download policy content as PDF or text
|
disabled={isDeleting}
|
||||||
const element = document.createElement("a");
|
|
||||||
const file = new Blob([policy.content || ""], {
|
|
||||||
type: "text/plain",
|
|
||||||
});
|
|
||||||
element.href = URL.createObjectURL(file);
|
|
||||||
element.download = `${policy.name}.txt`;
|
|
||||||
document.body.appendChild(element);
|
|
||||||
element.click();
|
|
||||||
document.body.removeChild(element);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4 mr-2" />
|
<svg
|
||||||
Download
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
<path d="M3 6h18"></path>
|
||||||
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
||||||
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
||||||
|
</svg>
|
||||||
|
{isDeleting ? "Deleting..." : "Delete Policy"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</CardContent>
|
||||||
}
|
</Card>
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<Card className="border shadow-sm">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<div className="p-2 rounded-md bg-slate-100">
|
|
||||||
<Shield className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{policy.status && (
|
|
||||||
<Badge
|
|
||||||
className={`px-3 py-1 rounded-md font-medium ${
|
|
||||||
policy.status === "ACTIVE"
|
|
||||||
? "bg-green-100 text-green-700 hover:bg-green-200"
|
|
||||||
: policy.status === "DRAFT"
|
|
||||||
? "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
|
|
||||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{policy.status === "ACTIVE"
|
|
||||||
? "Active"
|
|
||||||
: policy.status === "DRAFT"
|
|
||||||
? "Draft"
|
|
||||||
: policy.status}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-3xl font-bold mb-3">{policy.name}</h1>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mb-6">
|
|
||||||
{getDescription(policy.content)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Tabs
|
|
||||||
defaultValue="content"
|
|
||||||
value={activeTab}
|
|
||||||
onValueChange={setActiveTab}
|
|
||||||
className="mb-6"
|
|
||||||
>
|
|
||||||
<TabsList className="border-b w-full rounded-none bg-transparent p-0 h-auto">
|
|
||||||
<TabsTrigger
|
|
||||||
value="content"
|
|
||||||
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
|
|
||||||
activeTab === "content"
|
|
||||||
? "border-primary text-primary"
|
|
||||||
: "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Policy Content
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="history"
|
|
||||||
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
|
|
||||||
activeTab === "history"
|
|
||||||
? "border-primary text-primary"
|
|
||||||
: "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Version History
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="approvals"
|
|
||||||
className={`rounded-none border-b-2 border-transparent px-4 py-2 font-medium ${
|
|
||||||
activeTab === "approvals"
|
|
||||||
? "border-primary text-primary"
|
|
||||||
: "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Approvals
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="content" className="pt-6">
|
|
||||||
<div className="prose prose-sm md:prose-base lg:prose-lg max-w-none">
|
|
||||||
<div
|
|
||||||
className="policy-content"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: policy.content || "",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="history" className="pt-6">
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Version history will be available soon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="approvals" className="pt-6">
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Approval workflow will be available soon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<Card className="border shadow-sm mb-6">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<h2 className="text-xl font-semibold mb-6">Policy Details</h2>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
|
||||||
<FileText className="h-4 w-4" />
|
|
||||||
<span className="text-sm">Last Updated</span>
|
|
||||||
</div>
|
|
||||||
<p className="font-medium">
|
|
||||||
{formatDate(policy.updatedAt)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
|
||||||
<FileText className="h-4 w-4" />
|
|
||||||
<span className="text-sm">Review Due</span>
|
|
||||||
</div>
|
|
||||||
<p className="font-medium">
|
|
||||||
{policy.reviewDate
|
|
||||||
? formatDate(policy.reviewDate)
|
|
||||||
: "Not set"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
<span className="text-sm">Owner</span>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
to={`/organizations/${organizationId}/people/${policy.owner?.id}`}
|
|
||||||
className="hover:underline"
|
|
||||||
>
|
|
||||||
<p className="font-medium">
|
|
||||||
{policy.owner ? policy.owner.fullName : "Not assigned"}
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Button asChild className="w-full">
|
|
||||||
<Link
|
|
||||||
to={`/organizations/${organizationId}/policies/${policy.id}/update`}
|
|
||||||
>
|
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
|
||||||
Edit Policy
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Card className="border shadow-sm mt-6 bg-red-50">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<h3 className="text-red-500 font-semibold mb-3">Danger Zone</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Permanently delete this policy and all of its data. This
|
|
||||||
action cannot be undone.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
className="w-full"
|
|
||||||
onClick={handleDeletePolicy}
|
|
||||||
disabled={isDeleting}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="mr-2"
|
|
||||||
>
|
|
||||||
<path d="M3 6h18"></path>
|
|
||||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
|
||||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
|
||||||
</svg>
|
|
||||||
{isDeleting ? "Deleting..." : "Delete Policy"}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PolicyOverviewPageFallback() {
|
export function PolicyOverviewPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-6">
|
<PageTemplateSkeleton
|
||||||
|
withDescription
|
||||||
|
actions={
|
||||||
|
<div className="flex gap-2 w-1/3">
|
||||||
|
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
|
||||||
|
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<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">
|
||||||
<Card className="border shadow-sm">
|
<Card className="border shadow-sm">
|
||||||
@@ -505,7 +500,7 @@ function PolicyOverviewPageFallback() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,8 +515,12 @@ export default function PolicyOverviewPage() {
|
|||||||
loadQuery({ policyId: policyId! });
|
loadQuery({ policyId: policyId! });
|
||||||
}, [loadQuery, policyId]);
|
}, [loadQuery, policyId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <PolicyOverviewPageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<PolicyOverviewPageFallback />}>
|
<Suspense fallback={<PolicyOverviewPageSkeleton />}>
|
||||||
{queryRef && <PolicyOverviewPageContent queryRef={queryRef} />}
|
{queryRef && <PolicyOverviewPageContent queryRef={queryRef} />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Suspense, useEffect, useState, useRef } from "react";
|
import { Suspense, useEffect, useState, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
graphql,
|
graphql,
|
||||||
@@ -39,7 +38,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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const settingsPageQuery = graphql`
|
const settingsPageQuery = graphql`
|
||||||
query SettingsPageQuery($organizationID: ID!) {
|
query SettingsPageQuery($organizationID: ID!) {
|
||||||
@@ -290,14 +289,11 @@ function SettingsPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<div className="container space-y-6">
|
title="Settings"
|
||||||
<PageHeader
|
description="Manage your details and personal preferences here"
|
||||||
className="mb-17"
|
>
|
||||||
title="Settings"
|
<div className="space-y-6">
|
||||||
description="Manage your details and personal preferences here"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Organization information</CardTitle>
|
<CardTitle>Organization information</CardTitle>
|
||||||
@@ -519,23 +515,28 @@ function SettingsPageContent({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsPageFallback() {
|
export function SettingsPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<PageTemplateSkeleton
|
||||||
<div className="space-y-1">
|
title="Settings"
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
description="Manage your details and personal preferences here"
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<div key={i} className="h-64 bg-muted animate-pulse rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</PageTemplateSkeleton>
|
||||||
{[1, 2].map((i) => (
|
|
||||||
<div key={i} className="h-64 bg-muted animate-pulse rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,17 +551,12 @@ export default function SettingsPage() {
|
|||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <SettingsPageFallback />;
|
return <SettingsPageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<SettingsPageSkeleton />}>
|
||||||
<Helmet>
|
<SettingsPageContent queryRef={queryRef} />
|
||||||
<title>Settings - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<SettingsPageFallback />}>
|
|
||||||
<SettingsPageContent queryRef={queryRef} />
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
PreloadedQuery,
|
PreloadedQuery,
|
||||||
useQueryLoader,
|
useQueryLoader,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -28,7 +27,7 @@ import type {
|
|||||||
ControlState,
|
ControlState,
|
||||||
ControlImportance,
|
ControlImportance,
|
||||||
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
|
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
|
||||||
import { PageHeader } from "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const updateControlMutation = graphql`
|
const updateControlMutation = graphql`
|
||||||
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
|
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
|
||||||
@@ -250,100 +249,97 @@ function UpdateControlPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Update Control"
|
||||||
<title>Update Control - Probo</title>
|
description="Update the control details"
|
||||||
</Helmet>
|
>
|
||||||
<div className="container">
|
<Card className="max-w-2xl">
|
||||||
<PageHeader
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
className="mb-17"
|
<EditableField
|
||||||
title="Update Control"
|
label="Name"
|
||||||
description="Update the control details"
|
value={formData.name}
|
||||||
/>
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="max-w-2xl">
|
<EditableField
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
label="Description"
|
||||||
<EditableField
|
value={formData.description}
|
||||||
label="Name"
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
value={formData.name}
|
required
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
multiline
|
||||||
required
|
helpText="Provide a detailed description of the control"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Description"
|
label="Category"
|
||||||
value={formData.description}
|
value={formData.category}
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
onChange={(value) => handleFieldChange("category", value)}
|
||||||
required
|
required
|
||||||
multiline
|
/>
|
||||||
helpText="Provide a detailed description of the control"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
<div className="space-y-2">
|
||||||
label="Category"
|
<Label htmlFor="importance" className="text-sm font-medium">
|
||||||
value={formData.category}
|
Importance
|
||||||
onChange={(value) => handleFieldChange("category", value)}
|
</Label>
|
||||||
required
|
<Select
|
||||||
/>
|
value={formData.importance}
|
||||||
|
onValueChange={(value) => handleFieldChange("importance", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select importance" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
||||||
|
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
||||||
|
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="importance" className="text-sm font-medium">
|
<Label htmlFor="state" className="text-sm font-medium">
|
||||||
Importance
|
State
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.importance}
|
value={formData.state}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => handleFieldChange("state", value)}
|
||||||
handleFieldChange("importance", value)
|
>
|
||||||
}
|
<SelectTrigger>
|
||||||
>
|
<SelectValue placeholder="Select state" />
|
||||||
<SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectValue placeholder="Select importance" />
|
<SelectContent>
|
||||||
</SelectTrigger>
|
<SelectItem value="NOT_STARTED">Not Started</SelectItem>
|
||||||
<SelectContent>
|
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
|
||||||
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
<SelectItem value="NOT_APPLICABLE">Not Applicable</SelectItem>
|
||||||
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
<SelectItem value="IMPLEMENTED">Implemented</SelectItem>
|
||||||
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex justify-end gap-3">
|
||||||
<Label htmlFor="state" className="text-sm font-medium">
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
State
|
Cancel
|
||||||
</Label>
|
</Button>
|
||||||
<Select
|
<Button type="submit" disabled={isInFlight || !hasChanges}>
|
||||||
value={formData.state}
|
{isInFlight ? "Updating..." : "Update Control"}
|
||||||
onValueChange={(value) => handleFieldChange("state", value)}
|
</Button>
|
||||||
>
|
</div>
|
||||||
<SelectTrigger>
|
</form>
|
||||||
<SelectValue placeholder="Select state" />
|
</Card>
|
||||||
</SelectTrigger>
|
</PageTemplate>
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="NOT_STARTED">Not Started</SelectItem>
|
|
||||||
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
|
|
||||||
<SelectItem value="NOT_APPLICABLE">Not Applicable</SelectItem>
|
|
||||||
<SelectItem value="IMPLEMENTED">Implemented</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight || !hasChanges}>
|
|
||||||
{isInFlight ? "Updating..." : "Update Control"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateControlPageFallback() {
|
export function UpdateControlPageSkeleton() {
|
||||||
return <div>Loading...</div>;
|
return (
|
||||||
|
<PageTemplateSkeleton
|
||||||
|
title="Update Control"
|
||||||
|
description="Update the control details"
|
||||||
|
>
|
||||||
|
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UpdateControlPage() {
|
export default function UpdateControlPage() {
|
||||||
@@ -358,11 +354,11 @@ export default function UpdateControlPage() {
|
|||||||
}, [controlId, loadQuery]);
|
}, [controlId, loadQuery]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <UpdateControlPageFallback />;
|
return <UpdateControlPageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<UpdateControlPageFallback />}>
|
<Suspense fallback={<UpdateControlPageSkeleton />}>
|
||||||
<UpdateControlPageContent queryRef={queryRef} />
|
<UpdateControlPageContent queryRef={queryRef} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
PreloadedQuery,
|
PreloadedQuery,
|
||||||
useQueryLoader,
|
useQueryLoader,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -18,7 +17,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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const updateFrameworkMutation = graphql`
|
const updateFrameworkMutation = graphql`
|
||||||
mutation UpdateFrameworkPageUpdateFrameworkMutation(
|
mutation UpdateFrameworkPageUpdateFrameworkMutation(
|
||||||
@@ -199,52 +198,51 @@ function UpdateFrameworkPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Update Framework"
|
||||||
<title>Update Framework - Probo</title>
|
description="Update the framework details"
|
||||||
</Helmet>
|
>
|
||||||
<div className="container">
|
<Card className="max-w-2xl">
|
||||||
<PageHeader
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
className="mb-17"
|
<EditableField
|
||||||
title="Update Framework"
|
label="Name"
|
||||||
description="Update the framework details"
|
value={formData.name}
|
||||||
/>
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="max-w-2xl">
|
<EditableField
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
label="Description"
|
||||||
<EditableField
|
value={formData.description}
|
||||||
label="Name"
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
value={formData.name}
|
required
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
multiline
|
||||||
required
|
helpText="Provide a detailed description of the framework"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<div className="flex justify-end gap-3">
|
||||||
label="Description"
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
value={formData.description}
|
Cancel
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
</Button>
|
||||||
required
|
<Button type="submit" disabled={isInFlight}>
|
||||||
multiline
|
{isInFlight ? "Updating..." : "Update Framework"}
|
||||||
helpText="Provide a detailed description of the framework"
|
</Button>
|
||||||
/>
|
</div>
|
||||||
|
</form>
|
||||||
<div className="flex justify-end gap-3">
|
</Card>
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
</PageTemplate>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight}>
|
|
||||||
{isInFlight ? "Updating..." : "Update Framework"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateFrameworkPageFallback() {
|
export function UpdateFrameworkPageSkeleton() {
|
||||||
return <div>Loading...</div>;
|
return (
|
||||||
|
<PageTemplateSkeleton
|
||||||
|
title="Update Framework"
|
||||||
|
description="Update the framework details"
|
||||||
|
>
|
||||||
|
<div className="max-w-2xl aspect-square bg-muted rounded-xl animate-pulse" />
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UpdateFrameworkPage() {
|
export default function UpdateFrameworkPage() {
|
||||||
@@ -259,11 +257,11 @@ export default function UpdateFrameworkPage() {
|
|||||||
}, [frameworkId, loadQuery]);
|
}, [frameworkId, loadQuery]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <UpdateFrameworkPageFallback />;
|
return <UpdateFrameworkPageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<UpdateFrameworkPageFallback />}>
|
<Suspense fallback={<UpdateFrameworkPageSkeleton />}>
|
||||||
<UpdateFrameworkPageContent queryRef={queryRef} />
|
<UpdateFrameworkPageContent queryRef={queryRef} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
useQueryLoader,
|
useQueryLoader,
|
||||||
PreloadedQuery,
|
PreloadedQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -20,7 +19,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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const UpdatePolicyPageQuery = graphql`
|
const UpdatePolicyPageQuery = graphql`
|
||||||
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
|
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
|
||||||
@@ -148,127 +147,116 @@ function UpdatePolicyPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate title="Update Policy" description="Update an existing policy">
|
||||||
<Helmet>
|
<form onSubmit={handleSubmit}>
|
||||||
<title>Update Policy - Probo Console</title>
|
<div className="grid gap-6">
|
||||||
</Helmet>
|
<Card>
|
||||||
<div className="container">
|
<CardHeader>
|
||||||
<PageHeader
|
<CardTitle>Policy Information</CardTitle>
|
||||||
className="mb-17"
|
</CardHeader>
|
||||||
title="Update Policy"
|
<CardContent className="space-y-4">
|
||||||
description="Update an existing policy"
|
<div className="space-y-2">
|
||||||
/>
|
<Label htmlFor="name">Policy Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder="Enter policy name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<div className="space-y-2">
|
||||||
<div className="grid gap-6">
|
<Label htmlFor="content">Policy Content</Label>
|
||||||
<Card>
|
<div className="min-h-[300px]">
|
||||||
<CardHeader>
|
<PolicyEditor
|
||||||
<CardTitle>Policy Information</CardTitle>
|
initialContent={content}
|
||||||
</CardHeader>
|
onChange={(html) => setContent(html)}
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Policy Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
placeholder="Enter policy name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="content">Policy Content</Label>
|
<Label>Status</Label>
|
||||||
<div className="min-h-[300px]">
|
<RadioGroup
|
||||||
<PolicyEditor
|
value={status}
|
||||||
initialContent={content}
|
onValueChange={(value) =>
|
||||||
onChange={(html) => setContent(html)}
|
setStatus(value as "DRAFT" | "ACTIVE")
|
||||||
/>
|
}
|
||||||
|
className="flex space-x-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="DRAFT" id="draft" />
|
||||||
|
<Label htmlFor="draft" className="cursor-pointer">
|
||||||
|
Draft
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="ACTIVE" id="active" />
|
||||||
|
<Label htmlFor="active" className="cursor-pointer">
|
||||||
|
Active
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Status</Label>
|
<Label htmlFor="owner" className="flex items-center gap-2">
|
||||||
<RadioGroup
|
<User className="h-4 w-4" />
|
||||||
value={status}
|
Policy Owner
|
||||||
onValueChange={(value) =>
|
</Label>
|
||||||
setStatus(value as "DRAFT" | "ACTIVE")
|
<PeopleSelector
|
||||||
}
|
organizationRef={data.organization}
|
||||||
className="flex space-x-4"
|
selectedPersonId={ownerId}
|
||||||
>
|
onSelect={setOwnerId}
|
||||||
<div className="flex items-center space-x-2">
|
placeholder="Select policy owner"
|
||||||
<RadioGroupItem value="DRAFT" id="draft" />
|
required
|
||||||
<Label htmlFor="draft" className="cursor-pointer">
|
/>
|
||||||
Draft
|
</div>
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="ACTIVE" id="active" />
|
|
||||||
<Label htmlFor="active" className="cursor-pointer">
|
|
||||||
Active
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="owner" className="flex items-center gap-2">
|
<Label htmlFor="reviewDate" className="flex items-center gap-2">
|
||||||
<User className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
Policy Owner
|
Review Date
|
||||||
</Label>
|
</Label>
|
||||||
<PeopleSelector
|
<Input
|
||||||
organizationRef={data.organization}
|
id="reviewDate"
|
||||||
selectedPersonId={ownerId}
|
type="date"
|
||||||
onSelect={setOwnerId}
|
value={reviewDate}
|
||||||
placeholder="Select policy owner"
|
onChange={(e) => setReviewDate(e.target.value)}
|
||||||
required
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex justify-end gap-4">
|
||||||
<Label
|
<Button
|
||||||
htmlFor="reviewDate"
|
type="button"
|
||||||
className="flex items-center gap-2"
|
variant="outline"
|
||||||
>
|
onClick={() =>
|
||||||
<Calendar className="h-4 w-4" />
|
navigate(
|
||||||
Review Date
|
`/organizations/${organizationId}/policies/${policyId}`
|
||||||
</Label>
|
)
|
||||||
<Input
|
}
|
||||||
id="reviewDate"
|
>
|
||||||
type="date"
|
Cancel
|
||||||
value={reviewDate}
|
</Button>
|
||||||
onChange={(e) => setReviewDate(e.target.value)}
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
/>
|
{isSubmitting ? "Updating..." : "Update Policy"}
|
||||||
</div>
|
</Button>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/policies/${policyId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating..." : "Update Policy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdatePolicyPageFallback() {
|
export function UpdatePolicyPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-6">
|
<PageTemplateSkeleton
|
||||||
|
title="Update Policy"
|
||||||
|
description="Update an existing policy"
|
||||||
|
>
|
||||||
<div className="flex items-center mb-6">
|
<div className="flex items-center mb-6">
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
<div className="h-12 w-12 bg-muted animate-pulse rounded-lg" />
|
<div className="h-12 w-12 bg-muted animate-pulse rounded-lg" />
|
||||||
@@ -279,7 +267,7 @@ function UpdatePolicyPageFallback() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted animate-pulse rounded-lg h-[600px]" />
|
<div className="bg-muted animate-pulse rounded-lg h-[600px]" />
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,8 +283,12 @@ export default function UpdatePolicyPage() {
|
|||||||
}
|
}
|
||||||
}, [organizationId, policyId, loadQuery]);
|
}, [organizationId, policyId, loadQuery]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <UpdatePolicyPageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<UpdatePolicyPageFallback />}>
|
<Suspense fallback={<UpdatePolicyPageSkeleton />}>
|
||||||
{queryRef && <UpdatePolicyPageContent queryRef={queryRef} />}
|
{queryRef && <UpdatePolicyPageContent queryRef={queryRef} />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,13 +16,12 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import type { VendorListPageQuery as VendorListPageQueryType } from "./__generated__/VendorListPageQuery.graphql";
|
import type { VendorListPageQuery as VendorListPageQueryType } from "./__generated__/VendorListPageQuery.graphql";
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
import { VendorListPageCreateVendorMutation } from "./__generated__/VendorListPageCreateVendorMutation.graphql";
|
import { VendorListPageCreateVendorMutation } from "./__generated__/VendorListPageCreateVendorMutation.graphql";
|
||||||
import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPageDeleteVendorMutation.graphql";
|
import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPageDeleteVendorMutation.graphql";
|
||||||
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 "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 25;
|
const ITEMS_PER_PAGE = 25;
|
||||||
|
|
||||||
@@ -240,97 +239,52 @@ function VendorListContent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate
|
||||||
<Helmet>
|
title="Vendors"
|
||||||
<title>Vendors - Probo</title>
|
description="Vendors are third-party services that your company uses. Add them to
|
||||||
</Helmet>
|
keep track of their risk and compliance status."
|
||||||
<div className="space-y-6">
|
>
|
||||||
<PageHeader
|
<div className="rounded-xl border bg-card p-4">
|
||||||
className="mb-17"
|
<div className="flex items-center gap-2 mb-4">
|
||||||
title="Vendors"
|
<Store className="h-5 w-5" />
|
||||||
description="Vendors are third-party services that your company uses. Add them to
|
<h3 className="font-medium">Add a vendor</h3>
|
||||||
keep track of their risk and compliance status."
|
</div>
|
||||||
/>
|
<div className="flex gap-2 relative">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type vendor's name"
|
||||||
|
value={searchTerm}
|
||||||
|
style={{ borderRadius: "0.3rem" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearchTerm(value);
|
||||||
|
if (value.trim() === "") {
|
||||||
|
setFilteredVendors([]);
|
||||||
|
} else {
|
||||||
|
const results = fuse.search(value).map((result) => result.item);
|
||||||
|
setFilteredVendors(results);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="rounded-xl border bg-card p-4">
|
{searchTerm.trim() !== "" && (
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<>
|
||||||
<Store className="h-5 w-5" />
|
{filteredVendors.length > 0 ? (
|
||||||
<h3 className="font-medium">Add a vendor</h3>
|
<div
|
||||||
</div>
|
style={{ borderRadius: "0.3rem" }}
|
||||||
<div className="flex gap-2 relative">
|
className="absolute top-full left-0 mt-1 w-[calc(100%-100px)] max-h-48 overflow-y-auto border bg-popover shadow-md z-10"
|
||||||
<Input
|
>
|
||||||
type="text"
|
{filteredVendors.map((vendor: VendorItem) => (
|
||||||
placeholder="Type vendor's name"
|
|
||||||
value={searchTerm}
|
|
||||||
style={{ borderRadius: "0.3rem" }}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setSearchTerm(value);
|
|
||||||
if (value.trim() === "") {
|
|
||||||
setFilteredVendors([]);
|
|
||||||
} else {
|
|
||||||
const results = fuse
|
|
||||||
.search(value)
|
|
||||||
.map((result) => result.item);
|
|
||||||
setFilteredVendors(results);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{searchTerm.trim() !== "" && (
|
|
||||||
<>
|
|
||||||
{filteredVendors.length > 0 ? (
|
|
||||||
<div
|
|
||||||
style={{ borderRadius: "0.3rem" }}
|
|
||||||
className="absolute top-full left-0 mt-1 w-[calc(100%-100px)] max-h-48 overflow-y-auto border bg-popover shadow-md z-10"
|
|
||||||
>
|
|
||||||
{filteredVendors.map((vendor: VendorItem) => (
|
|
||||||
<button
|
|
||||||
key={vendor.id}
|
|
||||||
className="w-full px-3 py-2 text-left hover:bg-accent"
|
|
||||||
onClick={() => {
|
|
||||||
createVendor({
|
|
||||||
variables: {
|
|
||||||
connections: [vendorsConnection.vendors.__id],
|
|
||||||
input: {
|
|
||||||
organizationId: data.organization.id,
|
|
||||||
name: vendor.name,
|
|
||||||
description: "",
|
|
||||||
serviceStartAt: new Date().toISOString(),
|
|
||||||
serviceCriticality: "LOW",
|
|
||||||
riskTier: "GENERAL",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted() {
|
|
||||||
setSearchTerm("");
|
|
||||||
setFilteredVendors([]);
|
|
||||||
toast({
|
|
||||||
title: "Vendor added",
|
|
||||||
description:
|
|
||||||
"The vendor has been added successfully",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{vendor.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{ borderRadius: "0.3rem" }}
|
|
||||||
className="absolute top-full left-0 mt-1 w-[calc(100%-100px)] border bg-popover shadow-md z-10"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 py-2 text-left hover:bg-accent flex items-center gap-2"
|
key={vendor.id}
|
||||||
|
className="w-full px-3 py-2 text-left hover:bg-accent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createVendor({
|
createVendor({
|
||||||
variables: {
|
variables: {
|
||||||
connections: [vendorsConnection.vendors.__id],
|
connections: [vendorsConnection.vendors.__id],
|
||||||
input: {
|
input: {
|
||||||
organizationId: data.organization.id,
|
organizationId: data.organization.id,
|
||||||
name: searchTerm.trim(),
|
name: vendor.name,
|
||||||
description: "",
|
description: "",
|
||||||
serviceStartAt: new Date().toISOString(),
|
serviceStartAt: new Date().toISOString(),
|
||||||
serviceCriticality: "LOW",
|
serviceCriticality: "LOW",
|
||||||
@@ -341,135 +295,174 @@ function VendorListContent({
|
|||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setFilteredVendors([]);
|
setFilteredVendors([]);
|
||||||
toast({
|
toast({
|
||||||
title: "Vendor created",
|
title: "Vendor added",
|
||||||
description:
|
description:
|
||||||
"The new vendor has been created successfully",
|
"The vendor has been added successfully",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="font-medium">Create new vendor:</span>{" "}
|
{vendor.name}
|
||||||
{searchTerm}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
))}
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{vendors.map((vendor) => (
|
|
||||||
<Link
|
|
||||||
key={vendor?.id}
|
|
||||||
to={`/organizations/${organizationId}/vendors/${vendor?.id}`}
|
|
||||||
className="block"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarFallback>{vendor?.name?.[0]}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="font-medium">{vendor?.name}</p>
|
|
||||||
{vendor?.description && (
|
|
||||||
<>
|
|
||||||
<span className="text-muted-foreground">•</span>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{vendor.description}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
) : (
|
||||||
<Badge
|
<div
|
||||||
variant="secondary"
|
style={{ borderRadius: "0.3rem" }}
|
||||||
className={
|
className="absolute top-full left-0 mt-1 w-[calc(100%-100px)] border bg-popover shadow-md z-10"
|
||||||
vendor.riskTier === "CRITICAL"
|
>
|
||||||
? "bg-red-100 text-red-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
<button
|
||||||
: vendor?.riskTier === "SIGNIFICANT"
|
className="w-full px-3 py-2 text-left hover:bg-accent flex items-center gap-2"
|
||||||
? "bg-yellow-100 text-yellow-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
onClick={() => {
|
||||||
: "bg-green-100 text-green-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
createVendor({
|
||||||
}
|
variables: {
|
||||||
>
|
connections: [vendorsConnection.vendors.__id],
|
||||||
{vendor.riskTier}
|
input: {
|
||||||
</Badge>
|
organizationId: data.organization.id,
|
||||||
<Button
|
name: searchTerm.trim(),
|
||||||
variant="ghost"
|
description: "",
|
||||||
size="icon"
|
serviceStartAt: new Date().toISOString(),
|
||||||
className="h-8 w-8 text-muted-foreground hover:bg-transparent hover:[&>svg]:text-destructive"
|
serviceCriticality: "LOW",
|
||||||
onClick={(e) => {
|
riskTier: "GENERAL",
|
||||||
e.preventDefault(); // Prevent navigation
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
"Are you sure you want to delete this vendor?"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
deleteVendor({
|
|
||||||
variables: {
|
|
||||||
connections: [vendorsConnection.vendors.__id],
|
|
||||||
input: {
|
|
||||||
vendorId: vendor.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
onCompleted() {
|
},
|
||||||
toast({
|
onCompleted() {
|
||||||
title: "Vendor deleted",
|
setSearchTerm("");
|
||||||
description:
|
setFilteredVendors([]);
|
||||||
"The vendor has been deleted successfully",
|
toast({
|
||||||
});
|
title: "Vendor created",
|
||||||
},
|
description:
|
||||||
});
|
"The new vendor has been created successfully",
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 transition-colors" />
|
<span className="font-medium">Create new vendor:</span>{" "}
|
||||||
</Button>
|
{searchTerm}
|
||||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{vendors.map((vendor) => (
|
||||||
|
<Link
|
||||||
|
key={vendor?.id}
|
||||||
|
to={`/organizations/${organizationId}/vendors/${vendor?.id}`}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarFallback>{vendor?.name?.[0]}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="font-medium">{vendor?.name}</p>
|
||||||
|
{vendor?.description && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{vendor.description}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
<div className="flex items-center gap-2">
|
||||||
))}
|
<Badge
|
||||||
</div>
|
variant="secondary"
|
||||||
|
className={
|
||||||
<LoadAboveButton
|
vendor.riskTier === "CRITICAL"
|
||||||
isLoading={isLoadingPrevious}
|
? "bg-red-100 text-red-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||||
hasMore={hasPrevious}
|
: vendor?.riskTier === "SIGNIFICANT"
|
||||||
onLoadMore={() => {
|
? "bg-yellow-100 text-yellow-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||||
startTransition(() => {
|
: "bg-green-100 text-green-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||||
setSearchParams((prev) => {
|
}
|
||||||
prev.set("before", pageInfo?.startCursor || "");
|
>
|
||||||
prev.delete("after");
|
{vendor.riskTier}
|
||||||
return prev;
|
</Badge>
|
||||||
});
|
<Button
|
||||||
loadPrevious(ITEMS_PER_PAGE);
|
variant="ghost"
|
||||||
});
|
size="icon"
|
||||||
}}
|
className="h-8 w-8 text-muted-foreground hover:bg-transparent hover:[&>svg]:text-destructive"
|
||||||
/>
|
onClick={(e) => {
|
||||||
<LoadBelowButton
|
e.preventDefault(); // Prevent navigation
|
||||||
isLoading={isLoadingNext}
|
if (
|
||||||
hasMore={hasNext}
|
window.confirm(
|
||||||
onLoadMore={() => {
|
"Are you sure you want to delete this vendor?"
|
||||||
startTransition(() => {
|
)
|
||||||
setSearchParams((prev) => {
|
) {
|
||||||
prev.set("after", pageInfo?.endCursor || "");
|
deleteVendor({
|
||||||
prev.delete("before");
|
variables: {
|
||||||
return prev;
|
connections: [vendorsConnection.vendors.__id],
|
||||||
});
|
input: {
|
||||||
loadNext(ITEMS_PER_PAGE);
|
vendorId: vendor.id,
|
||||||
});
|
},
|
||||||
}}
|
},
|
||||||
/>
|
onCompleted() {
|
||||||
|
toast({
|
||||||
|
title: "Vendor deleted",
|
||||||
|
description:
|
||||||
|
"The vendor has been deleted successfully",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 transition-colors" />
|
||||||
|
</Button>
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
<LoadAboveButton
|
||||||
|
isLoading={isLoadingPrevious}
|
||||||
|
hasMore={hasPrevious}
|
||||||
|
onLoadMore={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.set("before", pageInfo?.startCursor || "");
|
||||||
|
prev.delete("after");
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
loadPrevious(ITEMS_PER_PAGE);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<LoadBelowButton
|
||||||
|
isLoading={isLoadingNext}
|
||||||
|
hasMore={hasNext}
|
||||||
|
onLoadMore={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.set("after", pageInfo?.endCursor || "");
|
||||||
|
prev.delete("before");
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
loadNext(ITEMS_PER_PAGE);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VendorListFallback() {
|
export function VendorListPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<PageTemplateSkeleton
|
||||||
|
title="Vendors"
|
||||||
|
description="Vendors are third-party services that your company uses. Add them to
|
||||||
|
keep track of their risk and compliance status."
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
<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 className="h-4 w-96 bg-muted animate-pulse rounded mt-1" />
|
||||||
@@ -486,7 +479,7 @@ function VendorListFallback() {
|
|||||||
<div key={i} className="h-[72px] bg-muted animate-pulse rounded-xl" />
|
<div key={i} className="h-[72px] bg-muted animate-pulse rounded-xl" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,17 +504,12 @@ export default function VendorListPage() {
|
|||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <VendorListFallback />;
|
return <VendorListPageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<VendorListPageSkeleton />}>
|
||||||
<Helmet>
|
<VendorListContent queryRef={queryRef} />
|
||||||
<title>Vendors - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<VendorListFallback />}>
|
|
||||||
<VendorListContent queryRef={queryRef} />
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ import {
|
|||||||
import { Suspense, useEffect, useState, useCallback } from "react";
|
import { Suspense, useEffect, useState, useCallback } from "react";
|
||||||
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
|
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 { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader } from "@/components/PageHeader";
|
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
|
||||||
const vendorOverviewPageQuery = graphql`
|
const vendorOverviewPageQuery = graphql`
|
||||||
query VendorOverviewPageQuery($vendorId: ID!) {
|
query VendorOverviewPageQuery($vendorId: ID!) {
|
||||||
@@ -205,188 +204,176 @@ function VendorOverviewPageContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageTemplate title={formData.name}>
|
||||||
<Helmet>
|
<div className="max-w-2xl space-y-6">
|
||||||
<title>Vendor - Probo</title>
|
<EditableField
|
||||||
</Helmet>
|
label="Name"
|
||||||
<div className="container">
|
value={formData.name}
|
||||||
<PageHeader className="mb-17" title={formData.name} />
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
<div className="max-w-4xl space-y-6">
|
/>
|
||||||
<EditableField
|
|
||||||
label="Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Description"
|
label="Description"
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-lg font-medium">Service Information</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Basic information about the vendor service
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<EditableField
|
||||||
|
label="Service Start At"
|
||||||
|
value={formData.serviceStartAt}
|
||||||
|
type="datetime-local"
|
||||||
|
onChange={(value) => handleFieldChange("serviceStartAt", value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Service Termination At"
|
||||||
|
value={formData.serviceTerminationAt}
|
||||||
|
type="datetime-local"
|
||||||
|
onChange={(value) =>
|
||||||
|
handleFieldChange("serviceTerminationAt", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h2 className="text-lg font-medium">Service Information</h2>
|
<div className="flex items-center gap-2">
|
||||||
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
|
<Label className="text-sm">Service Criticality</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleFieldChange("serviceCriticality", "LOW")
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
|
formData.serviceCriticality === "LOW"
|
||||||
|
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||||
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Low
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleFieldChange("serviceCriticality", "MEDIUM")
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
|
formData.serviceCriticality === "MEDIUM"
|
||||||
|
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||||
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Medium
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleFieldChange("serviceCriticality", "HIGH")
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
|
formData.serviceCriticality === "HIGH"
|
||||||
|
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||||
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
High
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Basic information about the vendor service
|
{formData.serviceCriticality === "HIGH" &&
|
||||||
|
"Critical service - downtime severely impacts end-users"}
|
||||||
|
{formData.serviceCriticality === "MEDIUM" &&
|
||||||
|
"Important service - downtime moderately affects end-users"}
|
||||||
|
{formData.serviceCriticality === "LOW" &&
|
||||||
|
"Non-critical service - minimal end-user impact if down"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
<EditableField
|
<div className="flex items-center gap-2">
|
||||||
label="Service Start At"
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
value={formData.serviceStartAt}
|
<Label className="text-sm">Risk Tier</Label>
|
||||||
type="datetime-local"
|
|
||||||
onChange={(value) =>
|
|
||||||
handleFieldChange("serviceStartAt", value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Service Termination At"
|
|
||||||
value={formData.serviceTerminationAt}
|
|
||||||
type="datetime-local"
|
|
||||||
onChange={(value) =>
|
|
||||||
handleFieldChange("serviceTerminationAt", value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
|
||||||
<Label className="text-sm">Service Criticality</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
handleFieldChange("serviceCriticality", "LOW")
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.serviceCriticality === "LOW"
|
|
||||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Low
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
handleFieldChange("serviceCriticality", "MEDIUM")
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.serviceCriticality === "MEDIUM"
|
|
||||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Medium
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
handleFieldChange("serviceCriticality", "HIGH")
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.serviceCriticality === "HIGH"
|
|
||||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
High
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{formData.serviceCriticality === "HIGH" &&
|
|
||||||
"Critical service - downtime severely impacts end-users"}
|
|
||||||
{formData.serviceCriticality === "MEDIUM" &&
|
|
||||||
"Important service - downtime moderately affects end-users"}
|
|
||||||
{formData.serviceCriticality === "LOW" &&
|
|
||||||
"Non-critical service - minimal end-user impact if down"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
<div className="space-y-2">
|
<button
|
||||||
<div className="flex items-center gap-2">
|
onClick={() => handleFieldChange("riskTier", "CRITICAL")}
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
className={cn(
|
||||||
<Label className="text-sm">Risk Tier</Label>
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
</div>
|
formData.riskTier === "CRITICAL"
|
||||||
<div className="flex gap-2">
|
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||||
<button
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
onClick={() => handleFieldChange("riskTier", "CRITICAL")}
|
)}
|
||||||
className={cn(
|
>
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
Critical
|
||||||
formData.riskTier === "CRITICAL"
|
</button>
|
||||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
<button
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
onClick={() => handleFieldChange("riskTier", "SIGNIFICANT")}
|
||||||
)}
|
className={cn(
|
||||||
>
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
Critical
|
formData.riskTier === "SIGNIFICANT"
|
||||||
</button>
|
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||||
<button
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
onClick={() =>
|
)}
|
||||||
handleFieldChange("riskTier", "SIGNIFICANT")
|
>
|
||||||
}
|
Significant
|
||||||
className={cn(
|
</button>
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
<button
|
||||||
formData.riskTier === "SIGNIFICANT"
|
onClick={() => handleFieldChange("riskTier", "GENERAL")}
|
||||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
className={cn(
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
)}
|
formData.riskTier === "GENERAL"
|
||||||
>
|
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||||
Significant
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
</button>
|
)}
|
||||||
<button
|
>
|
||||||
onClick={() => handleFieldChange("riskTier", "GENERAL")}
|
General
|
||||||
className={cn(
|
</button>
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.riskTier === "GENERAL"
|
|
||||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
General
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{formData.riskTier === "CRITICAL" &&
|
|
||||||
"Handles sensitive data, critical for platform operation"}
|
|
||||||
{formData.riskTier === "SIGNIFICANT" &&
|
|
||||||
"No user data access, but important for platform management"}
|
|
||||||
{formData.riskTier === "GENERAL" &&
|
|
||||||
"General vendor with minimal risk"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
<EditableField
|
{formData.riskTier === "CRITICAL" &&
|
||||||
label="Status Page URL"
|
"Handles sensitive data, critical for platform operation"}
|
||||||
value={formData.statusPageUrl || ""}
|
{formData.riskTier === "SIGNIFICANT" &&
|
||||||
onChange={(value) =>
|
"No user data access, but important for platform management"}
|
||||||
handleFieldChange("statusPageUrl", value)
|
{formData.riskTier === "GENERAL" &&
|
||||||
}
|
"General vendor with minimal risk"}
|
||||||
/>
|
</p>
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Terms of Service URL"
|
|
||||||
value={formData.termsOfServiceUrl || ""}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleFieldChange("termsOfServiceUrl", value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Privacy Policy URL"
|
|
||||||
value={formData.privacyPolicyUrl || ""}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleFieldChange("privacyPolicyUrl", value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Status Page URL"
|
||||||
|
value={formData.statusPageUrl || ""}
|
||||||
|
onChange={(value) => handleFieldChange("statusPageUrl", value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Terms of Service URL"
|
||||||
|
value={formData.termsOfServiceUrl || ""}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleFieldChange("termsOfServiceUrl", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Privacy Policy URL"
|
||||||
|
value={formData.privacyPolicyUrl || ""}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleFieldChange("privacyPolicyUrl", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
@@ -402,13 +389,13 @@ function VendorOverviewPageContent({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VendorOverviewPageFallback() {
|
export function VendorOverviewPageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<PageTemplateSkeleton>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
||||||
@@ -418,7 +405,7 @@ function VendorOverviewPageFallback() {
|
|||||||
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageTemplateSkeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,17 +420,12 @@ export default function VendorOverviewPage() {
|
|||||||
}, [loadQuery, vendorId]);
|
}, [loadQuery, vendorId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <VendorOverviewPageFallback />;
|
return <VendorOverviewPageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<VendorOverviewPageSkeleton />}>
|
||||||
<Helmet>
|
<VendorOverviewPageContent queryRef={queryRef} />
|
||||||
<title>Vendor Overview - Probo Console</title>
|
</Suspense>
|
||||||
</Helmet>
|
|
||||||
<Suspense fallback={<VendorOverviewPageFallback />}>
|
|
||||||
<VendorOverviewPageContent queryRef={queryRef} />
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user