From 7afc596f3f6547df13425b89dafb1006c8adeddb Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 12 Dec 2022 16:51:17 +0000 Subject: [PATCH] API connection is getting created, just need to update with external id now --- .vscode/launch.json | 22 +++ .../app/assets/images/integrations/github.png | Bin 0 -> 6393 bytes .../navigation/OrganizationMenu.tsx | 16 +- apps/webapp/app/env.server.ts | 1 + apps/webapp/app/hooks/useOrganizations.ts | 10 ++ .../webapp/app/models/apiConnection.server.ts | 26 +++ .../orgs/$organizationSlug/integrations.tsx | 35 ++-- .../app/routes/resources/connection.tsx | 159 ++++++++++++++++++ .../migration.sql | 20 +++ .../migration.sql | 2 + apps/webapp/prisma/schema.prisma | 26 +++ turbo.json | 3 +- 12 files changed, 288 insertions(+), 32 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 apps/webapp/app/assets/images/integrations/github.png create mode 100644 apps/webapp/app/models/apiConnection.server.ts create mode 100644 apps/webapp/app/routes/resources/connection.tsx create mode 100644 apps/webapp/prisma/migrations/20221212112045_api_connections/migration.sql create mode 100644 apps/webapp/prisma/migrations/20221212134846_api_optional_external_id/migration.sql diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..5578de126 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "command": "pnpm run dev --filter webapp", + "name": "Run webapp", + "request": "launch", + "type": "node-terminal", + "cwd": "${workspaceFolder}" + }, + { + "type": "chrome", + "request": "launch", + "name": "Chrome webapp", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}/apps/webapp/app" + } + ] +} diff --git a/apps/webapp/app/assets/images/integrations/github.png b/apps/webapp/app/assets/images/integrations/github.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76 GIT binary patch literal 6393 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 Hu0mjfZ$b4` literal 0 HcmV?d00001 diff --git a/apps/webapp/app/components/navigation/OrganizationMenu.tsx b/apps/webapp/app/components/navigation/OrganizationMenu.tsx index d2ebff757..4d63bfa1d 100644 --- a/apps/webapp/app/components/navigation/OrganizationMenu.tsx +++ b/apps/webapp/app/components/navigation/OrganizationMenu.tsx @@ -1,17 +1,15 @@ -import { Organization } from ".prisma/client"; import { Popover, Transition } from "@headlessui/react"; import { BookmarkIcon, - BriefcaseIcon, CheckIcon, ChevronDownIcon, PlusIcon, } from "@heroicons/react/24/solid"; import { Link } from "@remix-run/react"; import classNames from "classnames"; -import React, { Fragment } from "react"; +import { Fragment } from "react"; import { - useCurrentOrganizationSlug, + useCurrentOrganization, useOrganizations, } from "~/hooks/useOrganizations"; @@ -19,11 +17,7 @@ const actionClassNames = "text-green-500"; export function OrganizationMenu() { const organizations = useOrganizations(); - const currentOrganizationSlug = useCurrentOrganizationSlug(); - - const currentOrganization = organizations?.find( - (org) => org.slug === currentOrganizationSlug - ); + const currentOrganization = useCurrentOrganization(); if (organizations === undefined) { return null; @@ -74,7 +68,7 @@ export function OrganizationMenu() { to={`/orgs/${organization.slug}`} className={classNames( "flex items-center justify-between gap-1.5 mx-1 px-3 py-2 text-slate-600 rounded hover:bg-slate-100 transition", - organization.slug === currentOrganizationSlug && + organization.slug === currentOrganization?.slug && "!bg-slate-200" )} > @@ -87,7 +81,7 @@ export function OrganizationMenu() { {organization.title} - {organization.slug === currentOrganizationSlug && ( + {organization.slug === currentOrganization?.slug && ( )} diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index e88fe8f13..5deecccff 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -24,6 +24,7 @@ const EnvironmentSchema = z.object({ PRIMARY_REGION: z.string().optional(), FLY_REGION: z.string().optional(), SESSION_SECRET: z.string(), + PIZZLY_HOST: z.string(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/hooks/useOrganizations.ts b/apps/webapp/app/hooks/useOrganizations.ts index c7918c758..63d55fb82 100644 --- a/apps/webapp/app/hooks/useOrganizations.ts +++ b/apps/webapp/app/hooks/useOrganizations.ts @@ -27,3 +27,13 @@ export function useCurrentOrganizationSlug(): string | undefined { const routeMatch = useMatchesData("routes/__app/orgs/$organizationSlug"); return routeMatch?.params?.organizationSlug; } + +export function useCurrentOrganization(): Organization | undefined { + const organizations = useOrganizations(); + const currentOrganizationSlug = useCurrentOrganizationSlug(); + + const currentOrganization = organizations?.find( + (org) => org.slug === currentOrganizationSlug + ); + return currentOrganization; +} diff --git a/apps/webapp/app/models/apiConnection.server.ts b/apps/webapp/app/models/apiConnection.server.ts new file mode 100644 index 000000000..f8c0262ae --- /dev/null +++ b/apps/webapp/app/models/apiConnection.server.ts @@ -0,0 +1,26 @@ +import type { APIConnection, Organization } from ".prisma/client"; +import { prisma } from "~/db.server"; + +export async function createAPIConnection({ + organizationId, + title, + apiIdentifier, + scopes, + type, +}: Pick & { + organizationId: Organization["id"]; +}) { + return await prisma.aPIConnection.create({ + data: { + title, + apiIdentifier, + type, + scopes, + organization: { + connect: { + id: organizationId, + }, + }, + }, + }); +} diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/integrations.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/integrations.tsx index d6ca211b4..529dbd434 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/integrations.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/integrations.tsx @@ -1,29 +1,24 @@ -import { useCallback } from "react"; -const Pizzly = require("@nangohq/pizzly-frontend"); +import invariant from "tiny-invariant"; +import { useCurrentOrganization } from "~/hooks/useOrganizations"; +import { Connect, integrations } from "~/routes/resources/connection"; export default function Integrations() { - const authenticateWithGitHub = useCallback(async () => { - const pizzly = new Pizzly("http://localhost:3004"); - pizzly - .auth("github", "test-connection") - .then((result: any) => { - console.log( - `OAuth flow succeeded for provider "${result.providerConfigKey}" and connection-id "${result.connectionId}"!` - ); - }) - .catch((error: any) => { - console.error( - `There was an error in the OAuth flow for integration "${error.providerConfigKey}" and connection-id "${error.connectionId}": ${error.error.type} - ${error.error.message}` - ); - }); - }, []); + const organization = useCurrentOrganization(); + invariant(organization, "Organization not found"); return (

Integrations

- + +
+ {integrations.map((integration) => ( + + ))} +
); } diff --git a/apps/webapp/app/routes/resources/connection.tsx b/apps/webapp/app/routes/resources/connection.tsx new file mode 100644 index 000000000..0ad5682f2 --- /dev/null +++ b/apps/webapp/app/routes/resources/connection.tsx @@ -0,0 +1,159 @@ +import githubLogo from "../../assets/images/integrations/github.png"; +import { useCallback, useEffect } from "react"; +import Pizzly from "@nangohq/pizzly-frontend"; +import { ActionArgs, json } from "@remix-run/server-runtime"; +import { requireUserId } from "~/services/session.server"; +import { env } from "~/env.server"; +import { z } from "zod"; +import { createAPIConnection } from "~/models/apiConnection.server"; +import { APIConnectionType } from ".prisma/client"; +import { useFetcher } from "@remix-run/react"; + +type Integration = { + key: string; + name: string; + logo: string; +}; + +export const integrations: Integration[] = [ + { + key: "github", + name: "GitHub", + logo: githubLogo, + }, +]; + +const createSchema = z.object({ + type: z.literal("create"), + organizationId: z.string(), + key: z.string(), +}); + +const updateSchema = z.object({ + type: z.literal("update"), + connectionId: z.string(), + externalId: z.number(), +}); +const requestSchema = z.discriminatedUnion("type", [ + createSchema, + updateSchema, +]); + +type CreateResponse = { + host: string; + integrationKey: string; + connectionId: string; +}; + +export const action = async ({ request, params }: ActionArgs) => { + const userId = await requireUserId(request); + if (userId === null) { + throw new Response("Unauthorized", { status: 401 }); + } + + if (request.method !== "POST") { + throw new Response("Method Not Allowed", { status: 405 }); + } + + try { + const formData = await request.formData(); + const body = Object.fromEntries(formData.entries()); + const parsed = requestSchema.parse(body); + + switch (parsed.type) { + case "create": { + const { organizationId, key } = parsed; + + const integrationInfo = integrations.find((i) => i.key === key); + if (!integrationInfo) { + throw new Error("Integration not found"); + } + + const connection = await createAPIConnection({ + organizationId, + title: integrationInfo.name, + apiIdentifier: key, + scopes: [], + type: APIConnectionType.HTTP, + }); + + const response: CreateResponse = { + host: env.PIZZLY_HOST, + integrationKey: key, + connectionId: connection.id, + }; + + return json(response); + } + case "update": { + const { connectionId, externalId } = parsed; + return {}; + } + } + } catch (error: any) { + return json({ message: error.message }, { status: 400 }); + } +}; + +export function Connect({ + integration, + organizationId, +}: { + integration: Integration; + organizationId: string; +}) { + const { createFetcher, status } = useCreateConnection(); + + return ( + + + + + + + ); +} + +type Status = "loading" | "idle"; + +export function useCreateConnection() { + const createConnectionFetcher = useFetcher(); + const status: Status = + createConnectionFetcher.state === "loading" ? "loading" : "idle"; + + useEffect(() => { + async function authenticationFlow() { + if (createConnectionFetcher.data === undefined) return; + + try { + const pizzly = new Pizzly(createConnectionFetcher.data.host); + + const result = await pizzly.auth( + createConnectionFetcher.data.integrationKey, + createConnectionFetcher.data.connectionId + ); + console.log( + `OAuth flow succeeded for provider "${result.providerConfigKey}" and connection-id "${result.connectionId}"!` + ); + } catch (error: any) { + console.error( + `There was an error in the OAuth flow for integration "${error.providerConfigKey}" and connection-id "${error.connectionId}": ${error.error.type} - ${error.error.message}` + ); + } + } + + authenticationFlow(); + }, [createConnectionFetcher.data]); + + return { + createFetcher: createConnectionFetcher, + status, + }; +} diff --git a/apps/webapp/prisma/migrations/20221212112045_api_connections/migration.sql b/apps/webapp/prisma/migrations/20221212112045_api_connections/migration.sql new file mode 100644 index 000000000..058a66c44 --- /dev/null +++ b/apps/webapp/prisma/migrations/20221212112045_api_connections/migration.sql @@ -0,0 +1,20 @@ +-- CreateEnum +CREATE TYPE "APIConnectionType" AS ENUM ('HTTP', 'GRAPHQL'); + +-- CreateTable +CREATE TABLE "APIConnection" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "apiIdentifier" TEXT NOT NULL, + "externalId" TEXT NOT NULL, + "scopes" TEXT[], + "type" "APIConnectionType" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "APIConnection_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "APIConnection" ADD CONSTRAINT "APIConnection_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/webapp/prisma/migrations/20221212134846_api_optional_external_id/migration.sql b/apps/webapp/prisma/migrations/20221212134846_api_optional_external_id/migration.sql new file mode 100644 index 000000000..6eb12f42c --- /dev/null +++ b/apps/webapp/prisma/migrations/20221212134846_api_optional_external_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "APIConnection" ALTER COLUMN "externalId" DROP NOT NULL; diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma index f4845b483..1c8b9c190 100644 --- a/apps/webapp/prisma/schema.prisma +++ b/apps/webapp/prisma/schema.prisma @@ -46,6 +46,28 @@ model Organization { users User[] workflows Workflow[] environments RuntimeEnvironment[] + apiConnections APIConnection[] +} + +model APIConnection { + id String @id @default(cuid()) + title String + + apiIdentifier String + externalId String? + scopes String[] + type APIConnectionType + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String +} + +enum APIConnectionType { + HTTP + GRAPHQL } model RuntimeEnvironment { @@ -75,3 +97,7 @@ model Workflow { @@unique([organizationId, slug]) } + +//todo Workflows have connection slots, which are filled with connections (can be empty) +//todo Workflow has one trigger (can also have a slot with connection) +//todo WorkflowRuns belong to a workflow + environment \ No newline at end of file diff --git a/turbo.json b/turbo.json index 24f7c8166..c158c7aa2 100644 --- a/turbo.json +++ b/turbo.json @@ -92,6 +92,7 @@ "SENTRY_DSN", "MAILGUN_KEY", "FROM_EMAIL", - "MERGENT_KEY" + "MERGENT_KEY", + "PIZZLY_HOST" ] }