mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 15:43:12 +00:00
Merge branch 'main' into feat/remindersImprovement
This commit is contained in:
@@ -6,7 +6,6 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { createNotification } from "../notifications";
|
||||
import { IconButton, Select, SelectItem, Tooltip } from "../v2";
|
||||
@@ -93,7 +92,7 @@ export default function NavHeader({
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
{pageName === "Secrets" ? (
|
||||
<Link
|
||||
to="/projects/$projectId/secret-manager/overview"
|
||||
to="/projects/secret-management/$projectId/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
className="text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
@@ -129,7 +128,7 @@ export default function NavHeader({
|
||||
<div className="flex items-center space-x-3">
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
<Link
|
||||
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
|
||||
to="/projects/secret-management/$projectId/secrets/$envSlug"
|
||||
params={{ projectId: currentWorkspace.id, envSlug: routerEnvSlug }}
|
||||
className="text-sm font-semibold text-primary/80 hover:text-primary"
|
||||
>
|
||||
@@ -191,7 +190,7 @@ export default function NavHeader({
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
|
||||
to="/projects/secret-management/$projectId/secrets/$envSlug"
|
||||
params={{
|
||||
projectId: currentWorkspace.id,
|
||||
envSlug: routerEnvSlug || ""
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Link } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { createNotification } from "../notifications";
|
||||
import { IconButton, Tooltip } from "../v2";
|
||||
@@ -70,7 +69,7 @@ export const SecretDashboardPathBreadcrumb = ({
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to={`/projects/$projectId/${ProjectType.SecretManager}/secrets/$envSlug` as const}
|
||||
to="/projects/secret-management/$projectId/secrets/$envSlug"
|
||||
params={{
|
||||
projectId,
|
||||
envSlug: environmentSlug
|
||||
|
||||
@@ -5,14 +5,12 @@ import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useUpdateProject } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
defaultProduct: z.nativeEnum(ProjectType).default(ProjectType.SecretManager),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -52,7 +50,6 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? "",
|
||||
defaultProduct: currentWorkspace.defaultProduct,
|
||||
...(showSlugField && { slug: currentWorkspace.slug })
|
||||
});
|
||||
}
|
||||
@@ -66,7 +63,6 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: data.name,
|
||||
newProjectDescription: data.description,
|
||||
defaultProduct: data.defaultProduct,
|
||||
...(showSlugField &&
|
||||
"slug" in data && {
|
||||
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
|
||||
@@ -216,31 +212,6 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultProduct"
|
||||
defaultValue={ProjectType.SecretManager}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Default Product"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full max-w-md bg-mineshaft-800"
|
||||
>
|
||||
<SelectItem value={ProjectType.SecretManager}>Secrets Manager</SelectItem>
|
||||
<SelectItem value={ProjectType.CertificateManager}>PKI Manager</SelectItem>
|
||||
<SelectItem value={ProjectType.KMS}>KMS</SelectItem>
|
||||
<SelectItem value={ProjectType.SSH}>SSH</SelectItem>
|
||||
<SelectItem value={ProjectType.SecretScanning}>Secret Scanning</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import z from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Lottie,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
@@ -31,10 +33,11 @@ import {
|
||||
useSubscription,
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import { getProjectHomePage, getProjectLottieIcon } from "@app/helpers/project";
|
||||
import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
@@ -43,6 +46,7 @@ const formSchema = z.object({
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional(),
|
||||
type: z.nativeEnum(ProjectType),
|
||||
kmsKeyId: z.string(),
|
||||
template: z.string()
|
||||
});
|
||||
@@ -56,6 +60,29 @@ interface NewProjectModalProps {
|
||||
|
||||
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange">;
|
||||
|
||||
const PROJECT_TYPE_MENU_ITEMS = [
|
||||
{
|
||||
label: "Secrets Management",
|
||||
value: ProjectType.SecretManager
|
||||
},
|
||||
{
|
||||
label: "Cert Management",
|
||||
value: ProjectType.CertificateManager
|
||||
},
|
||||
{
|
||||
label: "KMS",
|
||||
value: ProjectType.KMS
|
||||
},
|
||||
{
|
||||
label: "SSH",
|
||||
value: ProjectType.SSH
|
||||
},
|
||||
{
|
||||
label: "Secret Scanning",
|
||||
value: ProjectType.SecretScanning
|
||||
}
|
||||
];
|
||||
|
||||
const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
@@ -70,18 +97,11 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
OrgPermissionSubjects.ProjectTemplates
|
||||
);
|
||||
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||
});
|
||||
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg.id, {
|
||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<TAddProjectFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -91,6 +111,16 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
}
|
||||
});
|
||||
|
||||
const selectedProjectType = watch("type");
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates),
|
||||
select: (template) => template.filter((el) => el.type === selectedProjectType)
|
||||
});
|
||||
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg.id, {
|
||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
console.log("Current form errors:", errors);
|
||||
@@ -101,7 +131,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
name,
|
||||
description,
|
||||
kmsKeyId,
|
||||
template
|
||||
template,
|
||||
type
|
||||
}: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg) return;
|
||||
@@ -113,7 +144,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
projectName: name,
|
||||
projectDescription: description,
|
||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||
template
|
||||
template,
|
||||
type
|
||||
});
|
||||
await refetchWorkspaces();
|
||||
|
||||
@@ -121,7 +153,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
navigate({
|
||||
to: getProjectHomePage(project.defaultProduct),
|
||||
to: getProjectHomePage(project.type),
|
||||
params: { projectId: project.id }
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -150,6 +182,42 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue={ProjectType.SecretManager}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Type"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<div className="mt-2 grid grid-cols-5 gap-4">
|
||||
{PROJECT_TYPE_MENU_ITEMS.map((el) => (
|
||||
<div
|
||||
key={el.value}
|
||||
className={twMerge(
|
||||
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
|
||||
field.value === el.value && "border-primary-400 bg-mineshaft-600 opacity-100"
|
||||
)}
|
||||
onClick={() => field.onChange(el.value)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
field.onChange(el.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Lottie icon={getProjectLottieIcon(el.value)} className="h-8 w-8" />
|
||||
<div className="text-center text-xs">{el.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
@@ -171,6 +239,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="template"
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
|
||||
import { TOktaClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/okta-client-secret-rotation";
|
||||
|
||||
import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
|
||||
|
||||
type Props = {
|
||||
generatedCredentialsResponse: TOktaClientSecretRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
export const ViewOktaClientSecretRotationGeneratedCredentials = ({
|
||||
generatedCredentialsResponse: { generatedCredentials, activeIndex }
|
||||
}: Props) => {
|
||||
const inactiveIndex = activeIndex === 0 ? 1 : 0;
|
||||
|
||||
const activeCredentials = generatedCredentials[activeIndex];
|
||||
const inactiveCredentials = generatedCredentials[inactiveIndex];
|
||||
|
||||
return (
|
||||
<ViewRotationGeneratedCredentialsDisplay
|
||||
activeCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Client ID">{activeCredentials?.clientId}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Client Secret">
|
||||
{activeCredentials?.clientSecret}
|
||||
</CredentialDisplay>
|
||||
</>
|
||||
}
|
||||
inactiveCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Client ID">{inactiveCredentials?.clientId}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Client Secret">
|
||||
{inactiveCredentials?.clientSecret}
|
||||
</CredentialDisplay>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+8
@@ -22,6 +22,7 @@ import {
|
||||
|
||||
import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared";
|
||||
import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials";
|
||||
import { ViewOktaClientSecretRotationGeneratedCredentials } from "./ViewOktaClientSecretRotationGeneratedCredentials";
|
||||
|
||||
type Props = {
|
||||
secretRotation?: TSecretRotationV2;
|
||||
@@ -99,6 +100,13 @@ const Content = ({ secretRotation }: ContentProps) => {
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case SecretRotation.OktaClientSecret:
|
||||
Component = (
|
||||
<ViewOktaClientSecretRotationGeneratedCredentials
|
||||
generatedCredentialsResponse={generatedCredentialsResponse}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unhandled View Generated Credential Rotation Type");
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { useOktaConnectionListApps } from "@app/hooks/api/appConnections/okta";
|
||||
import { TOktaApp } from "@app/hooks/api/appConnections/okta/types";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const OktaClientSecretRotationParametersFields = () => {
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.OktaClientSecret;
|
||||
}
|
||||
>();
|
||||
|
||||
const connectionId = watch("connection.id");
|
||||
|
||||
const { data: apps, isPending: isAppsPending } = useOktaConnectionListApps(connectionId, {
|
||||
enabled: Boolean(connectionId)
|
||||
});
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="parameters.clientId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="OpenID Connect Application"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isAppsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={apps?.find((app) => app.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TOktaApp>)?.id ?? null);
|
||||
setValue("parameters.clientId", (option as SingleValue<TOktaApp>)?.id ?? "");
|
||||
}}
|
||||
options={apps}
|
||||
placeholder="Select an application..."
|
||||
getOptionLabel={(option) => option.label}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRo
|
||||
import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields";
|
||||
import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields";
|
||||
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
|
||||
import { OktaClientSecretRotationParametersFields } from "./OktaClientSecretRotationParametersFields";
|
||||
import { SqlCredentialsRotationParametersFields } from "./shared";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
@@ -17,7 +18,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ParametersFields = () => {
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretRotationReviewSection } from "./shared";
|
||||
|
||||
export const OktaClientSecretRotationReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.OktaClientSecret;
|
||||
}
|
||||
>();
|
||||
|
||||
const [parameters, { clientId, clientSecret }] = watch(["parameters", "secretsMapping"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretRotationReviewSection label="Parameters">
|
||||
<GenericFieldLabel label="App ID">{parameters.clientId}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
<SecretRotationReviewSection label="Secrets Mapping">
|
||||
<GenericFieldLabel label="Client ID">{clientId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Client Secret">{clientSecret}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -10,6 +10,7 @@ import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotati
|
||||
import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields";
|
||||
import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields";
|
||||
import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields";
|
||||
import { OktaClientSecretRotationReviewFields } from "./OktaClientSecretRotationReviewFields";
|
||||
import { SqlCredentialsRotationReviewFields } from "./shared";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
@@ -20,7 +21,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ReviewFields = () => {
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretsMappingTable } from "./shared";
|
||||
|
||||
export const OktaClientSecretRotationSecretsMappingFields = () => {
|
||||
const { control } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.OktaClientSecret;
|
||||
}
|
||||
>();
|
||||
|
||||
const { rotationOption } = useSecretRotationV2Option(SecretRotation.OktaClientSecret);
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: "Client ID",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.clientId}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.clientId"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "Client Secret",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.clientSecret}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.clientSecret"
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return <SecretsMappingTable items={items} />;
|
||||
};
|
||||
+3
-1
@@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecr
|
||||
import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields";
|
||||
import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields";
|
||||
import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields";
|
||||
import { OktaClientSecretRotationSecretsMappingFields } from "./OktaClientSecretRotationSecretsMappingFields";
|
||||
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
@@ -17,7 +18,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2SecretsMappingFields = () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotati
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
|
||||
|
||||
import { OktaClientSecretRotationSchema } from "./okta-client-secret-rotation-schema";
|
||||
import { OracleDBCredentialsRotationSchema } from "./oracledb-credentials-rotation-schema";
|
||||
|
||||
export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
|
||||
@@ -23,7 +24,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
|
||||
MySqlCredentialsRotationSchema,
|
||||
OracleDBCredentialsRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema
|
||||
AwsIamUserSecretRotationSchema,
|
||||
OktaClientSecretRotationSchema
|
||||
]),
|
||||
z.object({ id: z.string().optional() })
|
||||
)
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const OktaClientSecretRotationSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretRotation.OktaClientSecret),
|
||||
parameters: z.object({
|
||||
clientId: z.string().trim().min(1, "App ID required")
|
||||
}),
|
||||
secretsMapping: z.object({
|
||||
clientId: z.string().trim().min(1, "Client ID required"),
|
||||
clientSecret: z.string().trim().min(1, "Client Secret required")
|
||||
})
|
||||
})
|
||||
.merge(BaseSecretRotationSchema);
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import {
|
||||
TBitbucketEnvironment,
|
||||
TBitbucketRepo,
|
||||
TBitbucketWorkspace,
|
||||
useBitbucketConnectionListEnvironments,
|
||||
useBitbucketConnectionListRepositories,
|
||||
useBitbucketConnectionListWorkspaces
|
||||
} from "@app/hooks/api/appConnections/bitbucket";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const BitbucketSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.Bitbucket }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const workspace = useWatch({ name: "destinationConfig.workspaceSlug", control });
|
||||
const repository = useWatch({ name: "destinationConfig.repositorySlug", control });
|
||||
|
||||
const { data: workspaces = [], isPending: isWorkspacesLoading } =
|
||||
useBitbucketConnectionListWorkspaces(connectionId, {
|
||||
enabled: Boolean(connectionId)
|
||||
});
|
||||
|
||||
const { data: repositories = [], isPending: isRepositoriesLoading } =
|
||||
useBitbucketConnectionListRepositories(connectionId, workspace ?? "", {
|
||||
enabled: Boolean(connectionId) && Boolean(workspace)
|
||||
});
|
||||
|
||||
const { data: environments = [], isPending: isEnvironmentsLoading } =
|
||||
useBitbucketConnectionListEnvironments(connectionId, workspace ?? "", repository ?? "", {
|
||||
enabled: Boolean(connectionId) && Boolean(workspace) && Boolean(repository)
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.workspaceSlug", "");
|
||||
setValue("destinationConfig.repositorySlug", "");
|
||||
setValue("destinationConfig.environmentId", "");
|
||||
}}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="destinationConfig.workspaceSlug"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Bitbucket Workspace"
|
||||
tooltipClassName="max-w-md"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isWorkspacesLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={workspaces.find((w) => w.slug === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TBitbucketWorkspace>;
|
||||
onChange(v?.slug ?? "");
|
||||
// Clear downstream selections
|
||||
setValue("destinationConfig.repositorySlug", "");
|
||||
setValue("destinationConfig.environmentId", "");
|
||||
}}
|
||||
options={workspaces}
|
||||
placeholder="Select workspace..."
|
||||
getOptionLabel={(option) => option.slug}
|
||||
getOptionValue={(option) => option.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="destinationConfig.repositorySlug"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Bitbucket Repository"
|
||||
tooltipClassName="max-w-md"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isRepositoriesLoading && Boolean(workspace)}
|
||||
isDisabled={!workspace}
|
||||
value={repositories.find((r) => r.slug === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TBitbucketRepo>;
|
||||
onChange(v?.slug ?? "");
|
||||
// Clear downstream selections
|
||||
setValue("destinationConfig.environmentId", "");
|
||||
}}
|
||||
options={repositories}
|
||||
placeholder="Select repository..."
|
||||
getOptionLabel={(option) => option.full_name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="destinationConfig.environmentId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isOptional
|
||||
label="Bitbucket Deployment Environment"
|
||||
tooltipClassName="max-w-md"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isEnvironmentsLoading && Boolean(repository)}
|
||||
isDisabled={!repository}
|
||||
value={environments.find((e) => e.uuid === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TBitbucketEnvironment>;
|
||||
onChange(v?.uuid ?? "");
|
||||
}}
|
||||
options={environments}
|
||||
placeholder="Select environment..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.uuid}
|
||||
isClearable
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
@@ -9,6 +9,7 @@ import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
|
||||
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
|
||||
import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields";
|
||||
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
|
||||
import { BitbucketSyncFields } from "./BitbucketSyncFields";
|
||||
import { CamundaSyncFields } from "./CamundaSyncFields";
|
||||
import { ChecklySyncFields } from "./ChecklySyncFields";
|
||||
import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields";
|
||||
@@ -91,6 +92,8 @@ export const SecretSyncDestinationFields = () => {
|
||||
return <ChecklySyncFields />;
|
||||
case SecretSync.Supabase:
|
||||
return <SupabaseSyncFields />;
|
||||
case SecretSync.Bitbucket:
|
||||
return <BitbucketSyncFields />;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
||||
}
|
||||
|
||||
+1
@@ -63,6 +63,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.Railway:
|
||||
case SecretSync.Checkly:
|
||||
case SecretSync.Supabase:
|
||||
case SecretSync.Bitbucket:
|
||||
AdditionalSyncOptionsFieldsComponent = null;
|
||||
break;
|
||||
default:
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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 BitbucketSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Bitbucket }>();
|
||||
const repository = watch("destinationConfig.repositorySlug");
|
||||
const environment = watch("destinationConfig.environmentId");
|
||||
const workspace = watch("destinationConfig.workspaceSlug");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Repository">{repository}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Environment">{environment}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Workspace">{workspace}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
@@ -18,6 +18,7 @@ import {
|
||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||
import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields";
|
||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||
import { BitbucketSyncReviewFields } from "./BitbucketSyncReviewFields";
|
||||
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
||||
import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields";
|
||||
import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields";
|
||||
@@ -144,6 +145,9 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.Supabase:
|
||||
DestinationFieldsComponent = <SupabaseSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Bitbucket:
|
||||
DestinationFieldsComponent = <BitbucketSyncReviewFields />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
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 BitbucketSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Bitbucket),
|
||||
destinationConfig: z.object({
|
||||
repositorySlug: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Repository slug required")
|
||||
.describe("Repository slug"),
|
||||
environmentId: z.string().trim().optional().describe("Deployment environment uuid"),
|
||||
workspaceSlug: z.string().trim().min(1, "Workspace slug required").describe("Workspace slug")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -6,6 +6,7 @@ import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sy
|
||||
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
||||
import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destination-schema";
|
||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||
import { BitbucketSyncDestinationSchema } from "./bitbucket-sync-destination-schema";
|
||||
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
||||
import { ChecklySyncDestinationSchema } from "./checkly-sync-destination-schema";
|
||||
import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema";
|
||||
@@ -55,7 +56,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
SupabaseSyncDestinationSchema,
|
||||
ZabbixSyncDestinationSchema,
|
||||
RailwaySyncDestinationSchema,
|
||||
ChecklySyncDestinationSchema
|
||||
ChecklySyncDestinationSchema,
|
||||
BitbucketSyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||
|
||||
@@ -41,7 +41,8 @@ export const CopyButton = ({
|
||||
variant={variant}
|
||||
className={twMerge("group relative", color)}
|
||||
size={size}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyText();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ChangeEventHandler, useState } from "react";
|
||||
import { DayPicker, DayPickerProps, getDefaultClassNames, UI } from "react-day-picker";
|
||||
import { DayPicker, DayPickerProps, getDefaultClassNames, TZDate, UI } from "react-day-picker";
|
||||
import { faCalendar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover";
|
||||
import { format, setHours, setMinutes } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { formatDateTime, Timezone } from "@app/helpers/datetime";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2";
|
||||
@@ -19,6 +21,32 @@ export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
||||
popUpContentProps: PopoverContentProps;
|
||||
dateFormat?: "PPP" | "PP" | "P"; // extend as needed
|
||||
hideTime?: boolean;
|
||||
buttonClassName?: string;
|
||||
timezone?: Timezone;
|
||||
};
|
||||
|
||||
const localTimeToUTC = (timeString: string) => {
|
||||
const today = new Date();
|
||||
const [hours, minutes] = timeString.split(":").map(Number);
|
||||
|
||||
today.setHours(hours, minutes, 0, 0);
|
||||
|
||||
const utcHours = today.getUTCHours().toString().padStart(2, "0");
|
||||
const utcMinutes = today.getUTCMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${utcHours}:${utcMinutes}`;
|
||||
};
|
||||
|
||||
const utcTimeToLocal = (utcTimeString: string) => {
|
||||
const today = new Date();
|
||||
const [hours, minutes] = utcTimeString.split(":").map(Number);
|
||||
|
||||
today.setUTCHours(hours, minutes, 0, 0);
|
||||
|
||||
const localHours = today.getHours().toString().padStart(2, "0");
|
||||
const localMinutes = today.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${localHours}:${localMinutes}`;
|
||||
};
|
||||
|
||||
// Doc: https://react-day-picker.js.org/
|
||||
@@ -29,12 +57,16 @@ export const DatePicker = ({
|
||||
popUpContentProps,
|
||||
dateFormat = "PPP",
|
||||
hideTime = false,
|
||||
buttonClassName,
|
||||
timezone,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
const [timeValue, setTimeValue] = useState<string>(value ? format(value, "HH:mm") : "00:00");
|
||||
const displayUtc = timezone === Timezone.UTC;
|
||||
|
||||
const handleTimeChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const time = e.target.value;
|
||||
const time = displayUtc ? utcTimeToLocal(e.target.value) : e.target.value;
|
||||
|
||||
if (time) {
|
||||
setTimeValue(time);
|
||||
if (value) {
|
||||
@@ -59,8 +91,14 @@ export const DatePicker = ({
|
||||
return (
|
||||
<Popover {...popUpProps}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
||||
{value ? format(value, dateFormat) : "Pick a date and time"}
|
||||
<Button
|
||||
className={buttonClassName}
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faCalendar} />}
|
||||
>
|
||||
{value
|
||||
? formatDateTime({ timestamp: value, timezone, dateFormat })
|
||||
: "Pick a date and time"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
@@ -74,9 +112,10 @@ export const DatePicker = ({
|
||||
<DayPicker
|
||||
{...props}
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={handleDaySelect}
|
||||
selected={value ? new TZDate(value, displayUtc ? "UTC" : undefined) : undefined}
|
||||
onSelect={(date) => handleDaySelect(date ? new TZDate(date, undefined) : undefined)}
|
||||
className="font-inter text-mineshaft-200"
|
||||
timeZone={displayUtc ? "UTC" : undefined}
|
||||
classNames={{
|
||||
today: "text-primary border-primary",
|
||||
selected: " text-mineshaft-100 bg-mineshaft-500",
|
||||
@@ -91,7 +130,7 @@ export const DatePicker = ({
|
||||
<div className="mx-4 my-4">
|
||||
<Input
|
||||
type="time"
|
||||
value={timeValue}
|
||||
value={displayUtc ? localTimeToUTC(timeValue) : timeValue}
|
||||
onChange={handleTimeChange}
|
||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ type Props = {
|
||||
icon?: IconProp;
|
||||
isMulti?: boolean;
|
||||
iconClassName?: string;
|
||||
dropdownContainerStyle?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
|
||||
@@ -35,6 +36,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
position,
|
||||
containerClassName,
|
||||
iconClassName,
|
||||
dropdownContainerStyle,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -82,7 +84,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
dropdownContainerClassName
|
||||
)}
|
||||
position={position}
|
||||
style={{ width: "var(--radix-select-trigger-width)" }}
|
||||
style={dropdownContainerStyle ?? { width: "var(--radix-select-trigger-width)" }}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton>
|
||||
<div className="flex items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user