diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index f7eef6263..dae146bc6 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -12,6 +12,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, @@ -20,6 +21,7 @@ import { INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, + INTEGRATION_RAILWAY_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, @@ -94,6 +96,11 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_RAILWAY: + apps = await getAppsRailway({ + accessToken + }); + break; case INTEGRATION_FLYIO: apps = await getAppsFlyio({ accessToken, @@ -319,6 +326,60 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { return apps; }; +/** + * Return list of projects for Railway integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Railway API + * @returns {Object[]} apps - names and ids of Railway services + * @returns {String} apps.name - name of Railway project + * @returns {String} apps.appId - id of Railway project + * +*/ +const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { + let apps: any[] = []; + try { + const query = ` + query GetProjects($userId: String, $teamId: String) { + projects(userId: $userId, teamId: $teamId) { + edges { + node { + id + name + } + } + } + } + `; + + const variables = { + // userId: '', // Replace with the desired user ID or remove if not needed + // teamId: '', // Replace with the desired team ID or remove if not needed + }; + + const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, { + query, + variables, + }, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + apps = edges.map((e: any) => ({ + name: e.node.name, + appId: e.node.id + })); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to get Railway services"); + } + + return apps; +} + /** * Return list of apps for Fly.io integration * @param {Object} obj diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 698846ea7..4cfa739e6 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -9,6 +9,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, @@ -35,6 +36,7 @@ export interface IIntegration { | 'github' | 'gitlab' | 'render' + | 'railway' | 'flyio' | 'circleci' | 'travisci'; @@ -98,6 +100,7 @@ const integrationSchema = new Schema( INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 43bef63ad..64c4f2d69 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -9,6 +9,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, @@ -17,7 +18,7 @@ import { export interface IIntegrationAuth { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager'; + integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager'; teamId: string; accountId: string; refreshCiphertext?: string; @@ -51,6 +52,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 1e2ed4ae9..3029b265f 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -15,6 +15,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, @@ -31,6 +32,7 @@ import { INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, + INTEGRATION_RAILWAY_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, @@ -96,6 +98,7 @@ export { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, @@ -112,6 +115,7 @@ export { INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, + INTEGRATION_RAILWAY_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 52bfcc614..3c25c79da 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -17,6 +17,7 @@ const INTEGRATION_NETLIFY = "netlify"; const INTEGRATION_GITHUB = "github"; const INTEGRATION_GITLAB = "gitlab"; const INTEGRATION_RENDER = "render"; +const INTEGRATION_RAILWAY = "railway"; const INTEGRATION_FLYIO = "flyio"; const INTEGRATION_CIRCLECI = "circleci"; const INTEGRATION_TRAVISCI = "travisci"; @@ -52,6 +53,7 @@ const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; const INTEGRATION_RENDER_API_URL = "https://api.render.com"; +const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2"; const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; @@ -104,6 +106,15 @@ const getIntegrationOptions = () => { clientId: '', docsLink: '' }, + { + name: 'Railway', + slug: 'railway', + image: 'Railway.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, { name: 'Fly.io', slug: 'flyio', @@ -192,6 +203,7 @@ export { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, @@ -208,6 +220,7 @@ export { INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, + INTEGRATION_RAILWAY_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, diff --git a/frontend/public/images/integrations/Railway.png b/frontend/public/images/integrations/Railway.png new file mode 100644 index 000000000..61d53cf62 Binary files /dev/null and b/frontend/public/images/integrations/Railway.png differ diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx index c3c625a79..f58d1580d 100644 --- a/frontend/src/pages/integrations/[id].tsx +++ b/frontend/src/pages/integrations/[id].tsx @@ -207,6 +207,10 @@ export default function Integrations() { case 'travisci': link = `${window.location.origin}/integrations/travisci/authorize`; break; + case 'railway': + console.log('handleUnauthorized Railway: ', integrationOption); + link = `${window.location.origin}/integrations/railway/authorize`; + break; default: break; } @@ -259,6 +263,9 @@ export default function Integrations() { case 'travisci': link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`; break; + case 'railway': + console.log('handleAuthorized Railway: ', integrationAuth); + break; default: break; } diff --git a/frontend/src/pages/integrations/railway/authorize.tsx b/frontend/src/pages/integrations/railway/authorize.tsx new file mode 100644 index 000000000..94d3f06bf --- /dev/null +++ b/frontend/src/pages/integrations/railway/authorize.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; + +import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps'; +import { + Button, + Card, + CardTitle, + FormControl, + Input, +} from '../../../components/v2'; +import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; + +export default function RailwayAuthorizeIntegrationPage() { + const router = useRouter(); + const [apiKey, setApiKey] = useState(''); + const [apiKeyErrorText, setApiKeyErrorText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(''); + if (apiKey.length === 0) { + setApiKeyErrorText('API Key cannot be blank'); + return; + } + + setIsLoading(true); + + const integrationAuth = await saveIntegrationAccessToken({ + workspaceId: localStorage.getItem('projectData.id'), + integration: 'railway', + accessId: null, + accessToken: apiKey + }); + + setIsLoading(false); + + router.push( + `/integrations/railway/create?integrationAuthId=${integrationAuth._id}` + ); + } catch (err) { + console.error(err); + } + } + + return ( +
+ + Railway Integration + + setApiKey(e.target.value)} + /> + + + +
+ ); +} + +RailwayAuthorizeIntegrationPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file diff --git a/frontend/src/pages/integrations/railway/create.tsx b/frontend/src/pages/integrations/railway/create.tsx new file mode 100644 index 000000000..11e948628 --- /dev/null +++ b/frontend/src/pages/integrations/railway/create.tsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import queryString from 'query-string'; + +import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps'; +import { + Button, + Card, + CardTitle, + FormControl, + Select, + SelectItem +} from '../../../components/v2'; +import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; +import { useGetWorkspaceById } from '../../../hooks/api/workspace'; + +export default function RailwayCreateIntegrationPage() { + const router = useRouter(); + + const [targetAppId, setTargetAppId] = useState(''); + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]); + const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); + const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + integrationAuthId: integrationAuthId as string ?? '' + }); + + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetAppId(integrationAuthApps[0].appId as string); + } else { + setTargetAppId('none'); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + setIsLoading(true); + + if (!integrationAuth?._id) return; + + // const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId))?.name; + + console.log('handleButtonClick'); + + setIsLoading(false); + } catch (err) { + console.error(err); + } + } + + console.log('integrationAuthApps', integrationAuthApps); + + return workspace && selectedSourceEnvironment && integrationAuthApps ? ( +
+ + Railway Integration + + + + + + + + +
+ ) :
+} + +RailwayCreateIntegrationPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); + diff --git a/frontend/src/pages/integrations/render/create.tsx b/frontend/src/pages/integrations/render/create.tsx index cc61eec3e..e5ee04e0a 100644 --- a/frontend/src/pages/integrations/render/create.tsx +++ b/frontend/src/pages/integrations/render/create.tsx @@ -11,7 +11,7 @@ import { Select, SelectItem } from '../../../components/v2'; -import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; +import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; import { useGetWorkspaceById } from '../../../hooks/api/workspace'; import createIntegration from "../../api/integrations/createIntegration";