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 { CreateControlPageSkeleton } from "./frameworks/controls/CreateControlPage";
|
||||||
import { UpdateControlPageSkeleton } from "./frameworks/controls/UpdateControlPage";
|
import { UpdateControlPageSkeleton } from "./frameworks/controls/UpdateControlPage";
|
||||||
import { CreateFrameworkPageSkeleton } from "./frameworks/CreateFrameworkPage";
|
import { CreateFrameworkPageSkeleton } from "./frameworks/CreateFrameworkPage";
|
||||||
import { FrameworkListPageSkeleton } from "./frameworks/FrameworkListPage";
|
|
||||||
import { FrameworkOverviewPageSkeleton } from "./frameworks/FrameworkOverviewPage";
|
|
||||||
import { UpdateFrameworkPageSkeleton } from "./frameworks/UpdateFrameworkPage";
|
import { UpdateFrameworkPageSkeleton } from "./frameworks/UpdateFrameworkPage";
|
||||||
import "./HomePage";
|
import "./HomePage";
|
||||||
import { CreatePeoplePageSkeleton } from "./people/CreatePeoplePage";
|
import { CreatePeoplePageSkeleton } from "./people/CreatePeoplePage";
|
||||||
@@ -17,11 +15,13 @@ import { PolicyListPageSkeleton } from "./policies/PolicyListPage";
|
|||||||
import { PolicyOverviewPageSkeleton } from "./policies/PolicyOverviewPage";
|
import { PolicyOverviewPageSkeleton } from "./policies/PolicyOverviewPage";
|
||||||
import { UpdatePolicyPageSkeleton } from "./policies/UpdatePolicyPage";
|
import { UpdatePolicyPageSkeleton } from "./policies/UpdatePolicyPage";
|
||||||
import { SettingsPageSkeleton } from "./SettingsPage";
|
import { SettingsPageSkeleton } from "./SettingsPage";
|
||||||
import { VendorListPageSkeleton } from "./vendors/VendorListPage";
|
import { VendorListPage } from "./vendors/VendorListPage";
|
||||||
import { VendorOverviewPageSkeleton } from "./vendors/VendorOverviewPage";
|
import { VendorOverviewPageSkeleton } from "./vendors/VendorOverviewPage";
|
||||||
import { ErrorBoundaryWithLocation } from "./ErrorBoundary";
|
import { ErrorBoundaryWithLocation } from "./ErrorBoundary";
|
||||||
import OrganizationLayout from "./OrganizationLayout";
|
import OrganizationLayout from "./OrganizationLayout";
|
||||||
import NoOrganizationLayout from "./NoOrganizationLayout";
|
import NoOrganizationLayout from "./NoOrganizationLayout";
|
||||||
|
import { FrameworkListPage } from "./frameworks/FrameworkListPage";
|
||||||
|
import { FrameworkPage } from "./frameworks/FrameworkPage";
|
||||||
|
|
||||||
const CreateOrganizationPage = lazy(() => import("./CreateOrganizationPage"));
|
const CreateOrganizationPage = lazy(() => import("./CreateOrganizationPage"));
|
||||||
const ControlOverviewPage = lazy(
|
const ControlOverviewPage = lazy(
|
||||||
@@ -36,10 +36,6 @@ const UpdateControlPage = lazy(
|
|||||||
const CreateFrameworkPage = lazy(
|
const CreateFrameworkPage = lazy(
|
||||||
() => import("./frameworks/CreateFrameworkPage")
|
() => import("./frameworks/CreateFrameworkPage")
|
||||||
);
|
);
|
||||||
const FrameworkListPage = lazy(() => import("./frameworks/FrameworkListPage"));
|
|
||||||
const FrameworkOverviewPage = lazy(
|
|
||||||
() => import("./frameworks/FrameworkOverviewPage")
|
|
||||||
);
|
|
||||||
const UpdateFrameworkPage = lazy(
|
const UpdateFrameworkPage = lazy(
|
||||||
() => import("./frameworks/UpdateFrameworkPage")
|
() => import("./frameworks/UpdateFrameworkPage")
|
||||||
);
|
);
|
||||||
@@ -52,7 +48,7 @@ const PolicyListPage = lazy(() => import("./policies/PolicyListPage"));
|
|||||||
const PolicyOverviewPage = lazy(() => import("./policies/PolicyOverviewPage"));
|
const PolicyOverviewPage = lazy(() => import("./policies/PolicyOverviewPage"));
|
||||||
const UpdatePolicyPage = lazy(() => import("./policies/UpdatePolicyPage"));
|
const UpdatePolicyPage = lazy(() => import("./policies/UpdatePolicyPage"));
|
||||||
const SettingsPage = lazy(() => import("./SettingsPage"));
|
const SettingsPage = lazy(() => import("./SettingsPage"));
|
||||||
const VendorListPage = lazy(() => import("./vendors/VendorListPage"));
|
|
||||||
const VendorOverviewPage = lazy(() => import("./vendors/VendorOverviewPage"));
|
const VendorOverviewPage = lazy(() => import("./vendors/VendorOverviewPage"));
|
||||||
|
|
||||||
export function OrganizationsRoutes() {
|
export function OrganizationsRoutes() {
|
||||||
@@ -99,26 +95,8 @@ export function OrganizationsRoutes() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path="vendors" element={<VendorListPage />} />
|
||||||
path="vendors"
|
<Route path="frameworks" element={<FrameworkListPage />} />
|
||||||
element={
|
|
||||||
<Suspense fallback={<VendorListPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<VendorListPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="frameworks"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<FrameworkListPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="frameworks/create"
|
path="frameworks/create"
|
||||||
element={
|
element={
|
||||||
@@ -129,16 +107,7 @@ export function OrganizationsRoutes() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path="frameworks/:frameworkId" element={<FrameworkPage />} />
|
||||||
path="frameworks/:frameworkId"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<FrameworkOverviewPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<FrameworkOverviewPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/update"
|
path="frameworks/:frameworkId/update"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,285 +1,12 @@
|
|||||||
import { Suspense, useEffect, useRef, useState } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import {
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
graphql,
|
import { useLocation } from "react-router";
|
||||||
PreloadedQuery,
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
usePreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
useMutation,
|
|
||||||
ConnectionHandler,
|
|
||||||
} from "react-relay";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
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`
|
const FrameworkListView = lazy(() => import("./FrameworkListView"));
|
||||||
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 FrameworkListPageImportFrameworkMutation = graphql`
|
export function FrameworkListSkeleton() {
|
||||||
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() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Frameworks"
|
title="Frameworks"
|
||||||
@@ -309,24 +36,14 @@ export function FrameworkListPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FrameworkListPage() {
|
export function FrameworkListPage() {
|
||||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkListPageQueryType>(
|
const location = useLocation();
|
||||||
FrameworkListPageQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadQuery({ organizationId: organizationId! });
|
|
||||||
}, [loadQuery, organizationId]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <FrameworkListPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<FrameworkListPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<FrameworkListSkeleton />}>
|
||||||
{queryRef && <FrameworkListPageContent queryRef={queryRef} />}
|
<ErrorBoundaryWithLocation>
|
||||||
|
<FrameworkListView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</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 { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import type { FrameworkOverviewPageQuery as FrameworkOverviewPageQueryType } from "./__generated__/FrameworkOverviewPageQuery.graphql";
|
import type { FrameworkViewQuery as FrameworkViewQueryType } from "./__generated__/FrameworkViewQuery.graphql";
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { FrameworkViewSkeleton } from "./FrameworkPage";
|
||||||
|
|
||||||
const FrameworkOverviewPageQuery = graphql`
|
const FrameworkViewQuery = graphql`
|
||||||
query FrameworkOverviewPageQuery($frameworkId: ID!) {
|
query FrameworkViewQuery($frameworkId: ID!) {
|
||||||
node(id: $frameworkId) {
|
node(id: $frameworkId) {
|
||||||
id
|
id
|
||||||
... on Framework {
|
... on Framework {
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
controls(first: 100)
|
controls(first: 100) @connection(key: "FrameworkView_controls") {
|
||||||
@connection(key: "FrameworkOverviewPage_controls") {
|
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@@ -66,12 +66,12 @@ interface Category {
|
|||||||
controls: Control[];
|
controls: Control[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function FrameworkOverviewPageContent({
|
function FrameworkViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
queryRef: PreloadedQuery<FrameworkOverviewPageQueryType>;
|
queryRef: PreloadedQuery<FrameworkViewQueryType>;
|
||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery(FrameworkOverviewPageQuery, queryRef);
|
const data = usePreloadedQuery(FrameworkViewQuery, queryRef);
|
||||||
const framework = data.node;
|
const framework = data.node;
|
||||||
const controls = framework.controls?.edges.map((edge) => edge?.node) ?? [];
|
const controls = framework.controls?.edges.map((edge) => edge?.node) ?? [];
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -326,44 +326,22 @@ function FrameworkOverviewPageContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FrameworkOverviewPageSkeleton() {
|
export default function FrameworkView() {
|
||||||
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() {
|
|
||||||
const { frameworkId } = useParams();
|
const { frameworkId } = useParams();
|
||||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkOverviewPageQueryType>(
|
const [queryRef, loadQuery] =
|
||||||
FrameworkOverviewPageQuery
|
useQueryLoader<FrameworkViewQueryType>(FrameworkViewQuery);
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuery({ frameworkId: frameworkId! });
|
loadQuery({ frameworkId: frameworkId! });
|
||||||
}, [loadQuery, frameworkId]);
|
}, [loadQuery, frameworkId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <FrameworkOverviewPageSkeleton />;
|
return <FrameworkViewSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<FrameworkOverviewPageSkeleton />}>
|
<Suspense fallback={<FrameworkViewSkeleton />}>
|
||||||
{queryRef && <FrameworkOverviewPageContent queryRef={queryRef} />}
|
{queryRef && <FrameworkViewContent queryRef={queryRef} />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<f8b972f4147ecb52da49f44ef9c62df1>>
|
* @generated SignedSource<<0d36f3f459adaca2f0abab59d9208bf9>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,11 +14,11 @@ export type ImportFrameworkInput = {
|
|||||||
file: any;
|
file: any;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type FrameworkListPageImportFrameworkMutation$variables = {
|
export type FrameworkListViewImportFrameworkMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: ImportFrameworkInput;
|
input: ImportFrameworkInput;
|
||||||
};
|
};
|
||||||
export type FrameworkListPageImportFrameworkMutation$data = {
|
export type FrameworkListViewImportFrameworkMutation$data = {
|
||||||
readonly importFramework: {
|
readonly importFramework: {
|
||||||
readonly frameworkEdge: {
|
readonly frameworkEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -39,9 +39,9 @@ export type FrameworkListPageImportFrameworkMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type FrameworkListPageImportFrameworkMutation = {
|
export type FrameworkListViewImportFrameworkMutation = {
|
||||||
response: FrameworkListPageImportFrameworkMutation$data;
|
response: FrameworkListViewImportFrameworkMutation$data;
|
||||||
variables: FrameworkListPageImportFrameworkMutation$variables;
|
variables: FrameworkListViewImportFrameworkMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -175,7 +175,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "FrameworkListPageImportFrameworkMutation",
|
"name": "FrameworkListViewImportFrameworkMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -200,7 +200,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "FrameworkListPageImportFrameworkMutation",
|
"name": "FrameworkListViewImportFrameworkMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -233,16 +233,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "946e840d5f7bb3a33e7ee1d0e2180bb5",
|
"cacheID": "b225cace8b9956333d0175a7e0a4c04d",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "FrameworkListPageImportFrameworkMutation",
|
"name": "FrameworkListViewImportFrameworkMutation",
|
||||||
"operationKind": "mutation",
|
"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;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<25802163c3ca6cc24524ebbc1388cd95>>
|
* @generated SignedSource<<c2ec2b3948d67ed6b924494adcae63aa>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
export type FrameworkListPageQuery$variables = {
|
export type FrameworkListViewQuery$variables = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type FrameworkListPageQuery$data = {
|
export type FrameworkListViewQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly frameworks?: {
|
readonly frameworks?: {
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
@@ -36,9 +36,9 @@ export type FrameworkListPageQuery$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type FrameworkListPageQuery = {
|
export type FrameworkListViewQuery = {
|
||||||
response: FrameworkListPageQuery$data;
|
response: FrameworkListViewQuery$data;
|
||||||
variables: FrameworkListPageQuery$variables;
|
variables: FrameworkListViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -209,7 +209,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "FrameworkListPageQuery",
|
"name": "FrameworkListViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -227,7 +227,7 @@ return {
|
|||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "FrameworkConnection",
|
"concreteType": "FrameworkConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__FrameworkListPage_frameworks_connection",
|
"name": "__FrameworkListView_frameworks_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v5/*: any*/),
|
"selections": (v5/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -247,7 +247,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "FrameworkListPageQuery",
|
"name": "FrameworkListViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -276,7 +276,7 @@ return {
|
|||||||
"args": (v3/*: any*/),
|
"args": (v3/*: any*/),
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "FrameworkListPage_frameworks",
|
"key": "FrameworkListView_frameworks",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "frameworks"
|
"name": "frameworks"
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "b55da571816341f2576684823b6d67d0",
|
"cacheID": "88bb77e2d10398380353d5a90ba5d6d6",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -306,13 +306,13 @@ return {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "FrameworkListPageQuery",
|
"name": "FrameworkListViewQuery",
|
||||||
"operationKind": "query",
|
"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;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<d673a8265dbda865ed008ca8d8d91c38>>
|
* @generated SignedSource<<c36de2ad1593c2dfefe28d7d3bbbddba>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
export type FrameworkOverviewPageQuery$variables = {
|
export type FrameworkViewQuery$variables = {
|
||||||
frameworkId: string;
|
frameworkId: string;
|
||||||
};
|
};
|
||||||
export type FrameworkOverviewPageQuery$data = {
|
export type FrameworkViewQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly controls?: {
|
readonly controls?: {
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
@@ -33,9 +33,9 @@ export type FrameworkOverviewPageQuery$data = {
|
|||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type FrameworkOverviewPageQuery = {
|
export type FrameworkViewQuery = {
|
||||||
response: FrameworkOverviewPageQuery$data;
|
response: FrameworkViewQuery$data;
|
||||||
variables: FrameworkOverviewPageQuery$variables;
|
variables: FrameworkViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -174,7 +174,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "FrameworkOverviewPageQuery",
|
"name": "FrameworkViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -195,7 +195,7 @@ return {
|
|||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "ControlConnection",
|
"concreteType": "ControlConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__FrameworkOverviewPage_controls_connection",
|
"name": "__FrameworkView_controls_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v6/*: any*/),
|
"selections": (v6/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -215,7 +215,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "FrameworkOverviewPageQuery",
|
"name": "FrameworkViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -247,7 +247,7 @@ return {
|
|||||||
"args": (v7/*: any*/),
|
"args": (v7/*: any*/),
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "FrameworkOverviewPage_controls",
|
"key": "FrameworkView_controls",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "controls"
|
"name": "controls"
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "d1033e296ed81f7dd74c61e04dbc7d1f",
|
"cacheID": "f8edefde219c5fa1a5c7c85e287d846a",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -276,13 +276,13 @@ return {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "FrameworkOverviewPageQuery",
|
"name": "FrameworkViewQuery",
|
||||||
"operationKind": "query",
|
"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;
|
export default node;
|
||||||
@@ -1,462 +1,11 @@
|
|||||||
import { Suspense, useEffect, useState, useTransition } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import {
|
import { lazy, Suspense } from "react";
|
||||||
graphql,
|
import { useLocation } from "react-router";
|
||||||
PreloadedQuery,
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
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";
|
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 25;
|
const VendorListView = lazy(() => import("./VendorListView"));
|
||||||
|
|
||||||
const vendorListPageQuery = graphql`
|
export function VendorListSkeleton() {
|
||||||
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() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Vendors"
|
title="Vendors"
|
||||||
@@ -483,33 +32,14 @@ export function VendorListPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VendorListPage() {
|
export function VendorListPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const location = useLocation();
|
||||||
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 />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<VendorListPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<VendorListSkeleton />}>
|
||||||
<VendorListContent queryRef={queryRef} />
|
<ErrorBoundaryWithLocation>
|
||||||
|
<VendorListView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</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