Merge branch 'main' into feat/azureClientSecretsRotation

This commit is contained in:
carlosmonastyrski
2025-04-21 10:27:56 -03:00
175 changed files with 2608 additions and 249 deletions
@@ -14,6 +14,7 @@ import { GitHubSyncFields } from "./GitHubSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
import { WindmillSyncFields } from "./WindmillSyncFields";
export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -43,6 +44,8 @@ export const SecretSyncDestinationFields = () => {
return <CamundaSyncFields />;
case SecretSync.Vercel:
return <VercelSyncFields />;
case SecretSync.Windmill:
return <WindmillSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,105 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2";
import {
TWindmillWorkspace,
useWindmillConnectionListWorkspaces
} from "@app/hooks/api/appConnections/windmill";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const WindmillSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Windmill }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: workspaces, isLoading: isWorkspacesLoading } = useWindmillConnectionListWorkspaces(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.workspace", "");
}}
/>
<Controller
name="destinationConfig.workspace"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Workspace"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the workspace exists in the connection's Windmill instance URL."
>
<div>
<span>Don&#39;t see the workspace you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isWorkspacesLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={workspaces?.find((workspace) => workspace.name === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TWindmillWorkspace>)?.name ?? null)
}
options={workspaces}
placeholder="Select a workspace..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.path"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-sm"
tooltipText={
<>
The workspace path where secrets should be synced to. Path must follow Windmill{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.windmill.dev/docs/core_concepts/roles_and_permissions#path"
>
<span className="cursor-pointer underline decoration-primary underline-offset-2 hover:text-mineshaft-200">
owner path convention
</span>
.
</a>
</>
}
isError={Boolean(error)}
errorText={error?.message}
label="Path"
>
<Input value={value} onChange={onChange} placeholder="f/folder-name/" />
</FormControl>
)}
/>
</>
);
};
@@ -42,6 +42,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.TerraformCloud:
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -24,6 +24,7 @@ import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields";
export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -84,6 +85,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Vercel:
DestinationFieldsComponent = <VercelSyncReviewFields />;
break;
case SecretSync.Windmill:
DestinationFieldsComponent = <WindmillSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,18 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const WindmillSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Windmill }>();
const workspace = watch("destinationConfig.workspace");
const path = watch("destinationConfig.path");
return (
<>
<GenericFieldLabel label="Workspace">{workspace}</GenericFieldLabel>
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
</>
);
};
@@ -1,17 +1,17 @@
import { z } from "zod";
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sync-destination-schema";
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
@@ -24,7 +24,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
HumanitecSyncDestinationSchema,
TerraformCloudSyncDestinationSchema,
CamundaSyncDestinationSchema,
VercelSyncDestinationSchema
VercelSyncDestinationSchema,
WindmillSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;
@@ -0,0 +1,21 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const WindmillSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Windmill),
destinationConfig: z.object({
workspace: z.string().trim().min(1, "Workspace required"),
path: z
.string()
.trim()
.min(1, "Path required")
.regex(
/^([uf])\/([a-zA-Z0-9_-]+)(\/[a-zA-Z0-9_-]+)*\/$/,
'Invalid path - must follow Windmill path format. ex: "f/folder/path/"'
)
})
})
);
@@ -112,9 +112,8 @@ const PasswordGeneratorModal = ({
className="w-full max-w-lg rounded-lg border border-mineshaft-600 bg-mineshaft-800 shadow-xl"
>
<div className="p-6">
<h2 className="mb-1 text-xl font-semibold text-bunker-200">Password Generator</h2>
<p className="mb-6 text-sm text-bunker-400">Generate strong unique passwords</p>
<h2 className="mb-1 text-xl font-semibold text-bunker-200">Generate Random Value</h2>
<p className="mb-6 text-sm text-bunker-400">Generate strong unique values</p>
<div className="relative mb-4 rounded-md bg-mineshaft-900 p-4">
<div className="flex items-center justify-between">
<div className="w-4/5 select-all break-all pr-2 font-mono text-lg">{password}</div>
@@ -146,18 +145,18 @@ const PasswordGeneratorModal = ({
<div className="mb-6">
<div className="mb-1 flex items-center justify-between">
<label htmlFor="password-length" className="text-sm text-bunker-300">
Password length: {passwordOptions.length}
<label htmlFor="value-length" className="text-sm text-bunker-300">
Length: {passwordOptions.length}
</label>
</div>
<Slider
id="password-length"
id="value-length"
min={minLength}
max={maxLength}
value={passwordOptions.length}
onChange={(value) => setPasswordOptions({ ...passwordOptions, length: value })}
className="mb-1"
aria-labelledby="password-length-label"
aria-labelledby="value-length-label"
/>
</div>
@@ -219,7 +218,7 @@ const PasswordGeneratorModal = ({
onClick={usePassword}
className="ml-2"
>
Use Password
Use Value
</Button>
)}
</div>
@@ -251,12 +250,13 @@ export const PasswordGenerator = ({
return (
<>
<IconButton
ariaLabel="generate password"
colorSchema="secondary"
ariaLabel="generate value"
colorSchema="primary"
variant="outline_bg"
size="sm"
onClick={toggleGenerator}
isDisabled={isDisabled}
className="rounded text-bunker-400 transition-colors duration-150 hover:bg-mineshaft-700 hover:text-bunker-200"
className="rounded"
>
<FontAwesomeIcon icon={faKey} />
</IconButton>