Merge remote-tracking branch 'origin/main' into feat/cardDeclinedAlert

This commit is contained in:
Carlos Monastyrski
2025-07-30 09:12:22 -03:00
1128 changed files with 23599 additions and 35714 deletions
@@ -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
@@ -1,26 +1,14 @@
import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
import { Tooltip } from "../v2";
import { AccessRestrictedBanner, Tooltip } from "../v2";
export const OrgPermissionGuardBanner = () => {
return (
<div className="container mx-auto flex h-full items-center justify-center">
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300">
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
<div className="text-sm">
Your role has limited permissions, please <br /> contact your admin to gain access
</div>
</div>
</div>
<AccessRestrictedBanner />
</div>
);
};
@@ -1,15 +1,12 @@
import { ReactNode } from "react";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { AccessRestrictedBanner } from "@app/components/v2";
type Props = {
containerClassName?: string;
className?: string;
children?: ReactNode;
};
export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => {
export const PermissionDeniedBanner = ({ containerClassName }: Props) => {
return (
<div
className={twMerge(
@@ -17,22 +14,7 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
containerClassName
)}
>
<div className={twMerge("rounded-md bg-mineshaft-800 p-16 text-bunker-300", className)}>
<div className="flex items-end space-x-12">
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
{children || (
<div className="text-sm">
Your role has limited permissions, please <br /> contact your administrator to gain
access
</div>
)}
</div>
</div>
</div>
<AccessRestrictedBanner />
</div>
);
};
@@ -1,9 +1,8 @@
import { FunctionComponent, ReactNode } from "react";
import { AbilityTuple, MongoAbility } from "@casl/ability";
import { Can } from "@casl/react";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AccessRestrictedBanner } from "@app/components/v2";
import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext";
import { Tooltip } from "../v2/Tooltip";
@@ -11,17 +10,7 @@ import { Tooltip } from "../v2/Tooltip";
export const ProjectPermissionGuardBanner = () => {
return (
<div className="container mx-auto flex h-full items-center justify-center">
<div className="flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300">
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
<div className="text-sm">
Your role has limited permissions, please <br /> contact your admin to gain access
</div>
</div>
</div>
<AccessRestrictedBanner />
</div>
);
};
@@ -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: "Certificates 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"
@@ -202,6 +271,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
value={value}
onValueChange={onChange}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
>
{projectTemplates.length
? projectTemplates.map((template) => (
@@ -237,6 +308,9 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
onChange(e);
}}
className="mb-12 w-full bg-mineshaft-600"
position="popper"
dropdownContainerClassName="max-w-none -top-1"
side="top"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
@@ -76,7 +76,6 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
</div>
)
}
onPointerDownOutside={(e) => e.preventDefault()}
className={selectedRotation ? "max-w-2xl" : "max-w-3xl"}
subTitle={
selectedRotation ? undefined : "Select a provider to create a secret rotation for."
@@ -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>
</>
}
/>
);
};
@@ -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");
}
@@ -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>
)}
/>
);
};
@@ -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 = () => {
@@ -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>
</>
);
};
@@ -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 = () => {
@@ -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} />;
};
@@ -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() })
)
@@ -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);
@@ -75,7 +75,6 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
</div>
)
}
onPointerDownOutside={(e) => e.preventDefault()}
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
subTitle={
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
@@ -56,7 +56,6 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
"Add Sync"
)
}
onPointerDownOutside={(e) => e.preventDefault()}
className="max-w-2xl"
bodyClassName="overflow-visible"
subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."}
@@ -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>
)}
/>
</>
);
};
@@ -0,0 +1,63 @@
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 { useDigitalOceanConnectionListApps } from "@app/hooks/api/appConnections/digital-ocean";
import { TDigitalOceanApp } from "@app/hooks/api/appConnections/digital-ocean/types";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const DigitalOceanAppPlatformSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.DigitalOceanAppPlatform }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: apps = [], isPending: isAccountsLoading } = useDigitalOceanConnectionListApps(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.appId", "");
setValue("destinationConfig.appName", "");
}}
/>
<Controller
name="destinationConfig.appId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Select an app"
tooltipClassName="max-w-md"
>
<FilterableSelect
isLoading={isAccountsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={apps.find((p) => p.id === value) ?? null}
onChange={(option) => {
const v = option as SingleValue<TDigitalOceanApp>;
onChange(v?.id ?? null);
setValue("destinationConfig.appName", v?.spec.name ?? "");
}}
options={apps}
placeholder="Select an app..."
getOptionLabel={(option) => option.spec.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
</>
);
};
@@ -9,7 +9,7 @@ import {
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync";
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
import { TSecretSyncForm } from "../schemas";
@@ -9,11 +9,13 @@ 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";
import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields";
import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { DigitalOceanAppPlatformSyncFields } from "./DigitalOceanAppPlatformSyncFields";
import { FlyioSyncFields } from "./FlyioSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
@@ -91,6 +93,10 @@ export const SecretSyncDestinationFields = () => {
return <ChecklySyncFields />;
case SecretSync.Supabase:
return <SupabaseSyncFields />;
case SecretSync.DigitalOceanAppPlatform:
return <DigitalOceanAppPlatformSyncFields />;
case SecretSync.Bitbucket:
return <BitbucketSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,40 @@
import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Switch, Tooltip } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const RenderSyncOptionsFields = () => {
const { control } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
return (
<Controller
name="syncOptions.autoRedeployServices"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl className="mt-4" isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="auto-redeploy-services"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Auto Redeploy Services On Sync
<Tooltip
className="max-w-md"
content={
<p>If enabled, services will be automatically redeployed upon secret changes.</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</Switch>
</FormControl>
)}
/>
);
};
@@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields";
type Props = {
hideInitialSync?: boolean;
@@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
break;
case SecretSync.Render:
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsFields />;
break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault:
@@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.OnePass:
case SecretSync.OCIVault:
case SecretSync.Heroku:
case SecretSync.Render:
case SecretSync.Flyio:
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
@@ -63,6 +66,8 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Railway:
case SecretSync.Checkly:
case SecretSync.Supabase:
case SecretSync.DigitalOceanAppPlatform:
case SecretSync.Bitbucket:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -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>
</>
);
};
@@ -0,0 +1,14 @@
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 DigitalOceanAppPlatformSyncReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.DigitalOceanAppPlatform }
>();
const appName = watch("destinationConfig.appName");
return <GenericFieldLabel label="App">{appName}</GenericFieldLabel>;
};
@@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form";
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";
export const RenderSyncOptionsReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
const [{ autoRedeployServices }] = watch(["syncOptions"]);
return (
<div>
{autoRedeployServices ? (
<GenericFieldLabel label="Auto Redeploy Services">
<Badge variant="success">Enabled</Badge>
</GenericFieldLabel>
) : (
<GenericFieldLabel label="Auto Redeploy Services">
<Badge variant="danger">Disabled</Badge>
</GenericFieldLabel>
)}
</div>
);
};
export const RenderSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
const serviceName = watch("destinationConfig.serviceName");
@@ -18,11 +18,13 @@ 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";
import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields";
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { DigitalOceanAppPlatformSyncReviewFields } from "./DigitalOceanAppPlatformSyncReviewFields";
import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
@@ -33,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields";
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
@@ -119,6 +121,7 @@ export const SecretSyncReviewFields = () => {
break;
case SecretSync.Render:
DestinationFieldsComponent = <RenderSyncReviewFields />;
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsReviewFields />;
break;
case SecretSync.Flyio:
DestinationFieldsComponent = <FlyioSyncReviewFields />;
@@ -144,6 +147,12 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Supabase:
DestinationFieldsComponent = <SupabaseSyncReviewFields />;
break;
case SecretSync.DigitalOceanAppPlatform:
DestinationFieldsComponent = <DigitalOceanAppPlatformSyncReviewFields />;
break;
case SecretSync.Bitbucket:
DestinationFieldsComponent = <BitbucketSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -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")
})
})
);
@@ -0,0 +1,20 @@
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 DigitalOceanAppPlatformSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.DigitalOceanAppPlatform),
destinationConfig: z.object({
appId: z
.string()
.min(1, "App ID is required")
.max(255, "App ID must be less than 255 characters"),
appName: z
.string()
.min(1, "Account Name is required")
.max(255, "App Name must be less than 255 characters")
})
})
);
@@ -2,9 +2,13 @@ import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync";
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge(
export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
z.object({
autoRedeployServices: z.boolean().optional()
})
).merge(
z.object({
destination: z.literal(SecretSync.Render),
destinationConfig: z.discriminatedUnion("scope", [
@@ -6,11 +6,13 @@ 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";
import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema";
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
import { DigitalOceanAppPlatformSyncDestinationSchema } from "./digital-ocean-app-platform-sync-destination-schema";
import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema";
@@ -55,7 +57,9 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
SupabaseSyncDestinationSchema,
ZabbixSyncDestinationSchema,
RailwaySyncDestinationSchema,
ChecklySyncDestinationSchema
ChecklySyncDestinationSchema,
DigitalOceanAppPlatformSyncDestinationSchema,
BitbucketSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;
@@ -0,0 +1,26 @@
import { ReactNode } from "react";
type Props = {
title?: string;
body?: ReactNode;
};
export const AccessRestrictedBanner = ({
title = "Access Restricted",
body = (
<>
Your current role doesn&#39;t provide access to this feature.
<br /> Contact your administrator to request access.
</>
)
}: Props) => {
return (
<div className="flex items-center rounded-md border border-mineshaft-500 bg-gradient-to-br from-mineshaft-900 to-mineshaft-600 px-16 py-12 text-center text-bunker-300">
<div>
<div className="text-4xl font-medium text-bunker-100">{title}</div>
<div className="-mt-1 text-sm">{body}</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./AccessRestrictedBanner";
@@ -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";
@@ -18,6 +20,33 @@ export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
popUpProps: PopoverProps;
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/
@@ -27,12 +56,17 @@ export const DatePicker = ({
popUpProps,
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) {
@@ -57,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
@@ -72,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",
@@ -85,14 +126,16 @@ export const DatePicker = ({
}}
/>
</div>
<div className="mx-4 my-4">
<Input
type="time"
value={timeValue}
onChange={handleTimeChange}
className="bg-mineshaft-700 text-white [color-scheme:dark]"
/>
</div>
{!hideTime && (
<div className="mx-4 my-4">
<Input
type="time"
value={displayUtc ? localTimeToUTC(timeValue) : timeValue}
onChange={handleTimeChange}
className="bg-mineshaft-700 text-white [color-scheme:dark]"
/>
</div>
)}
</PopoverContent>
</Popover>
);
+6 -1
View File
@@ -19,6 +19,8 @@ type Props = {
icon?: IconProp;
isMulti?: boolean;
iconClassName?: string;
dropdownContainerStyle?: React.CSSProperties;
side?: SelectPrimitive.SelectContentProps["side"];
};
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
@@ -35,6 +37,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
position,
containerClassName,
iconClassName,
dropdownContainerStyle,
side,
...props
},
ref
@@ -76,13 +80,14 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
side={side}
className={twMerge(
"relative top-1 z-[100] max-w-sm overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
position === "popper" && "max-h-72",
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">
+1
View File
@@ -1,4 +1,5 @@
/* eslint-disable react-refresh/only-export-components */
export * from "./AccessRestrictedBanner";
export * from "./Accordion";
export * from "./Alert";
export * from "./Badge";
+132 -132
View File
@@ -70,276 +70,276 @@ export const ROUTE_PATHS = Object.freeze({
},
SecretManager: {
ApprovalPage: setRoute(
"/projects/$projectId/secret-manager/approval",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/approval"
"/projects/secret-management/$projectId/approval",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval"
),
SecretDashboardPage: setRoute(
"/projects/$projectId/secret-manager/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/secrets/$envSlug"
"/projects/secret-management/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug"
),
RollbackPreviewPage: setRoute(
"/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId/restore",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
"/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
),
CommitDetailsPage: setRoute(
"/projects/$projectId/secret-manager/commits/$environment/$folderId/$commitId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId/$commitId"
"/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
),
CommitsPage: setRoute(
"/projects/$projectId/secret-manager/commits/$environment/$folderId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/commits/$environment/$folderId"
"/projects/secret-management/$projectId/commits/$environment/$folderId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId"
),
OverviewPage: setRoute(
"/projects/$projectId/secret-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview"
"/projects/secret-management/$projectId/overview",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview"
),
IntegrationsListPage: setRoute(
"/projects/$projectId/secret-manager/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/"
"/projects/secret-management/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/"
),
IntegrationDetailsByIDPage: setRoute(
"/projects/$projectId/secret-manager/integrations/$integrationId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/$integrationId"
"/projects/secret-management/$projectId/integrations/$integrationId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId"
),
SecretSyncDetailsByIDPage: setRoute(
"/projects/$projectId/secret-manager/integrations/secret-syncs/$destination/$syncId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
"/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
),
Integratons: {
SelectIntegrationAuth: setRoute(
"/projects/$projectId/secret-manager/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/select-integration-auth"
"/projects/secret-management/$projectId/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth"
),
HerokuOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/oauth2/callback"
"/projects/secret-management/$projectId/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
),
HerokuConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/heroku/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/heroku/create"
"/projects/secret-management/$projectId/integrations/heroku/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/create"
),
AwsParameterStoreConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-parameter-store/create"
"/projects/secret-management/$projectId/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
),
AwsSecretManagerConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/aws-secret-manager/create"
"/projects/secret-management/$projectId/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
),
AzureAppConfigurationsOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
"/projects/secret-management/$projectId/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
),
AzureAppConfigurationsConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-app-configuration/create"
"/projects/secret-management/$projectId/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
),
AzureDevopsConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-devops/create"
"/projects/secret-management/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/create"
),
AzureKeyVaultAuthorizePage: setRoute(
"/projects/$projectId/secret-manager/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/authorize"
"/projects/secret-management/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
),
AzureKeyVaultOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
"/projects/secret-management/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
),
AzureKeyVaultConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/azure-key-vault/create"
"/projects/secret-management/$projectId/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
),
BitbucketOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
"/projects/secret-management/$projectId/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
),
BitbucketConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/bitbucket/create"
"/projects/secret-management/$projectId/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/create"
),
ChecklyConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/checkly/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/checkly/create"
"/projects/secret-management/$projectId/integrations/checkly/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/create"
),
CircleConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/circleci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/circleci/create"
"/projects/secret-management/$projectId/integrations/circleci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/create"
),
CloudflarePagesConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-pages/create"
"/projects/secret-management/$projectId/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
),
DigitalOceanAppPlatformConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
"/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
),
CloudflareWorkersConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloudflare-workers/create"
"/projects/secret-management/$projectId/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
),
CodefreshConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/codefresh/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/codefresh/create"
"/projects/secret-management/$projectId/integrations/codefresh/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/codefresh/create"
),
GcpSecretManagerConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/create"
"/projects/secret-management/$projectId/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
),
GcpSecretManagerOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
"/projects/secret-management/$projectId/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
),
GithubConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/github/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/create"
"/projects/secret-management/$projectId/integrations/github/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/create"
),
GithubOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/github/oauth2/callback"
"/projects/secret-management/$projectId/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
),
GitlabConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/gitlab/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/create"
"/projects/secret-management/$projectId/integrations/gitlab/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/create"
),
GitlabOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/gitlab/oauth2/callback"
"/projects/secret-management/$projectId/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
),
VercelOauthCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/oauth2/callback"
"/projects/secret-management/$projectId/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
),
VercelConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/vercel/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/vercel/create"
"/projects/secret-management/$projectId/integrations/vercel/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/create"
),
FlyioConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/flyio/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/flyio/create"
"/projects/secret-management/$projectId/integrations/flyio/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/flyio/create"
),
HashicorpVaultConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hashicorp-vault/create"
"/projects/secret-management/$projectId/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
),
HasuraCloudConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/hasura-cloud/create"
"/projects/secret-management/$projectId/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
),
LaravelForgeConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/laravel-forge/create"
"/projects/secret-management/$projectId/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
),
NorthflankConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/northflank/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/northflank/create"
"/projects/secret-management/$projectId/integrations/northflank/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/northflank/create"
),
RailwayConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/railway/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/railway/create"
"/projects/secret-management/$projectId/integrations/railway/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/railway/create"
),
RenderConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/render/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/render/create"
"/projects/secret-management/$projectId/integrations/render/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/render/create"
),
RundeckConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/rundeck/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/rundeck/create"
"/projects/secret-management/$projectId/integrations/rundeck/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/rundeck/create"
),
WindmillConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/windmill/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/windmill/create"
"/projects/secret-management/$projectId/integrations/windmill/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/windmill/create"
),
TravisCIConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/travisci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/travisci/create"
"/projects/secret-management/$projectId/integrations/travisci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/travisci/create"
),
TerraformCloudConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/terraform-cloud/create"
"/projects/secret-management/$projectId/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
),
TeamcityConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/teamcity/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/teamcity/create"
"/projects/secret-management/$projectId/integrations/teamcity/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/teamcity/create"
),
SupabaseConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/supabase/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/supabase/create"
"/projects/secret-management/$projectId/integrations/supabase/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/supabase/create"
),
OctopusDeployCloudConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/octopus-deploy/create"
"/projects/secret-management/$projectId/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
),
DatabricksConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/databricks/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/databricks/create"
"/projects/secret-management/$projectId/integrations/databricks/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/databricks/create"
),
QoveryConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/qovery/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/qovery/create"
"/projects/secret-management/$projectId/integrations/qovery/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/qovery/create"
),
Cloud66ConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/cloud-66/create"
"/projects/secret-management/$projectId/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/create"
),
NetlifyConfigurePage: setRoute(
"/projects/$projectId/secret-manager/integrations/netlify/create",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/create"
"/projects/secret-management/$projectId/integrations/netlify/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/create"
),
NetlifyOuathCallbackPage: setRoute(
"/projects/$projectId/secret-manager/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/integrations/netlify/oauth2/callback"
"/projects/secret-management/$projectId/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
)
}
},
CertManager: {
CertAuthDetailsByIDPage: setRoute(
"/projects/$projectId/cert-manager/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName"
"/projects/cert-management/$projectId/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName"
),
SubscribersPage: setRoute(
"/projects/$projectId/cert-manager/subscribers",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers"
"/projects/cert-management/$projectId/subscribers",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
),
CertificatesPage: setRoute(
"/projects/$projectId/cert-manager/certificates",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates"
"/projects/cert-management/$projectId/certificates",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
),
CertificateAuthoritiesPage: setRoute(
"/projects/$projectId/cert-manager/certificate-authorities",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities"
"/projects/cert-management/$projectId/certificate-authorities",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
),
AlertingPage: setRoute(
"/projects/$projectId/cert-manager/alerting",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting"
"/projects/cert-management/$projectId/alerting",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting"
),
PkiCollectionDetailsByIDPage: setRoute(
"/projects/$projectId/cert-manager/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId"
"/projects/cert-management/$projectId/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId"
),
PkiSubscriberDetailsByIDPage: setRoute(
"/projects/$projectId/cert-manager/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName"
"/projects/cert-management/$projectId/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
)
},
Ssh: {
SshCaByIDPage: setRoute(
"/projects/$projectId/ssh/ca/$caId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ca/$caId"
"/projects/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ca/$caId"
),
SshHostGroupDetailsByIDPage: setRoute(
"/projects/$projectId/ssh/ssh-host-groups/$sshHostGroupId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/ssh/_ssh-layout/ssh-host-groups/$sshHostGroupId"
"/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId",
"/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
)
},
SecretScanning: {
DataSourceByIdPage: setRoute(
"/projects/$projectId/secret-scanning/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/data-sources/$type/$dataSourceId"
"/projects/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
),
FindingsPage: setRoute(
"/projects/$projectId/secret-scanning/findings",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-scanning/_secret-scanning-layout/findings"
"/projects/secret-scanning/$projectId/findings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings"
)
},
Public: {
+12 -2
View File
@@ -30,6 +30,7 @@ import {
LdapConnectionMethod,
MsSqlConnectionMethod,
MySqlConnectionMethod,
OktaConnectionMethod,
OnePassConnectionMethod,
OracleDBConnectionMethod,
PostgresConnectionMethod,
@@ -42,6 +43,7 @@ import {
} from "@app/hooks/api/appConnections/types";
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
@@ -98,7 +100,12 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
[AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" },
[AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" }
[AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" },
[AppConnection.DigitalOcean]: {
name: "Digital Ocean",
image: "Digital Ocean.png"
},
[AppConnection.Okta]: { name: "Okta", image: "Okta.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -132,6 +139,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case CloudflareConnectionMethod.ApiToken:
case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
case DigitalOceanConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
@@ -162,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case RenderConnectionMethod.ApiKey:
case ChecklyConnectionMethod.ApiKey:
return { name: "API Key", icon: faKey };
case AzureClientSecretsConnectionMethod.ClientSecret:
return { name: "Client Secret", icon: faKey };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}
+24
View File
@@ -0,0 +1,24 @@
import { format } from "date-fns";
export enum Timezone {
Local = "local",
UTC = "UTC"
}
export const formatDateTime = ({
timezone,
timestamp,
dateFormat = "MMM do yyyy, hh:mm a"
}: {
timestamp: string | Date;
timezone?: Timezone;
dateFormat?: string;
}) => {
const date = new Date(timestamp);
if (timezone === Timezone.UTC) {
const utcDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000);
return `${format(utcDate, dateFormat)}`;
}
return format(date, dateFormat);
};
+29 -8
View File
@@ -42,7 +42,8 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
const {
data: { project }
} = await createWorkspace({
projectName
projectName,
type: ProjectType.SecretManager
});
try {
@@ -59,20 +60,34 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
return project;
};
export const getProjectBaseURL = (type: ProjectType) => {
switch (type) {
case ProjectType.SecretManager:
return "/projects/secret-management/$projectId";
case ProjectType.CertificateManager:
return "/projects/cert-management/$projectId";
default:
return `/projects/${type}/$projectId` as const;
}
};
export const getProjectHomePage = (type: ProjectType) => {
switch (type) {
case ProjectType.SecretManager:
return "/projects/secret-management/$projectId/overview";
case ProjectType.CertificateManager:
return `/projects/$projectId/${type}/subscribers` as const;
return "/projects/cert-management/$projectId/subscribers";
case ProjectType.SecretScanning:
return `/projects/$projectId/${type}/data-sources` as const;
return `/projects/${type}/$projectId/data-sources` as const;
default:
return `/projects/$projectId/${type}/overview` as const;
return `/projects/${type}/$projectId/overview` as const;
}
};
export const getProjectTitle = (type: ProjectType) => {
const titleConvert = {
[ProjectType.SecretManager]: "Secret Management",
[ProjectType.SecretManager]: "Secrets Management",
[ProjectType.KMS]: "Key Management",
[ProjectType.CertificateManager]: "Cert Management",
[ProjectType.SSH]: "SSH",
@@ -81,7 +96,13 @@ export const getProjectTitle = (type: ProjectType) => {
return titleConvert[type];
};
export const getCurrentProductFromUrl = (location: string) => {
const type = Object.values(ProjectType).find((el) => location.includes(`/${el}`));
return type;
export const getProjectLottieIcon = (type: ProjectType) => {
const titleConvert = {
[ProjectType.SecretManager]: "vault",
[ProjectType.KMS]: "unlock",
[ProjectType.CertificateManager]: "note",
[ProjectType.SSH]: "terminal",
[ProjectType.SecretScanning]: "secret-scan"
};
return titleConvert[type];
};
+9 -2
View File
@@ -44,6 +44,11 @@ export const SECRET_ROTATION_MAP: Record<
name: "AWS IAM User Secret",
image: "Amazon Web Services.png",
size: 50
},
[SecretRotation.OktaClientSecret]: {
name: "Okta Client Secret",
image: "Okta.png",
size: 50
}
};
@@ -55,7 +60,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.OktaClientSecret]: AppConnection.Okta
};
// if a rotation can potentially have downtime due to rotating a single credential set this to false
@@ -67,7 +73,8 @@ export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
[SecretRotation.Auth0ClientSecret]: false,
[SecretRotation.AzureClientSecret]: true,
[SecretRotation.LdapPassword]: false,
[SecretRotation.AwsIamUserSecret]: true
[SecretRotation.AwsIamUserSecret]: true,
[SecretRotation.OktaClientSecret]: true
};
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
+12 -2
View File
@@ -4,9 +4,9 @@ import {
SecretSyncImportBehavior,
SecretSyncInitialSyncBehavior
} from "@app/hooks/api/secretSyncs";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/render-sync";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
@@ -101,6 +101,14 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.Supabase]: {
name: "Supabase",
image: "Supabase.png"
},
[SecretSync.DigitalOceanAppPlatform]: {
name: "Digital Ocean App Platform",
image: "Digital Ocean.png"
},
[SecretSync.Bitbucket]: {
name: "Bitbucket",
image: "Bitbucket.png"
}
};
@@ -131,7 +139,9 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Supabase]: AppConnection.Supabase,
[SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway,
[SecretSync.Checkly]: AppConnection.Checkly
[SecretSync.Checkly]: AppConnection.Checkly,
[SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean,
[SecretSync.Bitbucket]: AppConnection.Bitbucket
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
@@ -1,9 +1,8 @@
import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { AccessRestrictedBanner } from "@app/components/v2";
import { TOrgPermission, useOrgPermission } from "@app/context";
type Props<T extends Abilities> = (T extends AbilityTuple
@@ -14,11 +13,11 @@ type Props<T extends Abilities> = (T extends AbilityTuple
: {
action: string;
subject: string;
}) & { className?: string; containerClassName?: string };
}) & { containerClassName?: string };
export const withPermission = <T extends object, J extends TOrgPermission>(
Component: ComponentType<T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
{ action, subject, containerClassName }: Props<Generics<J>["abilities"]>
) => {
const HOC = (hocProps: T) => {
const { permission } = useOrgPermission();
@@ -33,22 +32,7 @@ export const withPermission = <T extends object, J extends TOrgPermission>(
containerClassName
)}
>
<div
className={twMerge(
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
<div className="text-sm">
Your role has limited permissions, please <br /> contact your admin to gain access
</div>
</div>
</div>
<AccessRestrictedBanner />
</div>
);
}
@@ -1,14 +1,12 @@
import { ComponentType } from "react";
import { AbilityTuple } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { AccessRestrictedBanner } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
type Props<T extends AbilityTuple> = {
className?: string;
containerClassName?: string;
action: T[0];
subject: T[1];
@@ -16,7 +14,7 @@ type Props<T extends AbilityTuple> = {
export const withProjectPermission = <T extends object>(
Component: ComponentType<Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T>,
{ action, subject, className, containerClassName }: Props<ProjectPermissionSet>
{ action, subject, containerClassName }: Props<ProjectPermissionSet>
) => {
const HOC = (hocProps: Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T) => {
const { permission } = useProjectPermission();
@@ -31,23 +29,7 @@ export const withProjectPermission = <T extends object>(
containerClassName
)}
>
<div
className={twMerge(
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="mb-2 text-4xl font-medium">Permission Denied</div>
<div className="text-sm">
You do not have permission to this page. <br /> Kindly contact your organization
administrator
</div>
</div>
</div>
<AccessRestrictedBanner />
</div>
);
}
@@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => {
return useMutation<object, object, TCreateAccessPolicyDTO>({
mutationFn: async ({
environment,
environments,
projectSlug,
approvals,
approvers,
@@ -29,7 +29,7 @@ export const useCreateAccessApprovalPolicy = () => {
approvalsRequired
}) => {
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
environment,
environments,
projectSlug,
approvals,
bypassers,
@@ -63,7 +63,8 @@ export const useUpdateAccessApprovalPolicy = () => {
secretPath,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
approvalsRequired,
environments
}) => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals,
@@ -73,7 +74,8 @@ export const useUpdateAccessApprovalPolicy = () => {
name,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
approvalsRequired,
environments
});
return data;
},
@@ -8,9 +8,8 @@ export type TAccessApprovalPolicy = {
name: string;
approvals: number;
secretPath: string;
envId: string;
workspace: string;
environment: WorkspaceEnv;
environments: WorkspaceEnv[];
projectId: string;
policyType: PolicyType;
approversRequired: boolean;
@@ -166,7 +165,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
export type TCreateAccessPolicyDTO = {
projectSlug: string;
name?: string;
environment: string;
environments: string[];
approvers?: Approver[];
bypassers?: Bypasser[];
approvals?: number;
@@ -182,7 +181,7 @@ export type TUpdateAccessPolicyDTO = {
approvers?: Approver[];
bypassers?: Bypasser[];
secretPath?: string;
environment?: string;
environments?: string[];
approvals?: number;
enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean;
+1
View File
@@ -51,6 +51,7 @@ export type TServerConfig = {
invalidatingCache: boolean;
fipsEnabled: boolean;
envOverrides?: Record<string, string>;
paramsFolderSecretDetectionEnabled: boolean;
};
export type TUpdateServerConfigDTO = {
@@ -4,8 +4,10 @@ import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import {
TBitbucketConnectionListEnvironmentsResponse,
TBitbucketConnectionListRepositoriesResponse,
TBitbucketConnectionListWorkspacesResponse,
TBitbucketEnvironment,
TBitbucketRepo,
TBitbucketWorkspace
} from "./types";
@@ -15,7 +17,9 @@ const bitbucketConnectionKeys = {
listRepos: (connectionId: string, workspaceSlug: string) =>
[...bitbucketConnectionKeys.all, "repos", connectionId, workspaceSlug] as const,
listWorkspaces: (connectionId: string) =>
[...bitbucketConnectionKeys.all, "workspaces", connectionId] as const
[...bitbucketConnectionKeys.all, "workspaces", connectionId] as const,
listEnvironments: (connectionId: string, workspaceSlug: string, repoSlug: string) =>
[...bitbucketConnectionKeys.all, "environments", connectionId, workspaceSlug, repoSlug] as const
};
export const useBitbucketConnectionListWorkspaces = (
@@ -68,3 +72,30 @@ export const useBitbucketConnectionListRepositories = (
...options
});
};
export const useBitbucketConnectionListEnvironments = (
connectionId: string,
workspaceSlug: string,
repoSlug: string,
options?: Omit<
UseQueryOptions<
TBitbucketEnvironment[],
unknown,
TBitbucketEnvironment[],
ReturnType<typeof bitbucketConnectionKeys.listEnvironments>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: bitbucketConnectionKeys.listEnvironments(connectionId, workspaceSlug, repoSlug),
queryFn: async () => {
const { data } = await apiRequest.get<TBitbucketConnectionListEnvironmentsResponse>(
`/api/v1/app-connections/bitbucket/${connectionId}/environments?workspaceSlug=${encodeURIComponent(workspaceSlug)}&repositorySlug=${encodeURIComponent(repoSlug)}`
);
return data.environments;
},
...options
});
};
@@ -15,3 +15,13 @@ export type TBitbucketConnectionListWorkspacesResponse = {
export type TBitbucketConnectionListRepositoriesResponse = {
repositories: TBitbucketRepo[];
};
export type TBitbucketEnvironment = {
uuid: string;
name: string;
slug: string;
};
export type TBitbucketConnectionListEnvironmentsResponse = {
environments: TBitbucketEnvironment[];
};
@@ -0,0 +1 @@
export * from "./queries";
@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import { TDigitalOceanApp } from "./types";
const digitalOceanAppPlatformConnectionKeys = {
all: [...appConnectionKeys.all, "digitalOceanAppPlatform"] as const,
listAccounts: (connectionId: string) =>
[...digitalOceanAppPlatformConnectionKeys.all, "workspace-scopes", connectionId] as const
};
export const useDigitalOceanConnectionListApps = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TDigitalOceanApp[],
unknown,
TDigitalOceanApp[],
ReturnType<typeof digitalOceanAppPlatformConnectionKeys.listAccounts>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: digitalOceanAppPlatformConnectionKeys.listAccounts(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ apps: TDigitalOceanApp[] }>(
`/api/v1/app-connections/digital-ocean/${connectionId}/apps`
);
return data.apps;
},
...options
});
};
@@ -0,0 +1,16 @@
export type TDigitalOceanAppPlatformVariable = {
key: string;
value: string;
type: "SECRET" | "GENERAL";
};
export type TDigitalOceanApp = {
id: string;
spec: {
name: string;
services: Array<{
name: string;
}>;
envs?: TDigitalOceanAppPlatformVariable[];
};
};
@@ -32,5 +32,7 @@ export enum AppConnection {
Zabbix = "zabbix",
Railway = "railway",
Checkly = "checkly",
Supabase = "supabase"
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Okta = "okta"
}
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,36 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TOktaApp } from "./types";
const oktaConnectionKeys = {
all: [...appConnectionKeys.all, "okta"] as const,
listApps: (connectionId: string) => [...oktaConnectionKeys.all, "apps", connectionId] as const
};
export const useOktaConnectionListApps = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TOktaApp[],
unknown,
TOktaApp[],
ReturnType<typeof oktaConnectionKeys.listApps>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: oktaConnectionKeys.listApps(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ apps: TOktaApp[] }>(
`/api/v1/app-connections/okta/${connectionId}/apps`
);
return data.apps;
},
...options
});
};
@@ -0,0 +1,4 @@
export type TOktaApp = {
id: string;
label: string;
};
@@ -152,6 +152,14 @@ export type TSupabaseConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Supabase;
};
export type TDigitalOceanConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.DigitalOcean;
};
export type TOktaConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Okta;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -183,7 +191,9 @@ export type TAppConnectionOption =
| TBitbucketConnectionOption
| TZabbixConnectionOption
| TRailwayConnectionOption
| TChecklyConnectionOption;
| TChecklyConnectionOption
| TDigitalOceanConnectionOption
| TOktaConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@@ -220,4 +230,6 @@ export type TAppConnectionOptionMap = {
[AppConnection.Railway]: TRailwayConnectionOption;
[AppConnection.Checkly]: TChecklyConnectionOption;
[AppConnection.Supabase]: TSupabaseConnectionOption;
[AppConnection.DigitalOcean]: TDigitalOceanConnectionOption;
[AppConnection.Okta]: TOktaConnectionOption;
};
@@ -2,15 +2,26 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum AzureClientSecretsConnectionMethod {
OAuth = "oauth"
OAuth = "oauth",
ClientSecret = "client-secret"
}
export type TAzureClientSecretsConnection = TRootAppConnection & {
app: AppConnection.AzureClientSecrets;
} & {
method: AzureClientSecretsConnectionMethod.OAuth;
credentials: {
code: string;
tenantId: string;
};
};
} & (
| {
method: AzureClientSecretsConnectionMethod.OAuth;
credentials: {
code: string;
tenantId: string;
};
}
| {
method: AzureClientSecretsConnectionMethod.ClientSecret;
credentials: {
clientSecret: string;
clientId: string;
tenantId: string;
};
}
);
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum DigitalOceanConnectionMethod {
ApiToken = "api-token"
}
export type TDigitalOceanConnection = TRootAppConnection & {
app: AppConnection.DigitalOcean;
method: DigitalOceanConnectionMethod.ApiToken;
credentials: {
apiToken: string;
};
};
@@ -12,6 +12,7 @@ import { TCamundaConnection } from "./camunda-connection";
import { TChecklyConnection } from "./checkly-connection";
import { TCloudflareConnection } from "./cloudflare-connection";
import { TDatabricksConnection } from "./databricks-connection";
import { TDigitalOceanConnection } from "./digital-ocean";
import { TFlyioConnection } from "./flyio-connection";
import { TGcpConnection } from "./gcp-connection";
import { TGitHubConnection } from "./github-connection";
@@ -24,6 +25,7 @@ import { TLdapConnection } from "./ldap-connection";
import { TMsSqlConnection } from "./mssql-connection";
import { TMySqlConnection } from "./mysql-connection";
import { TOCIConnection } from "./oci-connection";
import { TOktaConnection } from "./okta-connection";
import { TOracleDBConnection } from "./oracledb-connection";
import { TPostgresConnection } from "./postgres-connection";
import { TRailwayConnection } from "./railway-connection";
@@ -44,6 +46,7 @@ export * from "./azure-devops-connection";
export * from "./azure-key-vault-connection";
export * from "./bitbucket-connection";
export * from "./camunda-connection";
export * from "./checkly-connection";
export * from "./cloudflare-connection";
export * from "./databricks-connection";
export * from "./flyio-connection";
@@ -58,9 +61,12 @@ export * from "./ldap-connection";
export * from "./mssql-connection";
export * from "./mysql-connection";
export * from "./oci-connection";
export * from "./okta-connection";
export * from "./oracledb-connection";
export * from "./postgres-connection";
export * from "./railway-connection";
export * from "./render-connection";
export * from "./supabase-connection";
export * from "./teamcity-connection";
export * from "./terraform-cloud-connection";
export * from "./vercel-connection";
@@ -101,7 +107,9 @@ export type TAppConnection =
| TZabbixConnection
| TRailwayConnection
| TChecklyConnection
| TSupabaseConnection;
| TSupabaseConnection
| TDigitalOceanConnection
| TOktaConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -113,11 +121,20 @@ export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppCo
export type TCreateAppConnectionDTO = Pick<
TAppConnection,
"name" | "credentials" | "method" | "app" | "description" | "isPlatformManagedCredentials"
| "name"
| "credentials"
| "method"
| "app"
| "description"
| "isPlatformManagedCredentials"
| "gatewayId"
>;
export type TUpdateAppConnectionDTO = Partial<
Pick<TAppConnection, "name" | "credentials" | "description" | "isPlatformManagedCredentials">
Pick<
TAppConnection,
"name" | "credentials" | "description" | "isPlatformManagedCredentials" | "gatewayId"
>
> & {
connectionId: string;
app: AppConnection;
@@ -163,4 +180,6 @@ export type TAppConnectionMap = {
[AppConnection.Railway]: TRailwayConnection;
[AppConnection.Checkly]: TChecklyConnection;
[AppConnection.Supabase]: TSupabaseConnection;
[AppConnection.DigitalOcean]: TDigitalOceanConnection;
[AppConnection.Okta]: TOktaConnection;
};
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum OktaConnectionMethod {
ApiToken = "api-token"
}
export type TOktaConnection = TRootAppConnection & { app: AppConnection.Okta } & {
method: OktaConnectionMethod.ApiToken;
credentials: {
instanceUrl: string;
apiToken: string;
};
};
@@ -7,4 +7,5 @@ export type TRootAppConnection = {
createdAt: string;
updatedAt: string;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
};
@@ -57,6 +57,7 @@ export const useGetAuditLogs = (
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined,
placeholderData: (prev) => prev,
...options
});
};
@@ -1,8 +1,9 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { apiRequest } from "@app/config/request";
import { CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types";
import { Commit, CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types";
export const commitKeys = {
count: ({
@@ -242,7 +243,6 @@ export const useGetFolderCommitHistory = ({
workspaceId,
environment,
directory,
offset = 0,
limit = 20,
search,
sort = "desc"
@@ -250,22 +250,33 @@ export const useGetFolderCommitHistory = ({
workspaceId: string;
environment: string;
directory: string;
offset?: number;
limit?: number;
search?: string;
sort?: "asc" | "desc";
}) => {
return useQuery({
queryKey: [
commitKeys.history({ workspaceId, environment, directory }),
offset,
limit,
search,
sort
],
queryFn: () =>
fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort),
enabled: Boolean(workspaceId && environment)
return useInfiniteQuery({
initialPageParam: 0,
queryKey: [commitKeys.history({ workspaceId, environment, directory }), limit, search, sort],
queryFn: ({ pageParam }) =>
fetchFolderCommitHistory(workspaceId, environment, directory, pageParam, limit, search, sort),
enabled: Boolean(workspaceId && environment),
select: (data) => {
return (data?.pages ?? [])
?.map((page) => page.commits)
.flat()
.reduce(
(acc, commit) => {
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
if (!acc[date]) {
acc[date] = [];
}
acc[date].push(commit);
return acc;
},
{} as Record<string, Commit[]>
);
},
getNextPageParam: (lastPage, pages) => (lastPage.hasMore ? pages.length * limit : undefined)
});
};
@@ -62,3 +62,16 @@ export type RollbackPreview = {
folderPath: string;
changes: RollbackChange[];
};
interface CommitActorMetadata {
email?: string;
name?: string;
}
export interface Commit {
id: string;
message: string;
createdAt: string;
actorType: string;
actorMetadata?: CommitActorMetadata;
}
@@ -86,6 +86,7 @@ export const useCreateIdentity = () => {
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot });
}
});
};
@@ -110,6 +111,7 @@ export const useUpdateIdentity = () => {
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot });
}
});
};
@@ -130,6 +132,7 @@ export const useDeleteIdentity = () => {
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot });
}
});
};
@@ -25,7 +25,9 @@ import {
export const identitiesKeys = {
getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const,
searchIdentities: (dto: TSearchIdentitiesDTO) => ["identity", "search", dto] as const,
searchIdentitiesRoot: ["identity", "search"] as const,
searchIdentities: (dto: TSearchIdentitiesDTO) =>
[...identitiesKeys.searchIdentitiesRoot, dto] as const,
getIdentityUniversalAuth: (identityId: string) =>
[{ identityId }, "identity-universal-auth"] as const,
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
+1 -1
View File
@@ -51,7 +51,7 @@ export type IdentityMembershipOrg = {
export type IdentityMembership = {
id: string;
identity: Identity;
project: Pick<Workspace, "id" | "name" | "defaultProduct">;
project: Pick<Workspace, "id" | "name" | "type">;
roles: Array<
{
id: string;
@@ -0,0 +1 @@
export * from "./mutations";
+24 -1
View File
@@ -15,7 +15,7 @@ export const useImportEnvKey = () => {
formData.append("file", file);
try {
const response = await apiRequest.post("/api/v3/migrate/env-key/", formData, {
const response = await apiRequest.post("/api/v3/external-migration/env-key/", formData, {
headers: {
"Content-Type": "multipart/form-data"
},
@@ -39,3 +39,26 @@ export const useImportEnvKey = () => {
}
});
};
export const useImportVault = () => {
return useMutation({
mutationFn: async ({
vaultAccessToken,
vaultNamespace,
vaultUrl,
mappingType
}: {
vaultAccessToken: string;
vaultNamespace?: string;
vaultUrl: string;
mappingType: string;
}) => {
await apiRequest.post("/api/v3/external-migration/vault/", {
vaultAccessToken,
vaultNamespace,
vaultUrl,
mappingType
});
}
});
};
@@ -1,9 +1,12 @@
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "../workspace/types";
export type TProjectTemplate = {
id: string;
name: string;
description?: string;
type: ProjectType;
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
environments?: { name: string; slug: string; position: number }[] | null;
createdAt: string;
@@ -16,6 +19,7 @@ export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
export type TCreateProjectTemplateDTO = {
name: string;
description?: string;
type?: ProjectType;
};
export type TUpdateProjectTemplateDTO = Partial<
@@ -0,0 +1 @@
export { useCreateReminder, useDeleteReminder, useGetReminder } from "./queries";
@@ -0,0 +1,60 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { CreateReminderDTO, DeleteReminderDTO, Reminder } from "./types";
export const reminderKeys = {
getReminder: (secretId: string) => ["get-reminder", secretId] as const
};
export const useCreateReminder = (secretId: string) => {
const queryClient = useQueryClient();
return useMutation<Reminder, object, CreateReminderDTO>({
mutationFn: async ({ message, repeatDays, nextReminderDate, recipients }) => {
const { data } = await apiRequest.post<{ reminder: Reminder }>(
`/api/v1/reminders/secrets/${secretId}`,
{
message,
repeatDays,
nextReminderDate,
recipients
}
);
return data.reminder;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) });
}
});
};
export const useDeleteReminder = (secretId: string) => {
const queryClient = useQueryClient();
return useMutation<Reminder, object, DeleteReminderDTO>({
mutationFn: async () => {
const { data } = await apiRequest.delete<{ reminder: Reminder }>(
`/api/v1/reminders/secrets/${secretId}`
);
return data.reminder;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) });
}
});
};
export const useGetReminder = (secretId: string) => {
return useQuery({
queryKey: reminderKeys.getReminder(secretId),
queryFn: async () => {
const { data } = await apiRequest.get<{ reminder: Reminder }>(
`/api/v1/reminders/secrets/${secretId}`
);
return data.reminder;
},
enabled: Boolean(secretId)
});
};
+14
View File
@@ -0,0 +1,14 @@
export type CreateReminderDTO = {
message?: string | null;
repeatDays?: number | null;
nextReminderDate?: Date | null;
secretId: string;
recipients?: string[];
};
export type DeleteReminderDTO = {
secretId: string;
reminderId: string;
};
export type Reminder = { id: string } & CreateReminderDTO;
@@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => {
return useMutation<object, object, TCreateSecretPolicyDTO>({
mutationFn: async ({
environment,
environments,
workspaceId,
approvals,
approvers,
@@ -21,7 +21,7 @@ export const useCreateSecretApprovalPolicy = () => {
allowedSelfApprovals
}) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment,
environments,
workspaceId,
approvals,
approvers,
@@ -53,7 +53,8 @@ export const useUpdateSecretApprovalPolicy = () => {
secretPath,
name,
enforcementLevel,
allowedSelfApprovals
allowedSelfApprovals,
environments
}) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals,
@@ -62,7 +63,8 @@ export const useUpdateSecretApprovalPolicy = () => {
secretPath,
name,
enforcementLevel,
allowedSelfApprovals
allowedSelfApprovals,
environments
});
return data;
},
@@ -5,8 +5,7 @@ export type TSecretApprovalPolicy = {
id: string;
workspace: string;
name: string;
envId: string;
environment: WorkspaceEnv;
environments: WorkspaceEnv[];
secretPath?: string;
approvals: number;
approvers: Approver[];
@@ -48,7 +47,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = {
export type TCreateSecretPolicyDTO = {
workspaceId: string;
name?: string;
environment: string;
environments: string[];
secretPath: string;
approvers?: Approver[];
bypassers?: Bypasser[];
@@ -68,6 +67,7 @@ export type TUpdateSecretPolicyDTO = {
enforcementLevel?: EnforcementLevel;
// for invalidating list
workspaceId: string;
environments?: string[];
};
export type TDeleteSecretPolicyDTO = {
@@ -4,11 +4,19 @@ export enum ReservedFolders {
SecretReplication = "__reserve_replication_"
}
export enum PendingAction {
Create = "create",
Update = "update",
Delete = "delete"
}
export type TSecretFolder = {
id: string;
name: string;
description?: string;
parentId?: string | null;
isPending?: boolean;
pendingAction?: PendingAction;
};
export type TSecretFolderWithPath = TSecretFolder & { path: string };
@@ -6,7 +6,8 @@ export enum SecretRotation {
Auth0ClientSecret = "auth0-client-secret",
AzureClientSecret = "azure-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret"
AwsIamUserSecret = "aws-iam-user-secret",
OktaClientSecret = "okta-client-secret"
}
export enum SecretRotationStatus {
@@ -35,6 +35,11 @@ import {
TMySqlCredentialsRotation,
TMySqlCredentialsRotationGeneratedCredentialsResponse
} from "./mysql-credentials-rotation";
import {
TOktaClientSecretRotation,
TOktaClientSecretRotationGeneratedCredentialsResponse,
TOktaClientSecretRotationOption
} from "./okta-client-secret-rotation";
import {
TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationGeneratedCredentialsResponse
@@ -49,6 +54,7 @@ export type TSecretRotationV2 = (
| TAzureClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation
| TOktaClientSecretRotation
) & {
secrets: (SecretV3RawSanitized | null)[];
};
@@ -58,7 +64,8 @@ export type TSecretRotationV2Option =
| TAuth0ClientSecretRotationOption
| TAzureClientSecretRotationOption
| TLdapPasswordRotationOption
| TAwsIamUserSecretRotationOption;
| TAwsIamUserSecretRotationOption
| TOktaClientSecretRotationOption;
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
@@ -72,7 +79,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse =
| TAuth0ClientSecretRotationGeneratedCredentialsResponse
| TAzureClientSecretRotationGeneratedCredentialsResponse
| TLdapPasswordRotationGeneratedCredentialsResponse
| TAwsIamUserSecretRotationGeneratedCredentialsResponse;
| TAwsIamUserSecretRotationGeneratedCredentialsResponse
| TOktaClientSecretRotationGeneratedCredentialsResponse;
export type TCreateSecretRotationV2DTO = DiscriminativePick<
TSecretRotationV2,
@@ -124,6 +132,7 @@ export type TSecretRotationOptionMap = {
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption;
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption;
};
export type TSecretRotationGeneratedCredentialsResponseMap = {
@@ -135,4 +144,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse;
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse;
};
@@ -0,0 +1,37 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import {
TSecretRotationV2Base,
TSecretRotationV2GeneratedCredentialsResponseBase
} from "@app/hooks/api/secretRotationsV2/types/shared";
export type TOktaClientSecretRotation = TSecretRotationV2Base & {
type: SecretRotation.OktaClientSecret;
parameters: {
clientId: string;
};
secretsMapping: {
clientId: string;
clientSecret: string;
};
};
export type TOktaClientSecretRotationGeneratedCredentials = {
clientId: string;
clientSecret: string;
};
export type TOktaClientSecretRotationGeneratedCredentialsResponse =
TSecretRotationV2GeneratedCredentialsResponseBase<
SecretRotation.OktaClientSecret,
TOktaClientSecretRotationGeneratedCredentials
>;
export type TOktaClientSecretRotationOption = {
name: string;
type: SecretRotation.OktaClientSecret;
connection: AppConnection.Okta;
template: {
secretsMapping: TOktaClientSecretRotation["secretsMapping"];
};
};
+3 -1
View File
@@ -25,7 +25,9 @@ export enum SecretSync {
Supabase = "supabase",
Zabbix = "zabbix",
Railway = "railway",
Checkly = "checkly"
Checkly = "checkly",
DigitalOceanAppPlatform = "digital-ocean-app-platform",
Bitbucket = "bitbucket"
}
export enum SecretSyncStatus {
@@ -0,0 +1,17 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TBitbucketSync = TRootSecretSync & {
destination: SecretSync.Bitbucket;
destinationConfig: {
workspaceSlug: string;
repositorySlug: string;
environmentId?: string;
};
connection: {
app: AppConnection.Bitbucket;
name: string;
id: string;
};
};
@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TDigitalOceanAppPlatformSync = TRootSecretSync & {
destination: SecretSync.DigitalOceanAppPlatform;
destinationConfig: {
appId: string;
appName: string;
};
connection: {
app: AppConnection.DigitalOcean;
name: string;
id: string;
};
};
@@ -1,18 +1,19 @@
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
import { DiscriminativePick } from "@app/types";
import { TRenderSync } from "../render-sync";
import { TOnePassSync } from "./1password-sync";
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync";
import { TAzureDevOpsSync } from "./azure-devops-sync";
import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
import { TBitbucketSync } from "./bitbucket-sync";
import { TCamundaSync } from "./camunda-sync";
import { TChecklySync } from "./checkly-sync";
import { TCloudflarePagesSync } from "./cloudflare-pages-sync";
import { TCloudflareWorkersSync } from "./cloudflare-workers-sync";
import { TDatabricksSync } from "./databricks-sync";
import { TDigitalOceanAppPlatformSync } from "./digital-ocean-app-platform-sync";
import { TFlyioSync } from "./flyio-sync";
import { TGcpSync } from "./gcp-sync";
import { TGitHubSync } from "./github-sync";
@@ -22,6 +23,7 @@ import { THerokuSync } from "./heroku-sync";
import { THumanitecSync } from "./humanitec-sync";
import { TOCIVaultSync } from "./oci-vault-sync";
import { TRailwaySync } from "./railway-sync";
import { TRenderSync } from "./render-sync";
import { TSupabaseSync } from "./supabase";
import { TTeamCitySync } from "./teamcity-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync";
@@ -63,7 +65,9 @@ export type TSecretSync =
| TZabbixSync
| TRailwaySync
| TChecklySync
| TSupabaseSync;
| TSupabaseSync
| TDigitalOceanAppPlatformSync
| TBitbucketSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
@@ -1,6 +1,6 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TRenderSync = TRootSecretSync & {
destination: SecretSync.Render;
@@ -16,6 +16,10 @@ export type TRenderSync = TRootSecretSync & {
name: string;
id: string;
};
syncOptions: RootSyncOptions & {
autoRedeployServices?: boolean;
};
};
export enum RenderSyncScope {
@@ -2,9 +2,14 @@ import { MutationOptions, useMutation, useQueryClient } from "@tanstack/react-qu
import { apiRequest } from "@app/config/request";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import {
PendingChanges,
PendingSecretUpdate
} from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.store";
import { commitKeys } from "../folderCommits/queries";
import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries";
import { PendingAction } from "../secretFolders/types";
import { secretSnapshotKeys } from "../secretSnapshots/queries";
import { secretKeys } from "./queries";
import {
@@ -420,3 +425,102 @@ export const useBackfillSecretReference = () =>
return data.message;
}
});
export const useCreateCommit = () => {
const queryClient = useQueryClient();
return useMutation<
object,
object,
{
workspaceId: string;
environment: string;
secretPath: string;
pendingChanges: PendingChanges;
message: string;
}
>({
mutationFn: async ({ workspaceId, environment, secretPath, pendingChanges, message }) => {
const { data } = await apiRequest.post("/api/v1/pit/batch/commit", {
projectId: workspaceId,
environment,
secretPath,
changes: {
secrets: {
create:
pendingChanges.secrets
.filter((change) => change.type === PendingAction.Create)
.map((change) => ({
secretKey: change.secretKey,
secretValue: change.secretValue,
secretComment: change.secretComment,
skipMultilineEncoding: change.skipMultilineEncoding,
tagIds: change.tags?.map((tag) => tag.id),
secretMetadata: change.secretMetadata
})) || [],
update:
pendingChanges.secrets
.filter((change) => change.type === PendingAction.Update)
.map((change: PendingSecretUpdate) => ({
secretKey: change.secretKey,
newSecretName: change.newSecretName,
secretValue:
change.secretValue === ""
? ""
: change.secretValue || change.existingSecret.value,
secretComment:
change.secretComment === ""
? ""
: change.secretComment || change.existingSecret.comment,
skipMultilineEncoding:
change.skipMultilineEncoding !== undefined
? change.skipMultilineEncoding
: change.existingSecret.skipMultilineEncoding,
tagIds:
change.tags?.map((tag) => tag.id) ||
change.existingSecret.tags?.map((tag) => tag.id),
secretMetadata: change.secretMetadata || change.existingSecret.secretMetadata
})) || [],
delete:
pendingChanges.secrets.filter((change) => change.type === PendingAction.Delete) || []
},
folders: {
create:
pendingChanges.folders.filter((change) => change.type === PendingAction.Create) || [],
update:
pendingChanges.folders
.filter((change) => change.type === PendingAction.Update)
.map((change) => ({
...change,
description: change.description || null
})) || [],
delete:
pendingChanges.folders.filter((change) => change.type === PendingAction.Delete) || []
}
},
message
});
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
});
queryClient.invalidateQueries({
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
}
});
};
+3
View File
@@ -1,5 +1,6 @@
import { ProjectPermissionActions } from "@app/context";
import { PendingAction } from "../secretFolders/types";
import type { WsTag } from "../tags/types";
export enum SecretType {
@@ -66,6 +67,8 @@ export type SecretV3RawSanitized = {
isRotatedSecret?: boolean;
secretReminderRecipients?: SecretReminderRecipient[];
rotationId?: string;
isPending?: boolean;
pendingAction?: PendingAction;
};
export type SecretV3Raw = {
+6 -5
View File
@@ -255,12 +255,13 @@ export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, object, CreateWorkspaceDTO>({
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) =>
createWorkspace({
projectName,
projectDescription,
kmsKeyId,
template
template,
type
}),
onSuccess: () => {
queryClient.invalidateQueries({
@@ -281,7 +282,7 @@ export const useUpdateProject = () => {
newSlug,
secretSharing,
showSnapshotsLegacy,
defaultProduct
secretDetectionIgnoreValues
}) => {
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
`/api/v1/workspace/${projectID}`,
@@ -289,9 +290,9 @@ export const useUpdateProject = () => {
name: newProjectName,
description: newProjectDescription,
slug: newSlug,
defaultProduct,
secretSharing,
showSnapshotsLegacy
showSnapshotsLegacy,
secretDetectionIgnoreValues
}
);
return data.workspace;
+5 -2
View File
@@ -24,7 +24,7 @@ export type Workspace = {
__v: number;
id: string;
name: string;
defaultProduct: ProjectType;
type: ProjectType;
description?: string;
orgId: string;
version: ProjectVersion;
@@ -40,6 +40,7 @@ export type Workspace = {
hasDeleteProtection: boolean;
secretSharing: boolean;
showSnapshotsLegacy: boolean;
secretDetectionIgnoreValues: string[];
};
export type WorkspaceEnv = {
@@ -68,6 +69,7 @@ export type TGetUpgradeProjectStatusDTO = {
// mutation dto
export type CreateWorkspaceDTO = {
projectName: string;
type: ProjectType;
projectDescription?: string;
kmsKeyId?: string;
template?: string;
@@ -80,7 +82,7 @@ export type UpdateProjectDTO = {
newSlug?: string;
secretSharing?: boolean;
showSnapshotsLegacy?: boolean;
defaultProduct?: ProjectType;
secretDetectionIgnoreValues?: string[];
};
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
@@ -184,6 +186,7 @@ export type TSearchProjectsDTO = {
name?: string;
limit?: number;
offset?: number;
type?: ProjectType;
options?: { enabled?: boolean };
orderBy?: ProjectIdentityOrderBy;
orderDirection?: OrderByDirection;
@@ -0,0 +1,40 @@
import { useCallback } from "react";
import { useBlocker } from "@tanstack/react-router";
import {
BatchContext,
useBatchModeActions
} from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.store";
type TNavigationBlockerReturn = {
isBlocked: boolean;
};
export const useNavigationBlocker = ({
shouldBlock = false,
message = "Are you sure you want to leave? You may have unsaved changes.",
context
}: {
shouldBlock: boolean;
message: string;
context: BatchContext;
}): TNavigationBlockerReturn => {
const { clearAllPendingChanges } = useBatchModeActions();
const blockerFn = useCallback(() => {
if (!shouldBlock) return false;
// eslint-disable-next-line no-alert
const confirmed = window.confirm(message);
if (confirmed) {
clearAllPendingChanges(context);
}
return !confirmed;
}, [shouldBlock, message, context]);
useBlocker(blockerFn, shouldBlock);
return {
isBlocked: shouldBlock
};
};
+2 -1
View File
@@ -49,7 +49,8 @@ export const usePathAccessPolicies = ({ secretPath, environment }: Params) => {
return useMemo(() => {
const pathPolicies = policies?.filter(
(policy) =>
policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath)
policy.environments?.some((env) => env.slug === environment) &&
matchesPath(secretPath, policy.secretPath)
);
return {
@@ -0,0 +1,71 @@
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
type Params = {
minWidth: number;
maxWidth: number;
initialWidth: number;
};
export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => {
const [colWidth, setColWidth] = useState(initialWidth);
const [isResizing, setIsResizing] = useState(false);
const startX = useRef(0);
const startWidth = useRef(0);
const handleMouseDown = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
startX.current = e.clientX;
startWidth.current = colWidth;
},
[colWidth]
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isResizing) return;
const deltaX = e.clientX - startX.current;
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + deltaX));
setColWidth(newWidth);
},
[isResizing]
);
const handleMouseUp = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
if (isResizing) {
document.addEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.addEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "ew-resize";
document.body.style.userSelect = "none";
}
return () => {
document.removeEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, handleMouseMove, handleMouseUp]);
return {
colWidth,
handleMouseDown,
isResizing
};
};
+116 -28
View File
@@ -1,11 +1,16 @@
import { faBook, faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
export const KmsLayout = () => {
const { currentWorkspace } = useWorkspace();
const { assumedPrivilegeDetails } = useProjectPermission();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
@@ -19,40 +24,123 @@ export const KmsLayout = () => {
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">KMS</div>
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="unlock" />
KMS
</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/kms/overview"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Overview</MenuItem>}
<MenuGroup title="Resources">
<Link
to="/projects/kms/$projectId/overview"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCube} />
</div>
Overview
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/kmip"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faLock} />
</div>
KMIP
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/kms/$projectId/access-management"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Access Management
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/audit-logs"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link>
<Link
to="/projects/$projectId/kms/kmip"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>KMIP</MenuItem>}
</Link>
{
// <Link
// to="/projects/$projectId/kms/settings"
// params={{
// projectId: currentWorkspace.id
// }}
// >
// {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
// </Link>
}
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
@@ -1,12 +1,12 @@
import {
faBook,
faCog,
faCubes,
faDoorClosed,
faInfinity,
faMoneyBill,
faPlug,
faShare,
faTable,
faUserCog,
faUsers,
faUserTie
@@ -56,7 +56,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCubes} />
<FontAwesomeIcon icon={faTable} />
</div>
Projects
</div>
@@ -1,14 +1,28 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import {
faBell,
faBook,
faCertificate,
faCog,
faFileLines,
faHome,
faMobile,
faSitemap,
faStamp,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
export const PkiManagerLayout = () => {
const { currentWorkspace } = useWorkspace();
const { assumedPrivilegeDetails } = useProjectPermission();
const { t } = useTranslation();
return (
@@ -24,70 +38,174 @@ export const PkiManagerLayout = () => {
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" />
PKI Manager
</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/cert-manager/subscribers"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Subscribers</MenuItem>}
<MenuGroup title="Resources">
<Link
to="/projects/cert-management/$projectId/subscribers"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faSitemap} />
</div>
Subscribers
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-templates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faFileLines} />
</div>
Certificate Templates
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCertificate} />
</div>
Certificates
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificates Authority
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/alerting"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBell} />
</div>
Alerting
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/cert-management/$projectId/access-management"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Access Management
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/audit-logs"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link>
<Link
to="/projects/$projectId/cert-manager/certificate-templates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificate Templates</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificates"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}> Certificates</MenuItem>}
</Link>
<Link
to="/projects/$projectId/cert-manager/certificate-authorities"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}> Certificates Authorities</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/cert-manager/alerting"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Alerting</MenuItem>}
</Link>
{
// <Link
// to="/projects/$projectId/cert-manager/settings"
// params={{
// projectId: currentWorkspace.id
// }}
// >
// {({ isActive }) => <MenuItem isSelected={isActive}>Settings</MenuItem>}
// </Link>
}
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
@@ -1,53 +0,0 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
export const ProjectGeneralLayout = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
Project Overview
</div>
<div className="flex-1">
<Menu>
<Link
to="/projects/$projectId/access-management"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Access Control</MenuItem>}
</Link>
<Link
to="/projects/$projectId/settings"
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Project Settings</MenuItem>}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<Outlet />
</div>
</div>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More