diff --git a/apps/console/package.json b/apps/console/package.json index 5db93eaac..c39fe059a 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -34,7 +34,8 @@ "tailwindcss-animate": "^1.0.7", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-radio-group": "^1.1.3" + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-select": "^2.0.0" }, "devDependencies": { "@babel/core": "^7.26.7", diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 2eb4f62a8..ab9418bbb 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -29,6 +29,7 @@ const FrameworkListPage = lazy(() => import("./pages/FrameworkListPage")); const FrameworkOverviewPage = lazy(() => import("./pages/FrameworkOverviewPage")); const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage")); const SettingsPage = lazy(() => import("./pages/SettingsPage")); +const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage")); function App() { return ( @@ -67,6 +68,16 @@ function App() { } /> + + + + + + } + /> , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + {children} + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectItem, +} \ No newline at end of file diff --git a/apps/console/src/pages/CreatePeoplePage.tsx b/apps/console/src/pages/CreatePeoplePage.tsx new file mode 100644 index 000000000..7818e609c --- /dev/null +++ b/apps/console/src/pages/CreatePeoplePage.tsx @@ -0,0 +1,180 @@ +import { Suspense, useEffect, useState } from "react"; +import { useNavigate } from "react-router"; +import { + graphql, + PreloadedQuery, + usePreloadedQuery, + useQueryLoader, + useMutation, + ConnectionHandler, + useRelayEnvironment, +} from "react-relay"; +import { Helmet } from "react-helmet-async"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { CreatePeoplePageQuery as CreatePeoplePageQueryType } from "./__generated__/CreatePeoplePageQuery.graphql"; + +const createPeoplePageQuery = graphql` + query CreatePeoplePageQuery { + node(id: "AZSfP_xAcAC5IAAAAAAltA") { + id + ... on Organization { + name + } + } + } +`; + +const createPeopleMutation = graphql` + mutation CreatePeoplePageCreatePeopleMutation($input: CreatePeopleInput!) { + createPeople(input: $input) { + id + fullName + primaryEmailAddress + kind + } + } +`; + +function CreatePeoplePageContent({ + queryRef, +}: { + queryRef: PreloadedQuery; +}) { + const navigate = useNavigate(); + const environment = useRelayEnvironment(); + const data = usePreloadedQuery(createPeoplePageQuery, queryRef); + const [createPeople, isCreatingPeople] = useMutation(createPeopleMutation); + const [kind, setKind] = useState<'EMPLOYEE' | 'CONTRACTOR' | 'VENDOR'>('EMPLOYEE'); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + createPeople({ + variables: { + input: { + organizationId: data.node.id, + fullName: formData.get('fullName') as string, + primaryEmailAddress: formData.get('primaryEmailAddress') as string, + kind, + }, + }, + onCompleted() { + // Invalidate the peoples list query + environment.commitUpdate((store) => { + const organization = store.get(data.node.id); + if (organization) { + organization.invalidateRecord(); + } + }); + + navigate('/peoples'); + }, + }); + }; + + return ( +
+ + Create People - Probo Console + + +
+
+

Create People

+

+ Add a new person to your organization. +

+
+ + + + People Information + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+ ); +} + +export default function CreatePeoplePage() { + const [queryRef, loadQuery] = useQueryLoader(createPeoplePageQuery); + + useEffect(() => { + loadQuery({}); + }, [loadQuery]); + + if (!queryRef) { + return null; + } + + return ( + + + + ); +} \ No newline at end of file diff --git a/apps/console/src/pages/PeopleListPage.tsx b/apps/console/src/pages/PeopleListPage.tsx index e4a829cfd..4c55a007a 100644 --- a/apps/console/src/pages/PeopleListPage.tsx +++ b/apps/console/src/pages/PeopleListPage.tsx @@ -11,6 +11,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { CircleUser, Globe, Shield } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Link } from "react-router"; import { Helmet } from "react-helmet-async"; import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.graphql"; @@ -21,13 +22,15 @@ const PeopleListPageQuery = graphql` node(id: "AZSfP_xAcAC5IAAAAAAltA") { id ... on Organization { - peoples(first: $first, after: $after, last: $last, before: $before) { + peoples(first: $first, after: $after, last: $last, before: $before) + @connection(key: "PeopleListPageQuery_peoples") { edges { node { id fullName primaryEmailAddress additionalEmailAddresses + kind createdAt updatedAt } @@ -85,10 +88,19 @@ function PeopleListPageContent({ return (
-

Peoples

-

- Keep track of your company's workforce and their progress towards completing tasks assigned to them. -

+
+
+

People

+

+ Manage your organization's people. +

+
+ +
{peoples.map((person) => ( @@ -107,7 +119,8 @@ function PeopleListPageContent({
- Admin + {person?.kind === 'EMPLOYEE' ? 'Employee' : + person?.kind === 'CONTRACTOR' ? 'Contractor' : 'Vendor'}
diff --git a/apps/console/src/pages/__generated__/CreatePeoplePageCreatePeopleMutation.graphql.ts b/apps/console/src/pages/__generated__/CreatePeoplePageCreatePeopleMutation.graphql.ts new file mode 100644 index 000000000..688e20aa0 --- /dev/null +++ b/apps/console/src/pages/__generated__/CreatePeoplePageCreatePeopleMutation.graphql.ts @@ -0,0 +1,120 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "VENDOR"; +export type CreatePeopleInput = { + fullName: string; + kind: PeopleKind; + organizationId: string; + primaryEmailAddress: string; +}; +export type CreatePeoplePageCreatePeopleMutation$variables = { + input: CreatePeopleInput; +}; +export type CreatePeoplePageCreatePeopleMutation$data = { + readonly createPeople: { + readonly fullName: string; + readonly id: string; + readonly kind: PeopleKind; + readonly primaryEmailAddress: string; + }; +}; +export type CreatePeoplePageCreatePeopleMutation = { + response: CreatePeoplePageCreatePeopleMutation$data; + variables: CreatePeoplePageCreatePeopleMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "People", + "kind": "LinkedField", + "name": "createPeople", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "CreatePeoplePageCreatePeopleMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "CreatePeoplePageCreatePeopleMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "6438f79d9a2e3d3e5cfcc98c4107e512", + "id": null, + "metadata": {}, + "name": "CreatePeoplePageCreatePeopleMutation", + "operationKind": "mutation", + "text": "mutation CreatePeoplePageCreatePeopleMutation(\n $input: CreatePeopleInput!\n) {\n createPeople(input: $input) {\n id\n fullName\n primaryEmailAddress\n kind\n }\n}\n" + } +}; +})(); + +(node as any).hash = "55f9379fac4d33ca3b2c21acf80094de"; + +export default node; diff --git a/apps/console/src/pages/__generated__/CreatePeoplePageQuery.graphql.ts b/apps/console/src/pages/__generated__/CreatePeoplePageQuery.graphql.ts new file mode 100644 index 000000000..441f132fd --- /dev/null +++ b/apps/console/src/pages/__generated__/CreatePeoplePageQuery.graphql.ts @@ -0,0 +1,118 @@ +/** + * @generated SignedSource<<1440fead35c1cf239edc5574dcdf9dc3>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type CreatePeoplePageQuery$variables = Record; +export type CreatePeoplePageQuery$data = { + readonly node: { + readonly id: string; + readonly name?: string; + }; +}; +export type CreatePeoplePageQuery = { + response: CreatePeoplePageQuery$data; + variables: CreatePeoplePageQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "id", + "value": "AZSfP_xAcAC5IAAAAAAltA" + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "CreatePeoplePageQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/) + ], + "storageKey": "node(id:\"AZSfP_xAcAC5IAAAAAAltA\")" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "CreatePeoplePageQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v1/*: any*/), + (v2/*: any*/) + ], + "storageKey": "node(id:\"AZSfP_xAcAC5IAAAAAAltA\")" + } + ] + }, + "params": { + "cacheID": "e3812048d135626b79aa289d77ecd630", + "id": null, + "metadata": {}, + "name": "CreatePeoplePageQuery", + "operationKind": "query", + "text": "query CreatePeoplePageQuery {\n node(id: \"AZSfP_xAcAC5IAAAAAAltA\") {\n __typename\n id\n ... on Organization {\n name\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "629fdb563382b22be4a6c1f17fafda96"; + +export default node; diff --git a/apps/console/src/pages/__generated__/PeopleListPageQuery.graphql.ts b/apps/console/src/pages/__generated__/PeopleListPageQuery.graphql.ts index 932d38d7e..7d5e7bd2a 100644 --- a/apps/console/src/pages/__generated__/PeopleListPageQuery.graphql.ts +++ b/apps/console/src/pages/__generated__/PeopleListPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<2b0f0d83fcebe25d4dfe1209c757c792>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,6 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "VENDOR"; export type PeopleListPageQuery$variables = { after?: any | null | undefined; before?: any | null | undefined; @@ -26,6 +27,7 @@ export type PeopleListPageQuery$data = { readonly createdAt: any; readonly fullName: string; readonly id: string; + readonly kind: PeopleKind; readonly primaryEmailAddress: string; readonly updatedAt: any; }; @@ -80,148 +82,148 @@ v5 = { "storageKey": null }, v6 = { - "kind": "InlineFragment", - "selections": [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "after", - "variableName": "after" - }, - { - "kind": "Variable", - "name": "before", - "variableName": "before" - }, - { - "kind": "Variable", - "name": "first", - "variableName": "first" - }, - { - "kind": "Variable", - "name": "last", - "variableName": "last" - } - ], - "concreteType": "PeopleConnection", - "kind": "LinkedField", - "name": "peoples", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PeopleEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "People", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v5/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fullName", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "primaryEmailAddress", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "additionalEmailAddresses", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "updatedAt", - "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 - } - ], - "storageKey": null - } - ], - "type": "Organization", - "abstractKey": null -}; + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v7 = [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v5/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + }, + (v6/*: 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 + } +], +v8 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "after" + }, + { + "kind": "Variable", + "name": "before", + "variableName": "before" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "first" + }, + { + "kind": "Variable", + "name": "last", + "variableName": "last" + } +]; return { "fragment": { "argumentDefinitions": [ @@ -243,7 +245,23 @@ return { "plural": false, "selections": [ (v5/*: any*/), - (v6/*: any*/) + { + "kind": "InlineFragment", + "selections": [ + { + "alias": "peoples", + "args": null, + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "__PeopleListPageQuery_peoples_connection", + "plural": false, + "selections": (v7/*: any*/), + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null + } ], "storageKey": "node(id:\"AZSfP_xAcAC5IAAAAAAltA\")" } @@ -270,31 +288,62 @@ return { "name": "node", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, + (v6/*: any*/), (v5/*: any*/), - (v6/*: any*/) + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v8/*: any*/), + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "peoples", + "plural": false, + "selections": (v7/*: any*/), + "storageKey": null + }, + { + "alias": null, + "args": (v8/*: any*/), + "filters": null, + "handle": "connection", + "key": "PeopleListPageQuery_peoples", + "kind": "LinkedHandle", + "name": "peoples" + } + ], + "type": "Organization", + "abstractKey": null + } ], "storageKey": "node(id:\"AZSfP_xAcAC5IAAAAAAltA\")" } ] }, "params": { - "cacheID": "0074149682b4e4a31aa559fe8431d567", + "cacheID": "480538011d1eccb9405f2d495417fc8d", "id": null, - "metadata": {}, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": [ + "node", + "peoples" + ] + } + ] + }, "name": "PeopleListPageQuery", "operationKind": "query", - "text": "query PeopleListPageQuery(\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n node(id: \"AZSfP_xAcAC5IAAAAAAltA\") {\n __typename\n id\n ... on Organization {\n peoples(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n createdAt\n updatedAt\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n}\n" + "text": "query PeopleListPageQuery(\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n node(id: \"AZSfP_xAcAC5IAAAAAAltA\") {\n __typename\n id\n ... on Organization {\n peoples(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "5ca962add045876503cb7290d20b9f8b"; +(node as any).hash = "cc8b466dc73007c5590ba2e8ffc6def3"; export default node; diff --git a/go.mod b/go.mod index 4950eb0bf..eedb36e75 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/99designs/gqlgen v0.17.63 github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/cors v1.2.1 + github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.2 github.com/prometheus/client_golang v1.20.5 github.com/vektah/gqlparser/v2 v2.5.21 @@ -25,7 +26,6 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect diff --git a/package-lock.json b/package-lock.json index c7cbcf6e0..98069c1f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", @@ -2255,6 +2256,12 @@ "resolved": "packages/tsconfig", "link": true }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -2844,6 +2851,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", + "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", diff --git a/pkg/api/console/v1/schema.graphql b/pkg/api/console/v1/schema.graphql index b10f03fab..dfa280729 100644 --- a/pkg/api/console/v1/schema.graphql +++ b/pkg/api/console/v1/schema.graphql @@ -31,6 +31,12 @@ enum EvidenceState { EXPIRED } +enum PeopleKind { + EMPLOYEE + CONTRACTOR + VENDOR +} + type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! @@ -83,6 +89,7 @@ type People implements Node { fullName: String! primaryEmailAddress: String! additionalEmailAddresses: [String!]! + kind: PeopleKind! createdAt: Datetime! updatedAt: Datetime! } @@ -291,6 +298,7 @@ type Mutation { createVendor(input: CreateVendorInput!): Vendor! deleteVendor(input: DeleteVendorInput!): Void! deletePeople(input: DeletePeopleInput!): Void! + createPeople(input: CreatePeopleInput!): People! } input CreateVendorInput { @@ -304,4 +312,11 @@ input DeleteVendorInput { input DeletePeopleInput { peopleId: ID! +} + +input CreatePeopleInput { + organizationId: ID! + fullName: String! + primaryEmailAddress: String! + kind: PeopleKind! } \ No newline at end of file diff --git a/pkg/api/console/v1/schema/schema.go b/pkg/api/console/v1/schema/schema.go index 26080004b..7028b85fe 100644 --- a/pkg/api/console/v1/schema/schema.go +++ b/pkg/api/console/v1/schema/schema.go @@ -154,6 +154,7 @@ type ComplexityRoot struct { } Mutation struct { + CreatePeople func(childComplexity int, input types.CreatePeopleInput) int CreateVendor func(childComplexity int, input types.CreateVendorInput) int DeletePeople func(childComplexity int, input types.DeletePeopleInput) int DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int @@ -182,6 +183,7 @@ type ComplexityRoot struct { CreatedAt func(childComplexity int) int FullName func(childComplexity int) int ID func(childComplexity int) int + Kind func(childComplexity int) int PrimaryEmailAddress func(childComplexity int) int UpdatedAt func(childComplexity int) int } @@ -272,6 +274,7 @@ type MutationResolver interface { CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.Vendor, error) DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (string, error) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (string, error) + CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.People, error) } type OrganizationResolver interface { Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) @@ -703,6 +706,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.FrameworkEdge.Node(childComplexity), true + case "Mutation.createPeople": + if e.complexity.Mutation.CreatePeople == nil { + break + } + + args, err := ec.field_Mutation_createPeople_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreatePeople(childComplexity, args["input"].(types.CreatePeopleInput)), true + case "Mutation.createVendor": if e.complexity.Mutation.CreateVendor == nil { break @@ -866,6 +881,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.People.ID(childComplexity), true + case "People.kind": + if e.complexity.People.Kind == nil { + break + } + + return e.complexity.People.Kind(childComplexity), true + case "People.primaryEmailAddress": if e.complexity.People.PrimaryEmailAddress == nil { break @@ -1148,6 +1170,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { opCtx := graphql.GetOperationContext(ctx) ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputCreatePeopleInput, ec.unmarshalInputCreateVendorInput, ec.unmarshalInputDeletePeopleInput, ec.unmarshalInputDeleteVendorInput, @@ -1281,6 +1304,12 @@ enum EvidenceState { EXPIRED } +enum PeopleKind { + EMPLOYEE + CONTRACTOR + VENDOR +} + type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! @@ -1333,6 +1362,7 @@ type People implements Node { fullName: String! primaryEmailAddress: String! additionalEmailAddresses: [String!]! + kind: PeopleKind! createdAt: Datetime! updatedAt: Datetime! } @@ -1541,6 +1571,7 @@ type Mutation { createVendor(input: CreateVendorInput!): Vendor! deleteVendor(input: DeleteVendorInput!): Void! deletePeople(input: DeletePeopleInput!): Void! + createPeople(input: CreatePeopleInput!): People! } input CreateVendorInput { @@ -1554,6 +1585,13 @@ input DeleteVendorInput { input DeletePeopleInput { peopleId: ID! +} + +input CreatePeopleInput { + organizationId: ID! + fullName: String! + primaryEmailAddress: String! + kind: PeopleKind! }`, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -1870,6 +1908,29 @@ func (ec *executionContext) field_Framework_controls_argsBefore( return zeroVal, nil } +func (ec *executionContext) field_Mutation_createPeople_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_createPeople_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_createPeople_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.CreatePeopleInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNCreatePeopleInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreatePeopleInput(ctx, tmp) + } + + var zeroVal types.CreatePeopleInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_createVendor_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -4805,6 +4866,65 @@ func (ec *executionContext) fieldContext_Mutation_deletePeople(ctx context.Conte return fc, nil } +func (ec *executionContext) _Mutation_createPeople(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createPeople(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreatePeople(rctx, fc.Args["input"].(types.CreatePeopleInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.People) + fc.Result = res + return ec.marshalNPeople2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createPeople(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_People_id(ctx, field) + case "fullName": + return ec.fieldContext_People_fullName(ctx, field) + case "primaryEmailAddress": + return ec.fieldContext_People_primaryEmailAddress(ctx, field) + case "additionalEmailAddresses": + return ec.fieldContext_People_additionalEmailAddresses(ctx, field) + case "kind": + return ec.fieldContext_People_kind(ctx, field) + case "createdAt": + return ec.fieldContext_People_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_People_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type People", field.Name) + }, + } + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createPeople_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Organization_id(ctx, field) if err != nil { @@ -5440,6 +5560,44 @@ func (ec *executionContext) fieldContext_People_additionalEmailAddresses(_ conte return fc, nil } +func (ec *executionContext) _People_kind(ctx context.Context, field graphql.CollectedField, obj *types.People) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_People_kind(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Kind, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(types.PeopleKind) + fc.Result = res + return ec.marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeopleKind(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_People_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "People", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type PeopleKind does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _People_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.People) (ret graphql.Marshaler) { fc, err := ec.fieldContext_People_createdAt(ctx, field) if err != nil { @@ -5687,6 +5845,8 @@ func (ec *executionContext) fieldContext_PeopleEdge_node(_ context.Context, fiel return ec.fieldContext_People_primaryEmailAddress(ctx, field) case "additionalEmailAddresses": return ec.fieldContext_People_additionalEmailAddresses(ctx, field) + case "kind": + return ec.fieldContext_People_kind(ctx, field) case "createdAt": return ec.fieldContext_People_createdAt(ctx, field) case "updatedAt": @@ -8649,6 +8809,54 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputCreatePeopleInput(ctx context.Context, obj any) (types.CreatePeopleInput, error) { + var it types.CreatePeopleInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"organizationId", "fullName", "primaryEmailAddress", "kind"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "organizationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OrganizationID = data + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.FullName = data + case "primaryEmailAddress": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("primaryEmailAddress")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.PrimaryEmailAddress = data + case "kind": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind")) + data, err := ec.unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeopleKind(ctx, v) + if err != nil { + return it, err + } + it.Kind = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputCreateVendorInput(ctx context.Context, obj any) (types.CreateVendorInput, error) { var it types.CreateVendorInput asMap := map[string]any{} @@ -9715,6 +9923,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createPeople": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createPeople(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9969,6 +10184,11 @@ func (ec *executionContext) _People(ctx context.Context, sel ast.SelectionSet, o if out.Values[i] == graphql.Null { out.Invalids++ } + case "kind": + out.Values[i] = ec._People_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createdAt": out.Values[i] = ec._People_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -11154,6 +11374,11 @@ func (ec *executionContext) marshalNControlStateTransitionEdge2ᚖgithubᚗcom return ec._ControlStateTransitionEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNCreatePeopleInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreatePeopleInput(ctx context.Context, v any) (types.CreatePeopleInput, error) { + res, err := ec.unmarshalInputCreatePeopleInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNCreateVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateVendorInput(ctx context.Context, v any) (types.CreateVendorInput, error) { res, err := ec.unmarshalInputCreateVendorInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -11475,6 +11700,10 @@ func (ec *executionContext) marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋprobo return ec._PageInfo(ctx, sel, v) } +func (ec *executionContext) marshalNPeople2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx context.Context, sel ast.SelectionSet, v types.People) graphql.Marshaler { + return ec._People(ctx, sel, &v) +} + func (ec *executionContext) marshalNPeople2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeople(ctx context.Context, sel ast.SelectionSet, v *types.People) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -11547,6 +11776,16 @@ func (ec *executionContext) marshalNPeopleEdge2ᚖgithubᚗcomᚋgetproboᚋprob return ec._PeopleEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeopleKind(ctx context.Context, v any) (types.PeopleKind, error) { + var res types.PeopleKind + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNPeopleKind2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPeopleKind(ctx context.Context, sel ast.SelectionSet, v types.PeopleKind) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/api/console/v1/types/people.go b/pkg/api/console/v1/types/people.go index e0fd30c72..480c6647e 100644 --- a/pkg/api/console/v1/types/people.go +++ b/pkg/api/console/v1/types/people.go @@ -45,6 +45,7 @@ func NewPeople(p *coredata.People) *People { FullName: p.FullName, PrimaryEmailAddress: p.PrimaryEmailAddress, AdditionalEmailAddresses: p.AdditionalEmailAddresses, + Kind: PeopleKind(p.Kind.String()), CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, } diff --git a/pkg/api/console/v1/types/types.go b/pkg/api/console/v1/types/types.go index e4d6601ed..ef42e7009 100644 --- a/pkg/api/console/v1/types/types.go +++ b/pkg/api/console/v1/types/types.go @@ -60,6 +60,13 @@ type ControlStateTransitionEdge struct { Node *ControlStateTransition `json:"node"` } +type CreatePeopleInput struct { + OrganizationID gid.GID `json:"organizationId"` + FullName string `json:"fullName"` + PrimaryEmailAddress string `json:"primaryEmailAddress"` + Kind PeopleKind `json:"kind"` +} + type CreateVendorInput struct { OrganizationID gid.GID `json:"organizationId"` Name string `json:"name"` @@ -163,12 +170,13 @@ type PageInfo struct { } type People struct { - ID gid.GID `json:"id"` - FullName string `json:"fullName"` - PrimaryEmailAddress string `json:"primaryEmailAddress"` - AdditionalEmailAddresses []string `json:"additionalEmailAddresses"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID gid.GID `json:"id"` + FullName string `json:"fullName"` + PrimaryEmailAddress string `json:"primaryEmailAddress"` + AdditionalEmailAddresses []string `json:"additionalEmailAddresses"` + Kind PeopleKind `json:"kind"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } func (People) IsNode() {} @@ -338,6 +346,49 @@ func (e EvidenceState) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } +type PeopleKind string + +const ( + PeopleKindEmployee PeopleKind = "EMPLOYEE" + PeopleKindContractor PeopleKind = "CONTRACTOR" + PeopleKindVendor PeopleKind = "VENDOR" +) + +var AllPeopleKind = []PeopleKind{ + PeopleKindEmployee, + PeopleKindContractor, + PeopleKindVendor, +} + +func (e PeopleKind) IsValid() bool { + switch e { + case PeopleKindEmployee, PeopleKindContractor, PeopleKindVendor: + return true + } + return false +} + +func (e PeopleKind) String() string { + return string(e) +} + +func (e *PeopleKind) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = PeopleKind(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid PeopleKind", str) + } + return nil +} + +func (e PeopleKind) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + type TaskState string const ( diff --git a/pkg/api/console/v1/v1_resolver.go b/pkg/api/console/v1/v1_resolver.go index 8dbefc869..a1178eafd 100644 --- a/pkg/api/console/v1/v1_resolver.go +++ b/pkg/api/console/v1/v1_resolver.go @@ -98,6 +98,23 @@ func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeleteP return "", nil } +// CreatePeople is the resolver for the createPeople field. +func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.People, error) { + people, err := r.svc.CreatePeople(ctx, probo.CreatePeopleRequest{ + OrganizationID: input.OrganizationID, + FullName: input.FullName, + PrimaryEmailAddress: input.PrimaryEmailAddress, + AdditionalEmailAddresses: []string{}, + Kind: coredata.PeopleKindEmployee, + }) + + if err != nil { + return nil, fmt.Errorf("cannot create people: %w", err) + } + + return types.NewPeople(people), nil +} + // Frameworks is the resolver for the frameworks field. func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) { cursor := types.NewCursor(first, after, last, before)