Add support for service variables to Railway integration, add docs for Railway

This commit is contained in:
Tuan Dang
2023-04-09 15:00:40 +03:00
parent f6e23127ac
commit 710364e3a1
35 changed files with 307 additions and 16 deletions
@@ -249,7 +249,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
} }
/** /**
* Return list of available Railway environments for Railway project with * Return list of Railway environments for Railway project with
* id [appId] * id [appId]
* @param req * @param req
* @param res * @param res
@@ -315,6 +315,84 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R
}); });
} }
/**
* Return list of Railway services for Railway project with id
* [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayService {
node: {
id: string;
name: string;
}
}
interface Service {
name: string;
serviceId: string;
}
let services: Service[] = [];
const query = `
query project($id: String!) {
project(id: $id) {
createdAt
deletedAt
id
description
expiredAt
isPublic
isTempProject
isUpdatable
name
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
}
}
`;
if (appId && appId !== '') {
const variables = {
id: appId
}
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
services = edges.map((e: RailwayService) => ({
name: e.node.name,
serviceId: e.node.id
}));
}
return res.status(200).send({
services
});
}
/** /**
* Delete integration authorization with id [integrationAuthId] * Delete integration authorization with id [integrationAuthId]
* @param req * @param req
@@ -25,6 +25,8 @@ export const createIntegration = async (req: Request, res: Response) => {
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService,
targetServiceId,
owner, owner,
path, path,
region region
@@ -41,12 +43,14 @@ export const createIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService,
targetServiceId,
owner, owner,
path, path,
region, region,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId) integrationAuth: new Types.ObjectId(integrationAuthId)
}).save(); }).save();
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
+2 -4
View File
@@ -355,10 +355,7 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
} }
`; `;
const variables = { const variables = {};
// userId: '<USER_ID_GOES_HERE>', // Replace with the desired user ID or remove if not needed
// teamId: '<TEAM_ID_GOES_HERE>', // Replace with the desired team ID or remove if not needed
};
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, { const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query, query,
@@ -367,6 +364,7 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
headers: { headers: {
'Authorization': `Bearer ${accessToken}`, 'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
}, },
}); });
+3
View File
@@ -1179,6 +1179,7 @@ const syncSecretsRailway = async ({
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
const query = ` const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) { mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input) variableCollectionUpsert(input: $input)
@@ -1188,6 +1189,7 @@ const syncSecretsRailway = async ({
const input = { const input = {
projectId: integration.appId, projectId: integration.appId,
environmentId: integration.targetEnvironmentId, environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true, replace: true,
variables: secrets variables: secrets
}; };
@@ -1201,6 +1203,7 @@ const syncSecretsRailway = async ({
headers: { headers: {
'Authorization': `Bearer ${accessToken}`, 'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
}, },
}); });
+13 -1
View File
@@ -21,10 +21,12 @@ export interface IIntegration {
environment: string; environment: string;
isActive: boolean; isActive: boolean;
app: string; app: string;
appId: string;
owner: string; owner: string;
targetEnvironment: string; targetEnvironment: string;
targetEnvironmentId: string; targetEnvironmentId: string;
appId: string; targetService: string;
targetServiceId: string;
path: string; path: string;
region: string; region: string;
integration: integration:
@@ -78,6 +80,16 @@ const integrationSchema = new Schema<IIntegration>(
type: String, type: String,
default: null default: null
}, },
targetService: {
// railway-specific service
type: String,
default: null
},
targetServiceId: {
// railway-specific service
type: String,
default: null
},
owner: { owner: {
// github-specific repo owner-login // github-specific repo owner-login
type: String, type: String,
+2
View File
@@ -31,6 +31,8 @@ router.post( // new: add new integration for integration auth
body('sourceEnvironment').trim(), body('sourceEnvironment').trim(),
body('targetEnvironment').trim(), body('targetEnvironment').trim(),
body('targetEnvironmentId').trim(), body('targetEnvironmentId').trim(),
body('targetService').trim(),
body('targetServiceId').trim(),
body('owner').trim(), body('owner').trim(),
body('path').trim(), body('path').trim(),
body('region').trim(), body('region').trim(),
+14
View File
@@ -125,6 +125,20 @@ router.get(
integrationAuthController.getIntegrationAuthRailwayEnvironments integrationAuthController.getIntegrationAuthRailwayEnvironments
); );
router.get(
'/:integrationAuthId/railway/services',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthRailwayServices
);
router.delete( router.delete(
'/:integrationAuthId', '/:integrationAuthId',
requireAuth({ requireAuth({
Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+54
View File
@@ -0,0 +1,54 @@
---
title: "Railway"
description: "How to automatically sync secrets from Infisical into your Railway projects and services"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Enter your Railway API Token
Obtain a Railway API Token in your Railway [Account Settings > Tokens](https://railway.app/account/tokens).
![integrations railway dashboard](../../images/integrations-railway-dashboard.png)
![integrations railway token](../../images/integrations-railway-token.png)
<Note>
If this is your first time creating a Railway API token, then you'll be prompted to join
Railway's Private Boarding Beta program on the Railway Account Settings > Tokens page.
Note that Railway project tokens will not work for this integration since they don't work with
Railway's Public API.
</Note>
Press on the Railway tile and input your Railway API Key to grant Infisical access to your Railway account.
![integrations railway authorization](../../images/integrations-railway-authorization.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which Railway project and environment (and optionally service). Lastly, press create integration to start syncing secrets to Railway.
![integrations create railway](../../images/integrations-railway-create.png)
<Note>
Infisical integrates with both Railway's [shared variables](https://blog.railway.app/p/shared-variables-release) at the project environment level as well as service variables at the service level.
To sync secrets to a specific service in a project, you can select a service from the Railway Service dropdown; otherwise, leaving it empty will sync secrets to the shared variables of that project.
</Note>
![integrations railway](../../images/integrations-railway.png)
+1
View File
@@ -18,6 +18,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| [Vercel](/integrations/cloud/vercel) | Cloud | Available | | [Vercel](/integrations/cloud/vercel) | Cloud | Available |
| [Netlify](/integrations/cloud/netlify) | Cloud | Available | | [Netlify](/integrations/cloud/netlify) | Cloud | Available |
| [Render](/integrations/cloud/render) | Cloud | Available | | [Render](/integrations/cloud/render) | Cloud | Available |
| [Railway](/integrations/cloud/railway) | Cloud | Available |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available | | [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
+1
View File
@@ -149,6 +149,7 @@
"integrations/cloud/vercel", "integrations/cloud/vercel",
"integrations/cloud/netlify", "integrations/cloud/netlify",
"integrations/cloud/render", "integrations/cloud/render",
"integrations/cloud/railway",
"integrations/cloud/flyio", "integrations/cloud/flyio",
"integrations/cloud/azure-key-vault", "integrations/cloud/azure-key-vault",
"integrations/cicd/githubactions", "integrations/cicd/githubactions",
@@ -12,6 +12,7 @@ const integrationSlugNameMapping: Mapping = {
'github': 'GitHub', 'github': 'GitHub',
'gitlab': 'GitLab', 'gitlab': 'GitLab',
'render': 'Render', 'render': 'Render',
'railway': 'Railway',
'flyio': 'Fly.io', 'flyio': 'Fly.io',
'circleci': 'CircleCI', 'circleci': 'CircleCI',
'travisci': 'TravisCI' 'travisci': 'TravisCI'
@@ -45,6 +45,7 @@ type Props = {
handleDeleteIntegration: (args: { integration: Integration }) => void; handleDeleteIntegration: (args: { integration: Integration }) => void;
}; };
// TODO: refactor
const IntegrationTile = ({ const IntegrationTile = ({
integration, integration,
integrations, integrations,
@@ -55,7 +56,6 @@ const IntegrationTile = ({
handleDeleteIntegration handleDeleteIntegration
}: Props) => { }: Props) => {
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>( const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration?.environment) || { environments.find(({ slug }) => slug === integration?.environment) || {
name: '', name: '',
@@ -176,6 +176,21 @@ const IntegrationTile = ({
/> />
</div> </div>
); );
case 'railway':
return (
<div>
<div className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
<ListBox
data={
!integration.isActive
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
: null
}
isSelected={integration.targetEnvironment}
onChange={setIntegrationTargetEnvironment}
/>
</div>
);
default: default:
return <div />; return <div />;
} }
@@ -1,7 +1,7 @@
export { export {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams, useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches, useGetIntegrationAuthVercelBranches} from './queries';
useGetRailwayEnvironments
} from './queries';
@@ -6,6 +6,7 @@ import {
App, App,
Environment, Environment,
IntegrationAuth, IntegrationAuth,
Service,
Team Team
} from './types'; } from './types';
@@ -27,6 +28,13 @@ const integrationAuthKeys = {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => [{ integrationAuthId, appId }, 'integrationAuthRailwayEnvironments'] as const, }) => [{ integrationAuthId, appId }, 'integrationAuthRailwayEnvironments'] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, 'integrationAuthRailwayServices'] as const
} }
const fetchIntegrationAuthById = async (integrationAuthId: string) => { const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -86,6 +94,22 @@ const fetchIntegrationAuthRailwayEnvironments = async ({
return environments; return environments;
} }
const fetchIntegrationAuthRailwayServices = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
params: {
appId
}
});
return services;
}
export const useGetIntegrationAuthById = (integrationAuthId: string) => { export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -139,7 +163,7 @@ export const useGetIntegrationAuthVercelBranches = ({
}); });
} }
export const useGetRailwayEnvironments = ({ export const useGetIntegrationAuthRailwayEnvironments = ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
@@ -158,3 +182,23 @@ export const useGetRailwayEnvironments = ({
enabled: true enabled: true
}); });
} }
export const useGetIntegrationAuthRailwayServices = ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
integrationAuthId,
appId,
}),
queryFn: () => fetchIntegrationAuthRailwayServices({
integrationAuthId,
appId,
}),
enabled: true
});
}
@@ -20,4 +20,9 @@ export type Team = {
export type Environment = { export type Environment = {
name: string; name: string;
environmentId: string; environmentId: string;
}
export type Service = {
name: string;
serviceId: string;
} }
@@ -8,6 +8,8 @@ interface Props {
sourceEnvironment: string; sourceEnvironment: string;
targetEnvironment: string | null; targetEnvironment: string | null;
targetEnvironmentId: string | null; targetEnvironmentId: string | null;
targetService: string | null;
targetServiceId: string | null;
owner: string | null; owner: string | null;
path: string | null; path: string | null;
region: string | null; region: string | null;
@@ -26,6 +28,8 @@ const createIntegration = ({
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService,
targetServiceId,
owner, owner,
path, path,
region region
@@ -43,6 +47,8 @@ const createIntegration = ({
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService,
targetServiceId,
owner, owner,
path, path,
region region
@@ -99,6 +99,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path, path,
region: selectedAWSRegion region: selectedAWSRegion
@@ -98,6 +98,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: selectedAWSRegion region: selectedAWSRegion
@@ -63,6 +63,8 @@ export default function AzureKeyVaultCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null
@@ -61,6 +61,8 @@ export default function CircleCICreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null, region: null,
@@ -62,6 +62,8 @@ export default function FlyioCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null
@@ -65,6 +65,8 @@ export default function GitHubCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: targetApp.owner, owner: targetApp.owner,
path: null, path: null,
region: null region: null
@@ -90,6 +90,8 @@ export default function GitLabCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null
@@ -61,6 +61,8 @@ export default function HerokuCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null
@@ -70,6 +70,8 @@ export default function NetlifyCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null
@@ -14,7 +14,8 @@ import {
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetRailwayEnvironments useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices
} from '../../../hooks/api/integrationAuth'; } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace'; import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration"; import createIntegration from "../../api/integrations/createIntegration";
@@ -24,6 +25,8 @@ export default function RailwayCreateIntegrationPage() {
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironmentId, setTargetEnvironmentId] = useState(''); const [targetEnvironmentId, setTargetEnvironmentId] = useState('');
const [targetServiceId, setTargetServiceId] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -33,8 +36,11 @@ export default function RailwayCreateIntegrationPage() {
const { data: integrationAuthApps } = useGetIntegrationAuthApps({ const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? '' integrationAuthId: integrationAuthId as string ?? ''
}); });
const { data: targetEnvironments } = useGetIntegrationAuthRailwayEnvironments({
const { data: targetEnvironments } = useGetRailwayEnvironments({ integrationAuthId: integrationAuthId as string ?? '',
appId: targetAppId
});
const { data: targetServices } = useGetIntegrationAuthRailwayServices({
integrationAuthId: integrationAuthId as string ?? '', integrationAuthId: integrationAuthId as string ?? '',
appId: targetAppId appId: targetAppId
}); });
@@ -64,6 +70,12 @@ export default function RailwayCreateIntegrationPage() {
} }
} }
}, [targetEnvironments]); }, [targetEnvironments]);
const filteredServices = targetServices
?.concat({
name: '',
serviceId: ''
});
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
@@ -75,6 +87,8 @@ export default function RailwayCreateIntegrationPage() {
const targetEnvironment = targetEnvironments?.find((environment) => environment.environmentId === targetEnvironmentId); const targetEnvironment = targetEnvironments?.find((environment) => environment.environmentId === targetEnvironmentId);
if (!targetApp || !targetApp.appId || !targetEnvironment) return; if (!targetApp || !targetApp.appId || !targetEnvironment) return;
const targetService = targetServices?.find((service) => service.serviceId === targetServiceId);
await createIntegration({ await createIntegration({
integrationAuthId: integrationAuth?._id, integrationAuthId: integrationAuth?._id,
@@ -84,6 +98,8 @@ export default function RailwayCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment.name, targetEnvironment: targetEnvironment.name,
targetEnvironmentId: targetEnvironment.environmentId, targetEnvironmentId: targetEnvironment.environmentId,
targetService: targetService ? targetService.name : null,
targetServiceId: targetService ? targetService.serviceId : null,
owner: null, owner: null,
path: null, path: null,
region: null region: null
@@ -99,7 +115,7 @@ export default function RailwayCreateIntegrationPage() {
} }
} }
return workspace && selectedSourceEnvironment && integrationAuthApps && targetEnvironments ? ( return workspace && selectedSourceEnvironment && integrationAuthApps && targetEnvironments && filteredServices ? (
<div className="h-full w-full flex justify-center items-center"> <div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md"> <Card className="max-w-md p-8 rounded-md">
<CardTitle className="text-center">Railway Integration</CardTitle> <CardTitle className="text-center">Railway Integration</CardTitle>
@@ -158,6 +174,19 @@ export default function RailwayCreateIntegrationPage() {
)} )}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Railway Service (Optional)">
<Select
value={targetServiceId}
onValueChange={(val) => setTargetServiceId(val)}
className='w-full border border-mineshaft-500'
>
{filteredServices.map((targetService) => (
<SelectItem value={targetService.serviceId as string} key={`target-service-${targetService.serviceId as string}`}>
{targetService.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
@@ -61,6 +61,8 @@ export default function RenderCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null
@@ -61,6 +61,8 @@ export default function TravisCICreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null, region: null,
@@ -88,6 +88,8 @@ export default function VercelCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId: null, targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null, owner: null,
path, path,
region: null region: null