Fix some lazy suspense fallbacks
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -5,8 +5,6 @@ import { ControlOverviewPageSkeleton } from "./frameworks/controls/ControlOvervi
|
||||
import { CreateControlPageSkeleton } from "./frameworks/controls/CreateControlPage";
|
||||
import { UpdateControlPageSkeleton } from "./frameworks/controls/UpdateControlPage";
|
||||
import { CreateFrameworkPageSkeleton } from "./frameworks/CreateFrameworkPage";
|
||||
import { FrameworkListPageSkeleton } from "./frameworks/FrameworkListPage";
|
||||
import { FrameworkOverviewPageSkeleton } from "./frameworks/FrameworkOverviewPage";
|
||||
import { UpdateFrameworkPageSkeleton } from "./frameworks/UpdateFrameworkPage";
|
||||
import "./HomePage";
|
||||
import { CreatePeoplePageSkeleton } from "./people/CreatePeoplePage";
|
||||
@@ -17,11 +15,13 @@ import { PolicyListPageSkeleton } from "./policies/PolicyListPage";
|
||||
import { PolicyOverviewPageSkeleton } from "./policies/PolicyOverviewPage";
|
||||
import { UpdatePolicyPageSkeleton } from "./policies/UpdatePolicyPage";
|
||||
import { SettingsPageSkeleton } from "./SettingsPage";
|
||||
import { VendorListPageSkeleton } from "./vendors/VendorListPage";
|
||||
import { VendorListPage } from "./vendors/VendorListPage";
|
||||
import { VendorOverviewPageSkeleton } from "./vendors/VendorOverviewPage";
|
||||
import { ErrorBoundaryWithLocation } from "./ErrorBoundary";
|
||||
import OrganizationLayout from "./OrganizationLayout";
|
||||
import NoOrganizationLayout from "./NoOrganizationLayout";
|
||||
import { FrameworkListPage } from "./frameworks/FrameworkListPage";
|
||||
import { FrameworkPage } from "./frameworks/FrameworkPage";
|
||||
|
||||
const CreateOrganizationPage = lazy(() => import("./CreateOrganizationPage"));
|
||||
const ControlOverviewPage = lazy(
|
||||
@@ -36,10 +36,6 @@ const UpdateControlPage = lazy(
|
||||
const CreateFrameworkPage = lazy(
|
||||
() => import("./frameworks/CreateFrameworkPage")
|
||||
);
|
||||
const FrameworkListPage = lazy(() => import("./frameworks/FrameworkListPage"));
|
||||
const FrameworkOverviewPage = lazy(
|
||||
() => import("./frameworks/FrameworkOverviewPage")
|
||||
);
|
||||
const UpdateFrameworkPage = lazy(
|
||||
() => import("./frameworks/UpdateFrameworkPage")
|
||||
);
|
||||
@@ -52,7 +48,7 @@ const PolicyListPage = lazy(() => import("./policies/PolicyListPage"));
|
||||
const PolicyOverviewPage = lazy(() => import("./policies/PolicyOverviewPage"));
|
||||
const UpdatePolicyPage = lazy(() => import("./policies/UpdatePolicyPage"));
|
||||
const SettingsPage = lazy(() => import("./SettingsPage"));
|
||||
const VendorListPage = lazy(() => import("./vendors/VendorListPage"));
|
||||
|
||||
const VendorOverviewPage = lazy(() => import("./vendors/VendorOverviewPage"));
|
||||
|
||||
export function OrganizationsRoutes() {
|
||||
@@ -99,26 +95,8 @@ export function OrganizationsRoutes() {
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="vendors"
|
||||
element={
|
||||
<Suspense fallback={<VendorListPageSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<VendorListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks"
|
||||
element={
|
||||
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path="vendors" element={<VendorListPage />} />
|
||||
<Route path="frameworks" element={<FrameworkListPage />} />
|
||||
<Route
|
||||
path="frameworks/create"
|
||||
element={
|
||||
@@ -129,16 +107,7 @@ export function OrganizationsRoutes() {
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks/:frameworkId"
|
||||
element={
|
||||
<Suspense fallback={<FrameworkOverviewPageSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path="frameworks/:frameworkId" element={<FrameworkPage />} />
|
||||
<Route
|
||||
path="frameworks/:frameworkId/update"
|
||||
element={
|
||||
|
||||
@@ -1,285 +1,12 @@
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
ConnectionHandler,
|
||||
} from "react-relay";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||
import { useLocation } from "react-router";
|
||||
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Link, useParams } from "react-router";
|
||||
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FrameworkListPageImportFrameworkMutation as FrameworkListPageImportFrameworkMutationType } from "./__generated__/FrameworkListPageImportFrameworkMutation.graphql";
|
||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||
|
||||
const FrameworkListPageQuery = graphql`
|
||||
query FrameworkListPageQuery($organizationId: ID!) {
|
||||
organization: node(id: $organizationId) {
|
||||
... on Organization {
|
||||
frameworks(first: 100)
|
||||
@connection(key: "FrameworkListPage_frameworks") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
controls(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const FrameworkListView = lazy(() => import("./FrameworkListView"));
|
||||
|
||||
const FrameworkListPageImportFrameworkMutation = graphql`
|
||||
mutation FrameworkListPageImportFrameworkMutation(
|
||||
$input: ImportFrameworkInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
importFramework(input: $input) {
|
||||
frameworkEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
controls(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function FrameworkCard({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
status,
|
||||
progress,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
status?: string;
|
||||
progress?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="relative overflow-hidden border bg-card p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="size-16">{icon}</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold">{title}</h3>
|
||||
{status && (
|
||||
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
|
||||
{status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
|
||||
{progress && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="size-2 rounded-full bg-yellow-400" />
|
||||
{progress}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function FrameworkListPageContent({
|
||||
queryRef,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<FrameworkListPageQueryType>;
|
||||
}) {
|
||||
const data = usePreloadedQuery<FrameworkListPageQueryType>(
|
||||
FrameworkListPageQuery,
|
||||
queryRef
|
||||
);
|
||||
const { organizationId } = useParams();
|
||||
const frameworks =
|
||||
data.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
|
||||
|
||||
const [importFramework] =
|
||||
useMutation<FrameworkListPageImportFrameworkMutationType>(
|
||||
FrameworkListPageImportFrameworkMutation
|
||||
);
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
importFramework({
|
||||
variables: {
|
||||
connections: [
|
||||
ConnectionHandler.getConnectionID(
|
||||
organizationId!,
|
||||
"FrameworkListPage_frameworks"
|
||||
),
|
||||
],
|
||||
input: {
|
||||
organizationId: organizationId!,
|
||||
file: null,
|
||||
},
|
||||
},
|
||||
uploadables: {
|
||||
"input.file": file,
|
||||
},
|
||||
onCompleted: () => {
|
||||
setIsUploading(false);
|
||||
setIsImportDialogOpen(false);
|
||||
toast({
|
||||
title: "Framework imported",
|
||||
description: "Framework has been imported successfully.",
|
||||
variant: "default",
|
||||
});
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsUploading(false);
|
||||
toast({
|
||||
title: "Error importing framework",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
title="Frameworks"
|
||||
description="Manage your compliance frameworks"
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Dialog
|
||||
open={isImportDialogOpen}
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Import Framework
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Framework</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="framework-file">Upload Framework File</Label>
|
||||
<Input
|
||||
id="framework-file"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
accept=".json"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upload a JSON file containing your framework definition.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button asChild>
|
||||
<Link to={`/organizations/${organizationId}/frameworks/create`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Framework
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||
{frameworks.map((framework) => {
|
||||
const validatedControls = framework.controls.edges.filter(
|
||||
(edge) => edge?.node?.state === "IMPLEMENTED"
|
||||
).length;
|
||||
const totalControls = framework.controls.edges.length;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={framework.id}
|
||||
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
|
||||
>
|
||||
<FrameworkCard
|
||||
title={framework.name}
|
||||
description={framework.description}
|
||||
icon={
|
||||
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-lg font-semibold text-blue-900">
|
||||
{framework.name.split(" ")[0]}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
status={
|
||||
validatedControls === totalControls
|
||||
? "Compliant"
|
||||
: undefined
|
||||
}
|
||||
progress={
|
||||
validatedControls === totalControls
|
||||
? "All controls validated"
|
||||
: `${validatedControls}/${totalControls} Controls validated`
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function FrameworkListPageSkeleton() {
|
||||
export function FrameworkListSkeleton() {
|
||||
return (
|
||||
<PageTemplateSkeleton
|
||||
title="Frameworks"
|
||||
@@ -309,24 +36,14 @@ export function FrameworkListPageSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function FrameworkListPage() {
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
|
||||
FrameworkListPageQuery
|
||||
);
|
||||
|
||||
const { organizationId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ organizationId: organizationId! });
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <FrameworkListPageSkeleton />;
|
||||
}
|
||||
export function FrameworkListPage() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
||||
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
||||
<Suspense key={location.pathname} fallback={<FrameworkListSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkListView />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
ConnectionHandler,
|
||||
} from "react-relay";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Link, useParams } from "react-router";
|
||||
import type { FrameworkListViewQuery as FrameworkListViewQueryType } from "./__generated__/FrameworkListViewQuery.graphql";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FrameworkListViewImportFrameworkMutation as FrameworkListViewImportFrameworkMutationType } from "./__generated__/FrameworkListViewImportFrameworkMutation.graphql";
|
||||
import { PageTemplate } from "@/components/PageTemplate";
|
||||
import { FrameworkListSkeleton } from "./FrameworkListPage";
|
||||
|
||||
const FrameworkListViewQuery = graphql`
|
||||
query FrameworkListViewQuery($organizationId: ID!) {
|
||||
organization: node(id: $organizationId) {
|
||||
... on Organization {
|
||||
frameworks(first: 100)
|
||||
@connection(key: "FrameworkListView_frameworks") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
controls(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const FrameworkListViewImportFrameworkMutation = graphql`
|
||||
mutation FrameworkListViewImportFrameworkMutation(
|
||||
$input: ImportFrameworkInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
importFramework(input: $input) {
|
||||
frameworkEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
controls(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function FrameworkCard({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
status,
|
||||
progress,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
status?: string;
|
||||
progress?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="relative overflow-hidden border bg-card p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="size-16">{icon}</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold">{title}</h3>
|
||||
{status && (
|
||||
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
|
||||
{status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
|
||||
{progress && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="size-2 rounded-full bg-yellow-400" />
|
||||
{progress}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function FrameworkListViewContent({
|
||||
queryRef,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<FrameworkListViewQueryType>;
|
||||
}) {
|
||||
const data = usePreloadedQuery<FrameworkListViewQueryType>(
|
||||
FrameworkListViewQuery,
|
||||
queryRef
|
||||
);
|
||||
const { organizationId } = useParams();
|
||||
const frameworks =
|
||||
data.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
|
||||
|
||||
const [importFramework] =
|
||||
useMutation<FrameworkListViewImportFrameworkMutationType>(
|
||||
FrameworkListViewImportFrameworkMutation
|
||||
);
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
importFramework({
|
||||
variables: {
|
||||
connections: [
|
||||
ConnectionHandler.getConnectionID(
|
||||
organizationId!,
|
||||
"FrameworkListView_frameworks"
|
||||
),
|
||||
],
|
||||
input: {
|
||||
organizationId: organizationId!,
|
||||
file: null,
|
||||
},
|
||||
},
|
||||
uploadables: {
|
||||
"input.file": file,
|
||||
},
|
||||
onCompleted: () => {
|
||||
setIsUploading(false);
|
||||
setIsImportDialogOpen(false);
|
||||
toast({
|
||||
title: "Framework imported",
|
||||
description: "Framework has been imported successfully.",
|
||||
variant: "default",
|
||||
});
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsUploading(false);
|
||||
toast({
|
||||
title: "Error importing framework",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
title="Frameworks"
|
||||
description="Manage your compliance frameworks"
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Dialog
|
||||
open={isImportDialogOpen}
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Import Framework
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Framework</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="framework-file">Upload Framework File</Label>
|
||||
<Input
|
||||
id="framework-file"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
accept=".json"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upload a JSON file containing your framework definition.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button asChild>
|
||||
<Link to={`/organizations/${organizationId}/frameworks/create`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Framework
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||
{frameworks.map((framework) => {
|
||||
const validatedControls = framework.controls.edges.filter(
|
||||
(edge) => edge?.node?.state === "IMPLEMENTED"
|
||||
).length;
|
||||
const totalControls = framework.controls.edges.length;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={framework.id}
|
||||
to={`/organizations/${organizationId}/frameworks/${framework.id}`}
|
||||
>
|
||||
<FrameworkCard
|
||||
title={framework.name}
|
||||
description={framework.description}
|
||||
icon={
|
||||
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-lg font-semibold text-blue-900">
|
||||
{framework.name.split(" ")[0]}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
status={
|
||||
validatedControls === totalControls
|
||||
? "Compliant"
|
||||
: undefined
|
||||
}
|
||||
progress={
|
||||
validatedControls === totalControls
|
||||
? "All controls validated"
|
||||
: `${validatedControls}/${totalControls} Controls validated`
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FrameworkListView() {
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkListViewQueryType>(
|
||||
FrameworkListViewQuery
|
||||
);
|
||||
|
||||
const { organizationId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ organizationId: organizationId! });
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <FrameworkListSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<FrameworkListSkeleton />}>
|
||||
{queryRef && <FrameworkListViewContent queryRef={queryRef} />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||
|
||||
const FrameworkView = lazy(() => import("./FrameworkView"));
|
||||
|
||||
export function FrameworkViewSkeleton() {
|
||||
return (
|
||||
<PageTemplateSkeleton>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<div className="relative mb-6">
|
||||
<div className="bg-muted w-24 h-24 rounded-full animate-pulse mb-4" />
|
||||
<div className="h-6 w-48 bg-muted animate-pulse rounded mb-2" />
|
||||
<div className="h-20 w-full bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
<div className="h-4 w-32 bg-muted animate-pulse rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</PageTemplateSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
export function FrameworkPage() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Suspense key={location.pathname} fallback={<FrameworkViewSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkView />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -19,18 +19,18 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
|
||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||
import type { FrameworkViewQuery as FrameworkViewQueryType } from "./__generated__/FrameworkViewQuery.graphql";
|
||||
import { PageTemplate } from "@/components/PageTemplate";
|
||||
import { FrameworkViewSkeleton } from "./FrameworkPage";
|
||||
|
||||
const FrameworkOverviewPageQuery = graphql`
|
||||
query FrameworkOverviewPageQuery($frameworkId: ID!) {
|
||||
const FrameworkViewQuery = graphql`
|
||||
query FrameworkViewQuery($frameworkId: ID!) {
|
||||
node(id: $frameworkId) {
|
||||
id
|
||||
... on Framework {
|
||||
name
|
||||
description
|
||||
controls(first: 100)
|
||||
@connection(key: "FrameworkOverviewPage_controls") {
|
||||
controls(first: 100) @connection(key: "FrameworkView_controls") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
@@ -66,12 +66,12 @@ interface Category {
|
||||
controls: Control[];
|
||||
}
|
||||
|
||||
function FrameworkOverviewPageContent({
|
||||
function FrameworkViewContent({
|
||||
queryRef,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<FrameworkOverviewPageQueryType>;
|
||||
queryRef: PreloadedQuery<FrameworkViewQueryType>;
|
||||
}) {
|
||||
const data = usePreloadedQuery(FrameworkOverviewPageQuery, queryRef);
|
||||
const data = usePreloadedQuery(FrameworkViewQuery, queryRef);
|
||||
const framework = data.node;
|
||||
const controls = framework.controls?.edges.map((edge) => edge?.node) ?? [];
|
||||
const navigate = useNavigate();
|
||||
@@ -326,44 +326,22 @@ function FrameworkOverviewPageContent({
|
||||
);
|
||||
}
|
||||
|
||||
export function FrameworkOverviewPageSkeleton() {
|
||||
return (
|
||||
<PageTemplateSkeleton>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<div className="relative mb-6">
|
||||
<div className="bg-muted w-24 h-24 rounded-full animate-pulse mb-4" />
|
||||
<div className="h-6 w-48 bg-muted animate-pulse rounded mb-2" />
|
||||
<div className="h-20 w-full bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
<div className="h-4 w-32 bg-muted animate-pulse rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</PageTemplateSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FrameworkOverviewPage() {
|
||||
export default function FrameworkView() {
|
||||
const { frameworkId } = useParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(
|
||||
FrameworkOverviewPageQuery
|
||||
);
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<FrameworkViewQueryType>(FrameworkViewQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ frameworkId: frameworkId! });
|
||||
}, [loadQuery, frameworkId]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <FrameworkOverviewPageSkeleton />;
|
||||
return <FrameworkViewSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<FrameworkOverviewPageSkeleton />}>
|
||||
{queryRef && <FrameworkOverviewPageContent queryRef={queryRef} />}
|
||||
<Suspense fallback={<FrameworkViewSkeleton />}>
|
||||
{queryRef && <FrameworkViewContent queryRef={queryRef} />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<f8b972f4147ecb52da49f44ef9c62df1>>
|
||||
* @generated SignedSource<<0d36f3f459adaca2f0abab59d9208bf9>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -14,11 +14,11 @@ export type ImportFrameworkInput = {
|
||||
file: any;
|
||||
organizationId: string;
|
||||
};
|
||||
export type FrameworkListPageImportFrameworkMutation$variables = {
|
||||
export type FrameworkListViewImportFrameworkMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: ImportFrameworkInput;
|
||||
};
|
||||
export type FrameworkListPageImportFrameworkMutation$data = {
|
||||
export type FrameworkListViewImportFrameworkMutation$data = {
|
||||
readonly importFramework: {
|
||||
readonly frameworkEdge: {
|
||||
readonly node: {
|
||||
@@ -39,9 +39,9 @@ export type FrameworkListPageImportFrameworkMutation$data = {
|
||||
};
|
||||
};
|
||||
};
|
||||
export type FrameworkListPageImportFrameworkMutation = {
|
||||
response: FrameworkListPageImportFrameworkMutation$data;
|
||||
variables: FrameworkListPageImportFrameworkMutation$variables;
|
||||
export type FrameworkListViewImportFrameworkMutation = {
|
||||
response: FrameworkListViewImportFrameworkMutation$data;
|
||||
variables: FrameworkListViewImportFrameworkMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -175,7 +175,7 @@ return {
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "FrameworkListPageImportFrameworkMutation",
|
||||
"name": "FrameworkListViewImportFrameworkMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -200,7 +200,7 @@ return {
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "FrameworkListPageImportFrameworkMutation",
|
||||
"name": "FrameworkListViewImportFrameworkMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -233,16 +233,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "946e840d5f7bb3a33e7ee1d0e2180bb5",
|
||||
"cacheID": "b225cace8b9956333d0175a7e0a4c04d",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "FrameworkListPageImportFrameworkMutation",
|
||||
"name": "FrameworkListViewImportFrameworkMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation FrameworkListPageImportFrameworkMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n description\n controls(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n}\n"
|
||||
"text": "mutation FrameworkListViewImportFrameworkMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n description\n controls(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "76c55c89cce3718241b2ee63f026197b";
|
||||
(node as any).hash = "fa1a8aa9d16bc50eac99392d3de59f6c";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<25802163c3ca6cc24524ebbc1388cd95>>
|
||||
* @generated SignedSource<<c2ec2b3948d67ed6b924494adcae63aa>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||
export type FrameworkListPageQuery$variables = {
|
||||
export type FrameworkListViewQuery$variables = {
|
||||
organizationId: string;
|
||||
};
|
||||
export type FrameworkListPageQuery$data = {
|
||||
export type FrameworkListViewQuery$data = {
|
||||
readonly organization: {
|
||||
readonly frameworks?: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
@@ -36,9 +36,9 @@ export type FrameworkListPageQuery$data = {
|
||||
};
|
||||
};
|
||||
};
|
||||
export type FrameworkListPageQuery = {
|
||||
response: FrameworkListPageQuery$data;
|
||||
variables: FrameworkListPageQuery$variables;
|
||||
export type FrameworkListViewQuery = {
|
||||
response: FrameworkListViewQuery$data;
|
||||
variables: FrameworkListViewQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -209,7 +209,7 @@ return {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "FrameworkListPageQuery",
|
||||
"name": "FrameworkListViewQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "organization",
|
||||
@@ -227,7 +227,7 @@ return {
|
||||
"args": null,
|
||||
"concreteType": "FrameworkConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__FrameworkListPage_frameworks_connection",
|
||||
"name": "__FrameworkListView_frameworks_connection",
|
||||
"plural": false,
|
||||
"selections": (v5/*: any*/),
|
||||
"storageKey": null
|
||||
@@ -247,7 +247,7 @@ return {
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "FrameworkListPageQuery",
|
||||
"name": "FrameworkListViewQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "organization",
|
||||
@@ -276,7 +276,7 @@ return {
|
||||
"args": (v3/*: any*/),
|
||||
"filters": null,
|
||||
"handle": "connection",
|
||||
"key": "FrameworkListPage_frameworks",
|
||||
"key": "FrameworkListView_frameworks",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "frameworks"
|
||||
}
|
||||
@@ -291,7 +291,7 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "b55da571816341f2576684823b6d67d0",
|
||||
"cacheID": "88bb77e2d10398380353d5a90ba5d6d6",
|
||||
"id": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
@@ -306,13 +306,13 @@ return {
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "FrameworkListPageQuery",
|
||||
"name": "FrameworkListViewQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query FrameworkListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n frameworks(first: 100) {\n edges {\n node {\n id\n name\n description\n controls(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n"
|
||||
"text": "query FrameworkListViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n frameworks(first: 100) {\n edges {\n node {\n id\n name\n description\n controls(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "2070e0cdfaf5b1e5b4dfbb262127218b";
|
||||
(node as any).hash = "e5d315fb00e4fee74b53685b6441ed54";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<d673a8265dbda865ed008ca8d8d91c38>>
|
||||
* @generated SignedSource<<c36de2ad1593c2dfefe28d7d3bbbddba>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -11,10 +11,10 @@
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||
export type FrameworkOverviewPageQuery$variables = {
|
||||
export type FrameworkViewQuery$variables = {
|
||||
frameworkId: string;
|
||||
};
|
||||
export type FrameworkOverviewPageQuery$data = {
|
||||
export type FrameworkViewQuery$data = {
|
||||
readonly node: {
|
||||
readonly controls?: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
@@ -33,9 +33,9 @@ export type FrameworkOverviewPageQuery$data = {
|
||||
readonly name?: string;
|
||||
};
|
||||
};
|
||||
export type FrameworkOverviewPageQuery = {
|
||||
response: FrameworkOverviewPageQuery$data;
|
||||
variables: FrameworkOverviewPageQuery$variables;
|
||||
export type FrameworkViewQuery = {
|
||||
response: FrameworkViewQuery$data;
|
||||
variables: FrameworkViewQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -174,7 +174,7 @@ return {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "FrameworkOverviewPageQuery",
|
||||
"name": "FrameworkViewQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -195,7 +195,7 @@ return {
|
||||
"args": null,
|
||||
"concreteType": "ControlConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__FrameworkOverviewPage_controls_connection",
|
||||
"name": "__FrameworkView_controls_connection",
|
||||
"plural": false,
|
||||
"selections": (v6/*: any*/),
|
||||
"storageKey": null
|
||||
@@ -215,7 +215,7 @@ return {
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "FrameworkOverviewPageQuery",
|
||||
"name": "FrameworkViewQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -247,7 +247,7 @@ return {
|
||||
"args": (v7/*: any*/),
|
||||
"filters": null,
|
||||
"handle": "connection",
|
||||
"key": "FrameworkOverviewPage_controls",
|
||||
"key": "FrameworkView_controls",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "controls"
|
||||
}
|
||||
@@ -261,7 +261,7 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "d1033e296ed81f7dd74c61e04dbc7d1f",
|
||||
"cacheID": "f8edefde219c5fa1a5c7c85e287d846a",
|
||||
"id": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
@@ -276,13 +276,13 @@ return {
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "FrameworkOverviewPageQuery",
|
||||
"name": "FrameworkViewQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query FrameworkOverviewPageQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n category\n importance\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||
"text": "query FrameworkViewQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n category\n importance\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "0f64849d3b9455a880811d65870243e0";
|
||||
(node as any).hash = "9e8a9b37d956ec4c9dfba88e7b1120f3";
|
||||
|
||||
export default node;
|
||||
@@ -1,462 +1,11 @@
|
||||
import { Suspense, useEffect, useState, useTransition } from "react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
usePaginationFragment,
|
||||
} from "react-relay";
|
||||
import { useSearchParams, useParams } from "react-router";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Store, ChevronRight, Trash2 } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Link } from "react-router";
|
||||
import Fuse from "fuse.js";
|
||||
import type { VendorListPageQuery as VendorListPageQueryType } from "./__generated__/VendorListPageQuery.graphql";
|
||||
import { VendorListPageCreateVendorMutation } from "./__generated__/VendorListPageCreateVendorMutation.graphql";
|
||||
import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPageDeleteVendorMutation.graphql";
|
||||
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
|
||||
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||
|
||||
const ITEMS_PER_PAGE = 25;
|
||||
const VendorListView = lazy(() => import("./VendorListView"));
|
||||
|
||||
const vendorListPageQuery = graphql`
|
||||
query VendorListPageQuery(
|
||||
$organizationId: ID!
|
||||
$first: Int
|
||||
$after: CursorKey
|
||||
$last: Int
|
||||
$before: CursorKey
|
||||
) {
|
||||
organization: node(id: $organizationId) {
|
||||
id
|
||||
|
||||
...VendorListPage_vendors
|
||||
@arguments(first: $first, after: $after, last: $last, before: $before)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const vendorListFragment = graphql`
|
||||
fragment VendorListPage_vendors on Organization
|
||||
@refetchable(queryName: "VendorListPagePaginationQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int" }
|
||||
after: { type: "CursorKey" }
|
||||
last: { type: "Int" }
|
||||
before: { type: "CursorKey" }
|
||||
) {
|
||||
vendors(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: { direction: ASC, field: NAME }
|
||||
) @connection(key: "VendorListPage_vendors") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
riskTier
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const createVendorMutation = graphql`
|
||||
mutation VendorListPageCreateVendorMutation(
|
||||
$input: CreateVendorInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
createVendor(input: $input) {
|
||||
vendorEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
riskTier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteVendorMutation = graphql`
|
||||
mutation VendorListPageDeleteVendorMutation(
|
||||
$input: DeleteVendorInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
deleteVendor(input: $input) {
|
||||
deletedVendorId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Define a proper type for the vendor items
|
||||
interface VendorItem {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// TODO: Remove this once we have a real list of vendors
|
||||
const vendorsList: VendorItem[] = [
|
||||
{ id: "1", name: "Amazon Web Services", createdAt: new Date().toISOString() },
|
||||
{
|
||||
id: "2",
|
||||
name: "Google Cloud Platform",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{ id: "3", name: "Microsoft Azure", createdAt: new Date().toISOString() },
|
||||
{ id: "4", name: "Salesforce", createdAt: new Date().toISOString() },
|
||||
{ id: "5", name: "Slack", createdAt: new Date().toISOString() },
|
||||
{ id: "6", name: "Zoom", createdAt: new Date().toISOString() },
|
||||
{ id: "7", name: "Dropbox", createdAt: new Date().toISOString() },
|
||||
{ id: "8", name: "Trello", createdAt: new Date().toISOString() },
|
||||
{ id: "9", name: "Asana", createdAt: new Date().toISOString() },
|
||||
{ id: "10", name: "Notion", createdAt: new Date().toISOString() },
|
||||
{ id: "11", name: "GitHub", createdAt: new Date().toISOString() },
|
||||
{ id: "12", name: "GitLab", createdAt: new Date().toISOString() },
|
||||
{ id: "13", name: "Bitbucket", createdAt: new Date().toISOString() },
|
||||
{ id: "14", name: "Docker", createdAt: new Date().toISOString() },
|
||||
{ id: "15", name: "Kubernetes", createdAt: new Date().toISOString() },
|
||||
{ id: "16", name: "Jenkins", createdAt: new Date().toISOString() },
|
||||
{ id: "17", name: "CircleCI", createdAt: new Date().toISOString() },
|
||||
];
|
||||
|
||||
function LoadAboveButton({
|
||||
isLoading,
|
||||
hasMore,
|
||||
onLoadMore,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
}) {
|
||||
if (!hasMore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onLoadMore}
|
||||
disabled={isLoading || !hasMore}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? "Loading..." : "Load above"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadBelowButton({
|
||||
isLoading,
|
||||
hasMore,
|
||||
onLoadMore,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
}) {
|
||||
if (!hasMore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onLoadMore}
|
||||
disabled={isLoading || !hasMore}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? "Loading..." : "Load below"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VendorListContent({
|
||||
queryRef,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<VendorListPageQueryType>;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const data = usePreloadedQuery<VendorListPageQueryType>(
|
||||
vendorListPageQuery,
|
||||
queryRef
|
||||
);
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
const [, startTransition] = useTransition();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filteredVendors, setFilteredVendors] = useState<VendorItem[]>([]);
|
||||
const [createVendor] =
|
||||
useMutation<VendorListPageCreateVendorMutation>(createVendorMutation);
|
||||
const [deleteVendor] =
|
||||
useMutation<VendorListPageDeleteVendorMutation>(deleteVendorMutation);
|
||||
const { organizationId } = useParams();
|
||||
|
||||
const {
|
||||
data: vendorsConnection,
|
||||
loadNext,
|
||||
loadPrevious,
|
||||
hasNext,
|
||||
hasPrevious,
|
||||
isLoadingNext,
|
||||
isLoadingPrevious,
|
||||
} = usePaginationFragment<
|
||||
VendorListPagePaginationQuery,
|
||||
VendorListPage_vendors$key
|
||||
>(vendorListFragment, data.organization);
|
||||
|
||||
const vendors =
|
||||
vendorsConnection.vendors.edges.map((edge) => edge.node) ?? [];
|
||||
const pageInfo = vendorsConnection.vendors.pageInfo;
|
||||
|
||||
const fuse = new Fuse(vendorsList, {
|
||||
keys: ["name"],
|
||||
threshold: 0.3,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
title="Vendors"
|
||||
description="Vendors are third-party services that your company uses. Add them to
|
||||
keep track of their risk and compliance status."
|
||||
>
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Store className="h-5 w-5" />
|
||||
<h3 className="font-medium">Add a vendor</h3>
|
||||
</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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{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
|
||||
className="w-full px-3 py-2 text-left hover:bg-accent flex items-center gap-2"
|
||||
onClick={() => {
|
||||
createVendor({
|
||||
variables: {
|
||||
connections: [vendorsConnection.vendors.__id],
|
||||
input: {
|
||||
organizationId: data.organization.id,
|
||||
name: searchTerm.trim(),
|
||||
description: "",
|
||||
serviceStartAt: new Date().toISOString(),
|
||||
serviceCriticality: "LOW",
|
||||
riskTier: "GENERAL",
|
||||
},
|
||||
},
|
||||
onCompleted() {
|
||||
setSearchTerm("");
|
||||
setFilteredVendors([]);
|
||||
toast({
|
||||
title: "Vendor created",
|
||||
description:
|
||||
"The new vendor has been created successfully",
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="font-medium">Create new vendor:</span>{" "}
|
||||
{searchTerm}
|
||||
</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 className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
vendor.riskTier === "CRITICAL"
|
||||
? "bg-red-100 text-red-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||
: vendor?.riskTier === "SIGNIFICANT"
|
||||
? "bg-yellow-100 text-yellow-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||
: "bg-green-100 text-green-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||
}
|
||||
>
|
||||
{vendor.riskTier}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:bg-transparent hover:[&>svg]:text-destructive"
|
||||
onClick={(e) => {
|
||||
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({
|
||||
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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function VendorListPageSkeleton() {
|
||||
export function VendorListSkeleton() {
|
||||
return (
|
||||
<PageTemplateSkeleton
|
||||
title="Vendors"
|
||||
@@ -483,33 +32,14 @@ export function VendorListPageSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function VendorListPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<VendorListPageQueryType>(vendorListPageQuery);
|
||||
|
||||
const { organizationId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const after = searchParams.get("after");
|
||||
const before = searchParams.get("before");
|
||||
|
||||
loadQuery({
|
||||
organizationId: organizationId!,
|
||||
first: before ? undefined : ITEMS_PER_PAGE,
|
||||
after: after || undefined,
|
||||
last: before ? ITEMS_PER_PAGE : undefined,
|
||||
before: before || undefined,
|
||||
});
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <VendorListPageSkeleton />;
|
||||
}
|
||||
export function VendorListPage() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<VendorListPageSkeleton />}>
|
||||
<VendorListContent queryRef={queryRef} />
|
||||
<Suspense key={location.pathname} fallback={<VendorListSkeleton />}>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<VendorListView />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
489
apps/console/src/pages/organizations/vendors/VendorListView.tsx
vendored
Normal file
489
apps/console/src/pages/organizations/vendors/VendorListView.tsx
vendored
Normal file
@@ -0,0 +1,489 @@
|
||||
import { Suspense, useEffect, useState, useTransition } from "react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
usePaginationFragment,
|
||||
} from "react-relay";
|
||||
import { useSearchParams, useParams } from "react-router";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Store, ChevronRight, Trash2 } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Link } from "react-router";
|
||||
import Fuse from "fuse.js";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { PageTemplate } from "@/components/PageTemplate";
|
||||
import { VendorListSkeleton } from "./VendorListPage";
|
||||
import { VendorListViewQuery as VendorListViewQueryType } from "./__generated__/VendorListViewQuery.graphql";
|
||||
import { VendorListViewCreateVendorMutation } from "./__generated__/VendorListViewCreateVendorMutation.graphql";
|
||||
import { VendorListViewDeleteVendorMutation } from "./__generated__/VendorListViewDeleteVendorMutation.graphql";
|
||||
import { VendorListViewPaginationQuery } from "./__generated__/VendorListViewPaginationQuery.graphql";
|
||||
import { VendorListView_vendors$key } from "./__generated__/VendorListView_vendors.graphql";
|
||||
|
||||
const ITEMS_PER_PAGE = 25;
|
||||
|
||||
const vendorListViewQuery = graphql`
|
||||
query VendorListViewQuery(
|
||||
$organizationId: ID!
|
||||
$first: Int
|
||||
$after: CursorKey
|
||||
$last: Int
|
||||
$before: CursorKey
|
||||
) {
|
||||
organization: node(id: $organizationId) {
|
||||
id
|
||||
|
||||
...VendorListView_vendors
|
||||
@arguments(first: $first, after: $after, last: $last, before: $before)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const vendorListFragment = graphql`
|
||||
fragment VendorListView_vendors on Organization
|
||||
@refetchable(queryName: "VendorListViewPaginationQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int" }
|
||||
after: { type: "CursorKey" }
|
||||
last: { type: "Int" }
|
||||
before: { type: "CursorKey" }
|
||||
) {
|
||||
vendors(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: { direction: ASC, field: NAME }
|
||||
) @connection(key: "VendorListView_vendors") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
riskTier
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const createVendorMutation = graphql`
|
||||
mutation VendorListViewCreateVendorMutation(
|
||||
$input: CreateVendorInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
createVendor(input: $input) {
|
||||
vendorEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
riskTier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteVendorMutation = graphql`
|
||||
mutation VendorListViewDeleteVendorMutation(
|
||||
$input: DeleteVendorInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
deleteVendor(input: $input) {
|
||||
deletedVendorId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Define a proper type for the vendor items
|
||||
interface VendorItem {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// TODO: Remove this once we have a real list of vendors
|
||||
const vendorsList: VendorItem[] = [
|
||||
{ id: "1", name: "Amazon Web Services", createdAt: new Date().toISOString() },
|
||||
{
|
||||
id: "2",
|
||||
name: "Google Cloud Platform",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{ id: "3", name: "Microsoft Azure", createdAt: new Date().toISOString() },
|
||||
{ id: "4", name: "Salesforce", createdAt: new Date().toISOString() },
|
||||
{ id: "5", name: "Slack", createdAt: new Date().toISOString() },
|
||||
{ id: "6", name: "Zoom", createdAt: new Date().toISOString() },
|
||||
{ id: "7", name: "Dropbox", createdAt: new Date().toISOString() },
|
||||
{ id: "8", name: "Trello", createdAt: new Date().toISOString() },
|
||||
{ id: "9", name: "Asana", createdAt: new Date().toISOString() },
|
||||
{ id: "10", name: "Notion", createdAt: new Date().toISOString() },
|
||||
{ id: "11", name: "GitHub", createdAt: new Date().toISOString() },
|
||||
{ id: "12", name: "GitLab", createdAt: new Date().toISOString() },
|
||||
{ id: "13", name: "Bitbucket", createdAt: new Date().toISOString() },
|
||||
{ id: "14", name: "Docker", createdAt: new Date().toISOString() },
|
||||
{ id: "15", name: "Kubernetes", createdAt: new Date().toISOString() },
|
||||
{ id: "16", name: "Jenkins", createdAt: new Date().toISOString() },
|
||||
{ id: "17", name: "CircleCI", createdAt: new Date().toISOString() },
|
||||
];
|
||||
|
||||
function LoadAboveButton({
|
||||
isLoading,
|
||||
hasMore,
|
||||
onLoadMore,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
}) {
|
||||
if (!hasMore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onLoadMore}
|
||||
disabled={isLoading || !hasMore}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? "Loading..." : "Load above"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadBelowButton({
|
||||
isLoading,
|
||||
hasMore,
|
||||
onLoadMore,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
}) {
|
||||
if (!hasMore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onLoadMore}
|
||||
disabled={isLoading || !hasMore}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? "Loading..." : "Load below"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VendorListContent({
|
||||
queryRef,
|
||||
}: {
|
||||
queryRef: PreloadedQuery<VendorListViewQueryType>;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const data = usePreloadedQuery<VendorListViewQueryType>(
|
||||
vendorListViewQuery,
|
||||
queryRef
|
||||
);
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
const [, startTransition] = useTransition();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filteredVendors, setFilteredVendors] = useState<VendorItem[]>([]);
|
||||
const [createVendor] =
|
||||
useMutation<VendorListViewCreateVendorMutation>(createVendorMutation);
|
||||
const [deleteVendor] =
|
||||
useMutation<VendorListViewDeleteVendorMutation>(deleteVendorMutation);
|
||||
const { organizationId } = useParams();
|
||||
|
||||
const {
|
||||
data: vendorsConnection,
|
||||
loadNext,
|
||||
loadPrevious,
|
||||
hasNext,
|
||||
hasPrevious,
|
||||
isLoadingNext,
|
||||
isLoadingPrevious,
|
||||
} = usePaginationFragment<
|
||||
VendorListViewPaginationQuery,
|
||||
VendorListView_vendors$key
|
||||
>(vendorListFragment, data.organization);
|
||||
|
||||
const vendors =
|
||||
vendorsConnection.vendors.edges.map((edge) => edge.node) ?? [];
|
||||
const pageInfo = vendorsConnection.vendors.pageInfo;
|
||||
|
||||
const fuse = new Fuse(vendorsList, {
|
||||
keys: ["name"],
|
||||
threshold: 0.3,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
title="Vendors"
|
||||
description="Vendors are third-party services that your company uses. Add them to
|
||||
keep track of their risk and compliance status."
|
||||
>
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Store className="h-5 w-5" />
|
||||
<h3 className="font-medium">Add a vendor</h3>
|
||||
</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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{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
|
||||
className="w-full px-3 py-2 text-left hover:bg-accent flex items-center gap-2"
|
||||
onClick={() => {
|
||||
createVendor({
|
||||
variables: {
|
||||
connections: [vendorsConnection.vendors.__id],
|
||||
input: {
|
||||
organizationId: data.organization.id,
|
||||
name: searchTerm.trim(),
|
||||
description: "",
|
||||
serviceStartAt: new Date().toISOString(),
|
||||
serviceCriticality: "LOW",
|
||||
riskTier: "GENERAL",
|
||||
},
|
||||
},
|
||||
onCompleted() {
|
||||
setSearchTerm("");
|
||||
setFilteredVendors([]);
|
||||
toast({
|
||||
title: "Vendor created",
|
||||
description:
|
||||
"The new vendor has been created successfully",
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="font-medium">Create new vendor:</span>{" "}
|
||||
{searchTerm}
|
||||
</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 className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
vendor.riskTier === "CRITICAL"
|
||||
? "bg-red-100 text-red-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||
: vendor?.riskTier === "SIGNIFICANT"
|
||||
? "bg-yellow-100 text-yellow-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||
: "bg-green-100 text-green-900 rounded-full px-3 py-0.5 text-xs font-medium"
|
||||
}
|
||||
>
|
||||
{vendor.riskTier}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:bg-transparent hover:[&>svg]:text-destructive"
|
||||
onClick={(e) => {
|
||||
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({
|
||||
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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function VendorListView() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [queryRef, loadQuery] =
|
||||
useQueryLoader<VendorListViewQueryType>(vendorListViewQuery);
|
||||
|
||||
const { organizationId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const after = searchParams.get("after");
|
||||
const before = searchParams.get("before");
|
||||
|
||||
loadQuery({
|
||||
organizationId: organizationId!,
|
||||
first: before ? undefined : ITEMS_PER_PAGE,
|
||||
after: after || undefined,
|
||||
last: before ? ITEMS_PER_PAGE : undefined,
|
||||
before: before || undefined,
|
||||
});
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <VendorListSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<VendorListSkeleton />}>
|
||||
<VendorListContent queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
209
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewCreateVendorMutation.graphql.ts
generated
vendored
Normal file
209
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewCreateVendorMutation.graphql.ts
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @generated SignedSource<<38a771b621d1e813bc7dbdd6d5bcf323>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
|
||||
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
|
||||
export type CreateVendorInput = {
|
||||
description: string;
|
||||
name: string;
|
||||
organizationId: string;
|
||||
privacyPolicyUrl?: string | null | undefined;
|
||||
riskTier: RiskTier;
|
||||
serviceCriticality: ServiceCriticality;
|
||||
serviceStartAt: string;
|
||||
serviceTerminationAt?: string | null | undefined;
|
||||
statusPageUrl?: string | null | undefined;
|
||||
termsOfServiceUrl?: string | null | undefined;
|
||||
};
|
||||
export type VendorListViewCreateVendorMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: CreateVendorInput;
|
||||
};
|
||||
export type VendorListViewCreateVendorMutation$data = {
|
||||
readonly createVendor: {
|
||||
readonly vendorEdge: {
|
||||
readonly node: {
|
||||
readonly createdAt: string;
|
||||
readonly description: string;
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly riskTier: RiskTier;
|
||||
readonly updatedAt: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
export type VendorListViewCreateVendorMutation = {
|
||||
response: VendorListViewCreateVendorMutation$data;
|
||||
variables: VendorListViewCreateVendorMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "connections"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
},
|
||||
v2 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "vendorEdge",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "riskTier",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "VendorListViewCreateVendorMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "CreateVendorPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createVendor",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v1/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "VendorListViewCreateVendorMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "CreateVendorPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createVendor",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"filters": null,
|
||||
"handle": "prependEdge",
|
||||
"key": "",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "vendorEdge",
|
||||
"handleArgs": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "connections",
|
||||
"variableName": "connections"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "cedd0394889a1b00d329ed9f30dcb3b9",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "VendorListViewCreateVendorMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation VendorListViewCreateVendorMutation(\n $input: CreateVendorInput!\n) {\n createVendor(input: $input) {\n vendorEdge {\n node {\n id\n name\n description\n createdAt\n updatedAt\n riskTier\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "9be60a8a66ae0214cf280252be1c6b7b";
|
||||
|
||||
export default node;
|
||||
132
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewDeleteVendorMutation.graphql.ts
generated
vendored
Normal file
132
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewDeleteVendorMutation.graphql.ts
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @generated SignedSource<<a46552774a4d24ce7d1849d7485fb323>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type DeleteVendorInput = {
|
||||
vendorId: string;
|
||||
};
|
||||
export type VendorListViewDeleteVendorMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: DeleteVendorInput;
|
||||
};
|
||||
export type VendorListViewDeleteVendorMutation$data = {
|
||||
readonly deleteVendor: {
|
||||
readonly deletedVendorId: string;
|
||||
};
|
||||
};
|
||||
export type VendorListViewDeleteVendorMutation = {
|
||||
response: VendorListViewDeleteVendorMutation$data;
|
||||
variables: VendorListViewDeleteVendorMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "connections"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
},
|
||||
v2 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "deletedVendorId",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "VendorListViewDeleteVendorMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "DeleteVendorPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "deleteVendor",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v1/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "VendorListViewDeleteVendorMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "DeleteVendorPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "deleteVendor",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"filters": null,
|
||||
"handle": "deleteEdge",
|
||||
"key": "",
|
||||
"kind": "ScalarHandle",
|
||||
"name": "deletedVendorId",
|
||||
"handleArgs": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "connections",
|
||||
"variableName": "connections"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "7edee61297200dec1695f6b2325cd7f0",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "VendorListViewDeleteVendorMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation VendorListViewDeleteVendorMutation(\n $input: DeleteVendorInput!\n) {\n deleteVendor(input: $input) {\n deletedVendorId\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "85387685fd75c8b99fb9e9195d4a903f";
|
||||
|
||||
export default node;
|
||||
335
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewPaginationQuery.graphql.ts
generated
vendored
Normal file
335
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewPaginationQuery.graphql.ts
generated
vendored
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* @generated SignedSource<<e166844caf05e2dfc32321c2547cbf32>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type VendorListViewPaginationQuery$variables = {
|
||||
after?: string | null | undefined;
|
||||
before?: string | null | undefined;
|
||||
first?: number | null | undefined;
|
||||
id: string;
|
||||
last?: number | null | undefined;
|
||||
};
|
||||
export type VendorListViewPaginationQuery$data = {
|
||||
readonly node: {
|
||||
readonly " $fragmentSpreads": FragmentRefs<"VendorListView_vendors">;
|
||||
};
|
||||
};
|
||||
export type VendorListViewPaginationQuery = {
|
||||
response: VendorListViewPaginationQuery$data;
|
||||
variables: VendorListViewPaginationQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "after"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "before"
|
||||
},
|
||||
v2 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "first"
|
||||
},
|
||||
v3 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "id"
|
||||
},
|
||||
v4 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "last"
|
||||
},
|
||||
v5 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "id",
|
||||
"variableName": "id"
|
||||
}
|
||||
],
|
||||
v6 = {
|
||||
"kind": "Variable",
|
||||
"name": "after",
|
||||
"variableName": "after"
|
||||
},
|
||||
v7 = {
|
||||
"kind": "Variable",
|
||||
"name": "before",
|
||||
"variableName": "before"
|
||||
},
|
||||
v8 = {
|
||||
"kind": "Variable",
|
||||
"name": "first",
|
||||
"variableName": "first"
|
||||
},
|
||||
v9 = {
|
||||
"kind": "Variable",
|
||||
"name": "last",
|
||||
"variableName": "last"
|
||||
},
|
||||
v10 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
v11 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v12 = [
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/),
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "NAME"
|
||||
}
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "VendorListViewPaginationQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v5/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"args": [
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/)
|
||||
],
|
||||
"kind": "FragmentSpread",
|
||||
"name": "VendorListView_vendors"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "VendorListViewPaginationQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v5/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v10/*: any*/),
|
||||
(v11/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v12/*: any*/),
|
||||
"concreteType": "VendorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "vendors",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v11/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "riskTier",
|
||||
"storageKey": null
|
||||
},
|
||||
(v10/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasPreviousPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "startCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v12/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "VendorListView_vendors",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "vendors"
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "f447e5171c495d86e5132c58b2afc8c8",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "VendorListViewPaginationQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query VendorListViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorListView_vendors_pbnwq\n id\n }\n}\n\nfragment VendorListView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n riskTier\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "96466253eae17f60ffdf08ad59719e83";
|
||||
|
||||
export default node;
|
||||
337
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewQuery.graphql.ts
generated
vendored
Normal file
337
apps/console/src/pages/organizations/vendors/__generated__/VendorListViewQuery.graphql.ts
generated
vendored
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* @generated SignedSource<<843c11acfe2ed3f07fe2e05237f4cb08>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type VendorListViewQuery$variables = {
|
||||
after?: string | null | undefined;
|
||||
before?: string | null | undefined;
|
||||
first?: number | null | undefined;
|
||||
last?: number | null | undefined;
|
||||
organizationId: string;
|
||||
};
|
||||
export type VendorListViewQuery$data = {
|
||||
readonly organization: {
|
||||
readonly id: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"VendorListView_vendors">;
|
||||
};
|
||||
};
|
||||
export type VendorListViewQuery = {
|
||||
response: VendorListViewQuery$data;
|
||||
variables: VendorListViewQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "after"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "before"
|
||||
},
|
||||
v2 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "first"
|
||||
},
|
||||
v3 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "last"
|
||||
},
|
||||
v4 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "organizationId"
|
||||
},
|
||||
v5 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "id",
|
||||
"variableName": "organizationId"
|
||||
}
|
||||
],
|
||||
v6 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v7 = {
|
||||
"kind": "Variable",
|
||||
"name": "after",
|
||||
"variableName": "after"
|
||||
},
|
||||
v8 = {
|
||||
"kind": "Variable",
|
||||
"name": "before",
|
||||
"variableName": "before"
|
||||
},
|
||||
v9 = {
|
||||
"kind": "Variable",
|
||||
"name": "first",
|
||||
"variableName": "first"
|
||||
},
|
||||
v10 = {
|
||||
"kind": "Variable",
|
||||
"name": "last",
|
||||
"variableName": "last"
|
||||
},
|
||||
v11 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
v12 = [
|
||||
(v7/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/),
|
||||
(v10/*: any*/),
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "NAME"
|
||||
}
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "VendorListViewQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "organization",
|
||||
"args": (v5/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v6/*: any*/),
|
||||
{
|
||||
"args": [
|
||||
(v7/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/),
|
||||
(v10/*: any*/)
|
||||
],
|
||||
"kind": "FragmentSpread",
|
||||
"name": "VendorListView_vendors"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v4/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v0/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "VendorListViewQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "organization",
|
||||
"args": (v5/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v11/*: any*/),
|
||||
(v6/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v12/*: any*/),
|
||||
"concreteType": "VendorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "vendors",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v6/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "riskTier",
|
||||
"storageKey": null
|
||||
},
|
||||
(v11/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasPreviousPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "startCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v12/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "VendorListView_vendors",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "vendors"
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "4bef9b4458b57ed70122d2bcbb205788",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "VendorListViewQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query VendorListViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...VendorListView_vendors_pbnwq\n }\n}\n\nfragment VendorListView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n riskTier\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "ea4b2e523cecb2f3200afb17b4bd505a";
|
||||
|
||||
export default node;
|
||||
262
apps/console/src/pages/organizations/vendors/__generated__/VendorListView_vendors.graphql.ts
generated
vendored
Normal file
262
apps/console/src/pages/organizations/vendors/__generated__/VendorListView_vendors.graphql.ts
generated
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* @generated SignedSource<<7b424e7690762d554e28421c32fdee78>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type VendorListView_vendors$data = {
|
||||
readonly id: string;
|
||||
readonly vendors: {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly createdAt: string;
|
||||
readonly description: string;
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly riskTier: RiskTier;
|
||||
readonly updatedAt: string;
|
||||
};
|
||||
}>;
|
||||
readonly pageInfo: {
|
||||
readonly endCursor: string | null | undefined;
|
||||
readonly hasNextPage: boolean;
|
||||
readonly hasPreviousPage: boolean;
|
||||
readonly startCursor: string | null | undefined;
|
||||
};
|
||||
};
|
||||
readonly " $fragmentType": "VendorListView_vendors";
|
||||
};
|
||||
export type VendorListView_vendors$key = {
|
||||
readonly " $data"?: VendorListView_vendors$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"VendorListView_vendors">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = [
|
||||
"vendors"
|
||||
],
|
||||
v1 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"argumentDefinitions": [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "after"
|
||||
},
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "before"
|
||||
},
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "first"
|
||||
},
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "last"
|
||||
}
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": {
|
||||
"connection": [
|
||||
{
|
||||
"count": null,
|
||||
"cursor": null,
|
||||
"direction": "bidirectional",
|
||||
"path": (v0/*: any*/)
|
||||
}
|
||||
],
|
||||
"refetch": {
|
||||
"connection": {
|
||||
"forward": {
|
||||
"count": "first",
|
||||
"cursor": "after"
|
||||
},
|
||||
"backward": {
|
||||
"count": "last",
|
||||
"cursor": "before"
|
||||
},
|
||||
"path": (v0/*: any*/)
|
||||
},
|
||||
"fragmentPathInResult": [
|
||||
"node"
|
||||
],
|
||||
"operation": require('./VendorListViewPaginationQuery.graphql'),
|
||||
"identifierInfo": {
|
||||
"identifierField": "id",
|
||||
"identifierQueryVariableName": "id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "VendorListView_vendors",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "vendors",
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "NAME"
|
||||
}
|
||||
}
|
||||
],
|
||||
"concreteType": "VendorConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__VendorListView_vendors_connection",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "VendorEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "riskTier",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasPreviousPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "startCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": "__VendorListView_vendors_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "96466253eae17f60ffdf08ad59719e83";
|
||||
|
||||
export default node;
|
||||
Reference in New Issue
Block a user