feat: support render environment groups (#4327)

* feat: support env groups in render sync

* fix: update doc

* Update backend/src/services/app-connection/render/render-connection-service.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: pr changes

* fix: lint and type check

* fix: changes

* fix: remove secrets

* fix: MAX iterations in render sync

* fix: render sync review fields

* fix: pr changes

* fix: lint

* fix: changes

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Sid
2025-08-19 16:11:51 +05:30
committed by GitHub
parent 7748e03612
commit 461deef0d5
19 changed files with 582 additions and 127 deletions

View File

@@ -2491,6 +2491,7 @@ export const SecretSyncs = {
},
RENDER: {
serviceId: "The ID of the Render service to sync secrets to.",
environmentGroupId: "The ID of the Render environment group to sync secrets to.",
scope: "The Render scope that secrets should be synced to.",
type: "The Render resource type to sync secrets to."
},

View File

@@ -49,4 +49,32 @@ export const registerRenderConnectionRouter = async (server: FastifyZodProvider)
return services;
}
});
server.route({
method: "GET",
url: `/:connectionId/environment-groups`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const groups = await server.services.appConnection.render.listEnvironmentGroups(connectionId, req.permission);
return groups;
}
});
};

View File

@@ -8,9 +8,11 @@ import { IntegrationUrls } from "@app/services/integration-auth/integration-list
import { AppConnection } from "../app-connection-enums";
import { RenderConnectionMethod } from "./render-connection-enums";
import {
TRawRenderEnvironmentGroup,
TRawRenderService,
TRenderConnection,
TRenderConnectionConfig,
TRenderEnvironmentGroup,
TRenderService
} from "./render-connection-types";
@@ -32,7 +34,11 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
const perPage = 100;
let cursor;
let maxIterations = 10;
while (hasMorePages) {
if (maxIterations <= 0) break;
const res: TRawRenderService[] = (
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
params: new URLSearchParams({
@@ -59,6 +65,8 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
} else {
cursor = res[res.length - 1].cursor;
}
maxIterations -= 1;
}
return services;
@@ -86,3 +94,52 @@ export const validateRenderConnectionCredentials = async (config: TRenderConnect
return inputCredentials;
};
export const listRenderEnvironmentGroups = async (
appConnection: TRenderConnection
): Promise<TRenderEnvironmentGroup[]> => {
const {
credentials: { apiKey }
} = appConnection;
const groups: TRenderEnvironmentGroup[] = [];
let hasMorePages = true;
const perPage = 100;
let cursor;
let maxIterations = 10;
while (hasMorePages) {
if (maxIterations <= 0) break;
const res: TRawRenderEnvironmentGroup[] = (
await request.get<TRawRenderEnvironmentGroup[]>(`${IntegrationUrls.RENDER_API_URL}/v1/env-groups`, {
params: new URLSearchParams({
...(cursor ? { cursor: String(cursor) } : {}),
limit: String(perPage)
}),
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
"Accept-Encoding": "application/json"
}
})
).data;
res.forEach((item) => {
groups.push({
name: item.envGroup.name,
id: item.envGroup.id
});
});
if (res.length < perPage) {
hasMorePages = false;
} else {
cursor = res[res.length - 1].cursor;
}
maxIterations -= 1;
}
return groups;
};

View File

@@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listRenderServices } from "./render-connection-fns";
import { listRenderEnvironmentGroups, listRenderServices } from "./render-connection-fns";
import { TRenderConnection } from "./render-connection-types";
type TGetAppConnectionFunc = (
@@ -24,7 +24,20 @@ export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc)
}
};
const listEnvironmentGroups = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
try {
const groups = await listRenderEnvironmentGroups(appConnection);
return groups;
} catch (error) {
logger.error(error, "Failed to list environment groups for Render connection");
return [];
}
};
return {
listServices
listServices,
listEnvironmentGroups
};
};

View File

@@ -33,3 +33,16 @@ export type TRawRenderService = {
name: string;
};
};
export type TRenderEnvironmentGroup = {
name: string;
id: string;
};
export type TRawRenderEnvironmentGroup = {
cursor: string;
envGroup: {
id: string;
name: string;
};
};

View File

@@ -1,5 +1,6 @@
export enum RenderSyncScope {
Service = "service"
Service = "service",
EnvironmentGroup = "environment-group"
}
export enum RenderSyncType {

View File

@@ -1,11 +1,13 @@
/* eslint-disable no-await-in-loop */
import { isAxiosError } from "axios";
import { AxiosRequestConfig, isAxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { RenderSyncScope } from "./render-sync-enums";
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
const MAX_RETRIES = 5;
@@ -27,6 +29,80 @@ const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0)
}
};
async function getSecrets(input: { destination: TRenderSyncWithCredentials["destinationConfig"]; token: string }) {
const req: AxiosRequestConfig = {
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
method: "GET",
headers: {
Authorization: `Bearer ${input.token}`,
Accept: "application/json"
}
};
switch (input.destination.scope) {
case RenderSyncScope.Service: {
req.url = `/services/${input.destination.serviceId}/env-vars`;
const allSecrets: TRenderSecret[] = [];
let cursor: string | undefined;
do {
// eslint-disable-next-line @typescript-eslint/no-loop-func
const { data } = await makeRequestWithRetry(() =>
request.request<
{
envVar: {
key: string;
value: string;
};
cursor: string;
}[]
>({
...req,
params: {
cursor
}
})
);
const secrets = data.map((item) => ({
key: item.envVar.key,
value: item.envVar.value
}));
allSecrets.push(...secrets);
if (data.length > 0 && data[data.length - 1]?.cursor) {
cursor = data[data.length - 1].cursor;
} else {
cursor = undefined;
}
} while (cursor);
return allSecrets;
}
case RenderSyncScope.EnvironmentGroup: {
req.url = `/env-groups/${input.destination.environmentGroupId}`;
const res = await makeRequestWithRetry(() =>
request.request<{
envVars: {
key: string;
value: string;
}[];
}>(req)
);
return res.data.envVars.map((item) => ({
key: item.key,
value: item.value
}));
}
default:
throw new BadRequestError({ message: "Unknown render sync destination scope" });
}
}
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
const {
destinationConfig,
@@ -35,45 +111,12 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
}
} = secretSync;
const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`;
const allSecrets: TRenderSecret[] = [];
let cursor: string | undefined;
const secrets = await getSecrets({
destination: destinationConfig,
token: apiKey
});
do {
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
const { data } = await makeRequestWithRetry(() =>
request.get<
{
envVar: {
key: string;
value: string;
};
cursor: string;
}[]
>(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
})
);
const secrets = data.map((item) => ({
key: item.envVar.key,
value: item.envVar.value
}));
allSecrets.push(...secrets);
if (data.length > 0 && data[data.length - 1]?.cursor) {
cursor = data[data.length - 1].cursor;
} else {
cursor = undefined;
}
} while (cursor);
return allSecrets;
return secrets;
};
const batchUpdateEnvironmentSecrets = async (
@@ -87,14 +130,91 @@ const batchUpdateEnvironmentSecrets = async (
}
} = secretSync;
await makeRequestWithRetry(() =>
request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
const req: AxiosRequestConfig = {
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
method: "PUT",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
};
switch (destinationConfig.scope) {
case RenderSyncScope.Service: {
await makeRequestWithRetry(() =>
request.request({
...req,
url: `/services/${destinationConfig.serviceId}/env-vars`,
data: envVars
})
);
break;
}
case RenderSyncScope.EnvironmentGroup: {
for await (const variable of envVars) {
await makeRequestWithRetry(() =>
request.request({
...req,
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${variable.key}`,
data: {
value: variable.value
}
})
);
}
})
);
break;
}
default:
throw new BadRequestError({ message: "Unknown render sync destination scope" });
}
};
const deleteEnvironmentSecret = async (
secretSync: TRenderSyncWithCredentials,
envVar: { key: string; value: string }
): Promise<void> => {
const {
destinationConfig,
connection: {
credentials: { apiKey }
}
} = secretSync;
const req: AxiosRequestConfig = {
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
method: "DELETE",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
};
switch (destinationConfig.scope) {
case RenderSyncScope.Service: {
await makeRequestWithRetry(() =>
request.request({
...req,
url: `/services/${destinationConfig.serviceId}/env-vars/${envVar.key}`
})
);
break;
}
case RenderSyncScope.EnvironmentGroup: {
await makeRequestWithRetry(() =>
request.request({
...req,
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${envVar.key}`
})
);
break;
}
default:
throw new BadRequestError({ message: "Unknown render sync destination scope" });
}
};
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
@@ -105,18 +225,50 @@ const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
}
} = secretSync;
await makeRequestWithRetry(() =>
request.post(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
{},
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
const req: AxiosRequestConfig = {
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
};
switch (destinationConfig.scope) {
case RenderSyncScope.Service: {
await makeRequestWithRetry(() =>
request.request({
...req,
method: "POST",
url: `/services/${destinationConfig.serviceId}/deploys`,
data: {}
})
);
break;
}
case RenderSyncScope.EnvironmentGroup: {
const { data } = await request.request<{ serviceLinks: { id: string }[] }>({
...req,
method: "GET",
url: `/env-groups/${destinationConfig.environmentGroupId}`
});
for await (const link of data.serviceLinks) {
// eslint-disable-next-line @typescript-eslint/no-loop-func
await makeRequestWithRetry(() =>
request.request({
...req,
url: `/services/${link.id}/deploys`,
data: {}
})
);
}
)
);
break;
}
default:
throw new BadRequestError({ message: "Unknown render sync destination scope" });
}
};
export const RenderSyncFns = {
@@ -169,14 +321,15 @@ export const RenderSyncFns = {
const finalEnvVars: Array<{ key: string; value: string }> = [];
for (const renderSecret of renderSecrets) {
if (!(renderSecret.key in secretMap)) {
if (renderSecret.key in secretMap) {
finalEnvVars.push({
key: renderSecret.key,
value: renderSecret.value
});
}
}
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
await Promise.all(finalEnvVars.map((el) => deleteEnvironmentSecret(secretSync, el)));
if (secretSync.syncOptions.autoRedeployServices) {
await redeployService(secretSync);

View File

@@ -17,6 +17,14 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
}),
z.object({
scope: z.literal(RenderSyncScope.EnvironmentGroup).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
environmentGroupId: z
.string()
.min(1, "Environment Group ID is required")
.describe(SecretSyncs.DESTINATION_CONFIG.RENDER.environmentGroupId),
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
})
]);

View File

@@ -30,8 +30,9 @@ description: "Learn how to configure a Render Sync for Infisical."
![Configure Destination](/images/secret-syncs/render/render-sync-destination.png)
- **Render Connection**: The Render Connection to authenticate with.
- **Scope**: Select **Service**.
- **Service**: Choose the Render service you want to sync secrets to.
- **Scope**: Select **Service** or **Environment Group**.
- **Service**: Choose the Render service you want to sync secrets to.
- **Environment Group**: Choose the Render environment group you want to sync secrets to.
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Options](/images/secret-syncs/render/render-sync-options.png)

View File

@@ -5,7 +5,9 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs";
import {
TRenderEnvironmentGroup,
TRenderService,
useRenderConnectionListEnvironmentGroups,
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -19,6 +21,7 @@ export const RenderSyncFields = () => {
>();
const connectionId = useWatch({ name: "connection.id", control });
const selectedScope = useWatch({ name: "destinationConfig.scope", control });
const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices(
connectionId,
@@ -27,11 +30,17 @@ export const RenderSyncFields = () => {
}
);
const { data: groups = [], isPending: isGroupsPending } =
useRenderConnectionListEnvironmentGroups(connectionId, {
enabled: Boolean(connectionId) && selectedScope === RenderSyncScope.EnvironmentGroup
});
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.serviceId", "");
setValue("destinationConfig.environmentGroupId", "");
setValue("destinationConfig.type", RenderSyncType.Env);
setValue("destinationConfig.scope", RenderSyncScope.Service);
}}
@@ -83,30 +92,67 @@ export const RenderSyncFields = () => {
</FormControl>
)}
/>
<Controller
name="destinationConfig.serviceId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Service">
<FilterableSelect
isLoading={isServicesPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={services ? (services.find((service) => service.id === value) ?? []) : []}
onChange={(option) => {
onChange((option as SingleValue<TRenderService>)?.id ?? null);
setValue(
"destinationConfig.serviceName",
(option as SingleValue<TRenderService>)?.name ?? ""
);
}}
options={services}
placeholder="Select a service..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
{selectedScope === RenderSyncScope.Service && (
<Controller
name="destinationConfig.serviceId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Service"
>
<FilterableSelect
isLoading={isServicesPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={services ? (services.find((service) => service.id === value) ?? []) : []}
onChange={(option) => {
onChange((option as SingleValue<TRenderService>)?.id ?? null);
setValue(
"destinationConfig.serviceName",
(option as SingleValue<TRenderService>)?.name ?? ""
);
}}
options={services}
placeholder="Select a service..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
)}
{selectedScope === RenderSyncScope.EnvironmentGroup && (
<Controller
name="destinationConfig.environmentGroupId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Environment Group"
>
<FilterableSelect
isLoading={isGroupsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={groups ? (groups.find((g) => g.id === value) ?? []) : []}
onChange={(option) => {
onChange((option as SingleValue<TRenderEnvironmentGroup>)?.id ?? null);
setValue(
"destinationConfig.environmentGroupName",
(option as SingleValue<TRenderEnvironmentGroup>)?.name ?? ""
);
}}
options={groups}
placeholder="Select an environment group..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
)}
</>
);
};

View File

@@ -4,6 +4,7 @@ import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
export const RenderSyncOptionsReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
@@ -27,13 +28,20 @@ export const RenderSyncOptionsReviewFields = () => {
export const RenderSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
const serviceName = watch("destinationConfig.serviceName");
const scope = watch("destinationConfig.scope");
const config = watch("destinationConfig");
return (
<>
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
<GenericFieldLabel label="Service">{serviceName}</GenericFieldLabel>
<GenericFieldLabel label="Scope">{config.scope}</GenericFieldLabel>
{config.scope === RenderSyncScope.Service ? (
<GenericFieldLabel label="Service">
{config.serviceName ?? config.serviceId}
</GenericFieldLabel>
) : (
<GenericFieldLabel label="Service">
{config.environmentGroupName ?? config.environmentGroupId}
</GenericFieldLabel>
)}
</>
);
};

View File

@@ -17,6 +17,12 @@ export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
serviceId: z.string().trim().min(1, "Service is required"),
serviceName: z.string().trim().optional(),
type: z.nativeEnum(RenderSyncType)
}),
z.object({
scope: z.literal(RenderSyncScope.EnvironmentGroup),
environmentGroupId: z.string().trim().min(1, "Environment Group ID is required"),
environmentGroupName: z.string().trim().optional(),
type: z.nativeEnum(RenderSyncType)
})
])
})

View File

@@ -212,5 +212,9 @@ export const RENDER_SYNC_SCOPES: Record<RenderSyncScope, { name: string; descrip
[RenderSyncScope.Service]: {
name: "Service",
description: "Infisical will sync secrets to the specified Render service."
},
[RenderSyncScope.EnvironmentGroup]: {
name: "EnvironmentGroup",
description: "Infisical will sync secrets to the specified Render environment group."
}
};

View File

@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TRenderService } from "./types";
import { TRenderEnvironmentGroup, TRenderService } from "./types";
const renderConnectionKeys = {
all: [...appConnectionKeys.all, "render"] as const,
listServices: (connectionId: string) =>
[...renderConnectionKeys.all, "services", connectionId] as const
[...renderConnectionKeys.all, "services", connectionId] as const,
listEnvironmentGroups: (connectionId: string) =>
[...renderConnectionKeys.all, "environment-groups", connectionId] as const
};
export const useRenderConnectionListServices = (
@@ -35,3 +37,28 @@ export const useRenderConnectionListServices = (
...options
});
};
export const useRenderConnectionListEnvironmentGroups = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TRenderEnvironmentGroup[],
unknown,
TRenderEnvironmentGroup[],
ReturnType<typeof renderConnectionKeys.listEnvironmentGroups>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: renderConnectionKeys.listEnvironmentGroups(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TRenderEnvironmentGroup[]>(
`/api/v1/app-connections/render/${connectionId}/environment-groups`
);
return data;
},
...options
});
};

View File

@@ -2,3 +2,8 @@ export type TRenderService = {
id: string;
name: string;
};
export type TRenderEnvironmentGroup = {
id: string;
name: string;
};

View File

@@ -4,12 +4,19 @@ import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/typ
export type TRenderSync = TRootSecretSync & {
destination: SecretSync.Render;
destinationConfig: {
scope: RenderSyncScope.Service;
type: RenderSyncType;
serviceId: string;
serviceName?: string;
};
destinationConfig:
| {
type: RenderSyncType;
scope: RenderSyncScope.Service;
serviceId: string;
serviceName?: string | undefined;
}
| {
type: RenderSyncType;
scope: RenderSyncScope.EnvironmentGroup;
environmentGroupId: string;
environmentGroupName?: string | undefined;
};
connection: {
app: AppConnection.Render;
@@ -23,7 +30,8 @@ export type TRenderSync = TRootSecretSync & {
};
export enum RenderSyncScope {
Service = "service"
Service = "service",
EnvironmentGroup = "environment-group"
}
export enum RenderSyncType {

View File

@@ -1,5 +1,8 @@
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import {
useRenderConnectionListEnvironmentGroups,
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
@@ -9,21 +12,59 @@ type Props = {
};
export const RenderSyncDestinationCol = ({ secretSync }: Props) => {
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
const { data: services = [], isPending } = useRenderConnectionListServices(
secretSync.connectionId
secretSync.connectionId,
{
enabled: isServiceScope
}
);
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
...secretSync,
destinationConfig: {
...secretSync.destinationConfig,
serviceName: services.find((s) => s.id === secretSync.destinationConfig.serviceId)?.name
const { data: groups = [], isPending: isGroupsPending } =
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
switch (secretSync.destinationConfig.scope) {
case RenderSyncScope.Service: {
const id = secretSync.destinationConfig.serviceId;
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
...secretSync,
destinationConfig: {
...secretSync.destinationConfig,
serviceName: services.find((s) => s.id === id)?.name
}
});
if (isPending) {
return (
<SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />
);
}
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
}
});
case RenderSyncScope.EnvironmentGroup: {
const id = secretSync.destinationConfig.environmentGroupId;
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
...secretSync,
destinationConfig: {
...secretSync.destinationConfig,
environmentGroupName: groups.find((s) => s.id === id)?.name
}
});
if (isPending) {
return <SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />;
if (isGroupsPending) {
return (
<SecretSyncTableCell
primaryText="Loading environment group info..."
secondaryText="Environment Group"
/>
);
}
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
}
default:
throw new Error("Unknown render sync destination scope");
}
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -8,6 +8,7 @@ import {
} from "@app/hooks/api/secretSyncs/types/github-sync";
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
// This functional ensures parity across what is displayed in the destination column
// and the values used when search filtering
@@ -125,8 +126,15 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
secondaryText = destinationConfig.app;
break;
case SecretSync.Render:
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
secondaryText = "Service";
if (destinationConfig.scope === RenderSyncScope.Service) {
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
secondaryText = "Service";
} else {
primaryText =
destinationConfig.environmentGroupName ?? destinationConfig.environmentGroupId;
secondaryText = "Environment Group";
}
break;
case SecretSync.Flyio:
primaryText = destinationConfig.appId;

View File

@@ -1,23 +1,50 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import {
useRenderConnectionListEnvironmentGroups,
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
type Props = {
secretSync: TRenderSync;
};
export const RenderSyncDestinationSection = ({ secretSync }: Props) => {
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
const { data: services = [], isPending } = useRenderConnectionListServices(
secretSync.connectionId
secretSync.connectionId,
{
enabled: isServiceScope
}
);
const {
destinationConfig: { serviceId }
} = secretSync;
if (isPending) {
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
const { data: groups = [], isPending: isGroupsPending } =
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
switch (secretSync.destinationConfig.scope) {
case RenderSyncScope.Service: {
const id = secretSync.destinationConfig.serviceId;
if (isPending) {
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
}
const serviceName = services.find((service) => service.id === id)?.name;
return <GenericFieldLabel label="Service">{serviceName ?? id}</GenericFieldLabel>;
}
case RenderSyncScope.EnvironmentGroup: {
const id = secretSync.destinationConfig.environmentGroupId;
if (isGroupsPending) {
return <GenericFieldLabel label="Environment Group">Loading...</GenericFieldLabel>;
}
const envName = groups.find((g) => g.id === id)?.name;
return <GenericFieldLabel label="Environment Group">{envName ?? id}</GenericFieldLabel>;
}
default:
throw new Error("Unknown render sync destination scope");
}
const serviceName = services.find((service) => service.id === serviceId)?.name;
return <GenericFieldLabel label="Service">{serviceName ?? serviceId}</GenericFieldLabel>;
};