API connection is getting created, just need to update with external id now
This commit is contained in:
Vendored
+22
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -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}
|
||||
</span>
|
||||
</div>
|
||||
{organization.slug === currentOrganizationSlug && (
|
||||
{organization.slug === currentOrganization?.slug && (
|
||||
<CheckIcon className="h-5 w-5 text-blue-500" />
|
||||
)}
|
||||
</Popover.Button>
|
||||
|
||||
@@ -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<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<APIConnection, "title" | "apiIdentifier" | "type" | "scopes"> & {
|
||||
organizationId: Organization["id"];
|
||||
}) {
|
||||
return await prisma.aPIConnection.create({
|
||||
data: {
|
||||
title,
|
||||
apiIdentifier,
|
||||
type,
|
||||
scopes,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organizationId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<h1>Integrations</h1>
|
||||
<button onClick={() => authenticateWithGitHub()}>
|
||||
Connect to GitHub
|
||||
</button>
|
||||
|
||||
<div>
|
||||
{integrations.map((integration) => (
|
||||
<Connect
|
||||
key={integration.key}
|
||||
integration={integration}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<createFetcher.Form method="post" action="/resources/connection">
|
||||
<input type="hidden" name="type" value="create" />
|
||||
<input type="hidden" name="organizationId" value={organizationId} />
|
||||
<input type="hidden" name="key" value={integration.key} />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading"}
|
||||
className="border border-gray-500 rounded-md flex h-12 p-1 items-center disabled:opacity-50"
|
||||
>
|
||||
<img src={integration.logo} alt={integration.name} className="h-10" />
|
||||
<h1>Connect to {integration.name}</h1>
|
||||
</button>
|
||||
</createFetcher.Form>
|
||||
);
|
||||
}
|
||||
|
||||
type Status = "loading" | "idle";
|
||||
|
||||
export function useCreateConnection() {
|
||||
const createConnectionFetcher = useFetcher<CreateResponse>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "APIConnection" ALTER COLUMN "externalId" DROP NOT NULL;
|
||||
@@ -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
|
||||
+2
-1
@@ -92,6 +92,7 @@
|
||||
"SENTRY_DSN",
|
||||
"MAILGUN_KEY",
|
||||
"FROM_EMAIL",
|
||||
"MERGENT_KEY"
|
||||
"MERGENT_KEY",
|
||||
"PIZZLY_HOST"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user