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

This commit is contained in:
Carlos Monastyrski
2025-08-28 16:50:18 -03:00
151 changed files with 3300 additions and 1363 deletions
@@ -5,14 +5,15 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
import { FilterableSelect, FormControl } from "@app/components/v2";
import {
TChecklyAccount,
useChecklyConnectionListAccounts
useChecklyConnectionListAccounts,
useChecklyConnectionListGroups
} from "@app/hooks/api/appConnections/checkly";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const ChecklySyncFields = () => {
const { control, setValue } = useFormContext<
const { control, setValue, watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Checkly }
>();
@@ -25,12 +26,24 @@ export const ChecklySyncFields = () => {
}
);
const accountId = watch("destinationConfig.accountId");
const { data: groups = [], isPending: isGroupsLoading } = useChecklyConnectionListGroups(
connectionId,
accountId,
{
enabled: Boolean(connectionId && accountId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.accountId", "");
setValue("destinationConfig.accountName", "");
setValue("destinationConfig.groupId", undefined);
setValue("destinationConfig.groupName", undefined);
}}
/>
<Controller
@@ -60,6 +73,37 @@ export const ChecklySyncFields = () => {
</FormControl>
)}
/>
<Controller
name="destinationConfig.groupId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Select a group"
isOptional
helperText="If provided, secrets will be scoped to a check group instead"
tooltipClassName="max-w-md"
>
<FilterableSelect
isLoading={isGroupsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
isClearable
value={groups.find((p) => p.id === value) ?? null}
onChange={(option) => {
const v = option as SingleValue<TChecklyAccount>;
onChange(v?.id ?? null);
setValue("destinationConfig.groupName", v?.name ?? undefined);
}}
options={groups}
placeholder="Select a group..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
</>
);
};
@@ -5,7 +5,9 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs";
import {
TRenderEnvironmentGroup,
TRenderService,
useRenderConnectionListEnvironmentGroups,
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -19,6 +21,7 @@ export const RenderSyncFields = () => {
>();
const connectionId = useWatch({ name: "connection.id", control });
const selectedScope = useWatch({ name: "destinationConfig.scope", control });
const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices(
connectionId,
@@ -27,11 +30,17 @@ export const RenderSyncFields = () => {
}
);
const { data: groups = [], isPending: isGroupsPending } =
useRenderConnectionListEnvironmentGroups(connectionId, {
enabled: Boolean(connectionId) && selectedScope === RenderSyncScope.EnvironmentGroup
});
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.serviceId", "");
setValue("destinationConfig.environmentGroupId", "");
setValue("destinationConfig.type", RenderSyncType.Env);
setValue("destinationConfig.scope", RenderSyncScope.Service);
}}
@@ -83,30 +92,67 @@ export const RenderSyncFields = () => {
</FormControl>
)}
/>
<Controller
name="destinationConfig.serviceId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Service">
<FilterableSelect
isLoading={isServicesPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={services ? (services.find((service) => service.id === value) ?? []) : []}
onChange={(option) => {
onChange((option as SingleValue<TRenderService>)?.id ?? null);
setValue(
"destinationConfig.serviceName",
(option as SingleValue<TRenderService>)?.name ?? ""
);
}}
options={services}
placeholder="Select a service..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
{selectedScope === RenderSyncScope.Service && (
<Controller
name="destinationConfig.serviceId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Service"
>
<FilterableSelect
isLoading={isServicesPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={services ? (services.find((service) => service.id === value) ?? []) : []}
onChange={(option) => {
onChange((option as SingleValue<TRenderService>)?.id ?? null);
setValue(
"destinationConfig.serviceName",
(option as SingleValue<TRenderService>)?.name ?? ""
);
}}
options={services}
placeholder="Select a service..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
)}
{selectedScope === RenderSyncScope.EnvironmentGroup && (
<Controller
name="destinationConfig.environmentGroupId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Environment Group"
>
<FilterableSelect
isLoading={isGroupsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={groups ? (groups.find((g) => g.id === value) ?? []) : []}
onChange={(option) => {
onChange((option as SingleValue<TRenderEnvironmentGroup>)?.id ?? null);
setValue(
"destinationConfig.environmentGroupName",
(option as SingleValue<TRenderEnvironmentGroup>)?.name ?? ""
);
}}
options={groups}
placeholder="Select an environment group..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
)}
</>
);
};
@@ -6,7 +6,16 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
export const ChecklySyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Checkly }>();
const accountName = watch("destinationConfig.accountName");
const config = watch("destinationConfig");
return <GenericFieldLabel label="Account">{accountName}</GenericFieldLabel>;
return (
<>
<GenericFieldLabel label="Account">
{config.accountName ?? config.accountId}
</GenericFieldLabel>
{config.groupId && (
<GenericFieldLabel label="Group">{config.groupName ?? config.groupId}</GenericFieldLabel>
)}
</>
);
};
@@ -4,6 +4,7 @@ import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
export const RenderSyncOptionsReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
@@ -27,13 +28,20 @@ export const RenderSyncOptionsReviewFields = () => {
export const RenderSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
const serviceName = watch("destinationConfig.serviceName");
const scope = watch("destinationConfig.scope");
const config = watch("destinationConfig");
return (
<>
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
<GenericFieldLabel label="Service">{serviceName}</GenericFieldLabel>
<GenericFieldLabel label="Scope">{config.scope}</GenericFieldLabel>
{config.scope === RenderSyncScope.Service ? (
<GenericFieldLabel label="Service">
{config.serviceName ?? config.serviceId}
</GenericFieldLabel>
) : (
<GenericFieldLabel label="Service">
{config.environmentGroupName ?? config.environmentGroupId}
</GenericFieldLabel>
)}
</>
);
};
@@ -8,7 +8,15 @@ export const ChecklySyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.Checkly),
destinationConfig: z.object({
accountId: z.string(),
accountName: z.string()
accountName: z.string(),
groupId: z
.string()
.nullish()
.transform((val) => val || undefined),
groupName: z
.string()
.nullish()
.transform((val) => val || undefined)
})
})
);
@@ -17,6 +17,12 @@ export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
serviceId: z.string().trim().min(1, "Service is required"),
serviceName: z.string().trim().optional(),
type: z.nativeEnum(RenderSyncType)
}),
z.object({
scope: z.literal(RenderSyncScope.EnvironmentGroup),
environmentGroupId: z.string().trim().min(1, "Environment Group ID is required"),
environmentGroupName: z.string().trim().optional(),
type: z.nativeEnum(RenderSyncType)
})
])
})
@@ -98,7 +98,7 @@ export const DatePicker = ({
>
{value
? formatDateTime({ timestamp: value, timezone, dateFormat })
: "Pick a date and time"}
: `Select Date${hideTime ? "" : " and Time"}`}
</Button>
</PopoverTrigger>
<PopoverContent
@@ -122,7 +122,8 @@ export const DatePicker = ({
root: `text-mineshaft-300 ${defaultClassNames}`,
[UI.DayButton]: "p-3 rounded hover:text-mineshaft-100",
[UI.Weekday]: "px-3 pt-3",
[UI.Chevron]: "fill-mineshaft-300"
[UI.Chevron]: "fill-mineshaft-300/70 hover:fill-mineshaft-300",
disabled: "text-mineshaft-400 pointer-events-none"
}}
/>
</div>
+4
View File
@@ -212,5 +212,9 @@ export const RENDER_SYNC_SCOPES: Record<RenderSyncScope, { name: string; descrip
[RenderSyncScope.Service]: {
name: "Service",
description: "Infisical will sync secrets to the specified Render service."
},
[RenderSyncScope.EnvironmentGroup]: {
name: "EnvironmentGroup",
description: "Infisical will sync secrets to the specified Render environment group."
}
};
@@ -36,6 +36,7 @@ export type Approver = {
type: ApproverType;
sequence?: number;
approvalsRequired?: number;
isOrgMembershipActive: boolean;
};
export type Bypasser = {
@@ -82,6 +83,7 @@ export type TAccessApprovalRequest = {
name: string;
approvals: number;
approvers: {
isOrgMembershipActive: boolean;
userId: string;
sequence?: number;
approvalsRequired?: number;
@@ -98,6 +100,7 @@ export type TAccessApprovalRequest = {
};
reviewers: {
isOrgMembershipActive: boolean;
userId: string;
status: string;
}[];
@@ -177,7 +180,7 @@ export type TCreateAccessPolicyDTO = {
projectSlug: string;
name?: string;
environments: string[];
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
approvals?: number;
secretPath: string;
@@ -190,7 +193,7 @@ export type TCreateAccessPolicyDTO = {
export type TUpdateAccessPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
secretPath?: string;
environments?: string[];
@@ -8,7 +8,9 @@ import { TChecklyAccount } from "./types";
const checklyConnectionKeys = {
all: [...appConnectionKeys.all, "checkly"] as const,
listAccounts: (connectionId: string) =>
[...checklyConnectionKeys.all, "workspace-scopes", connectionId] as const
[...checklyConnectionKeys.all, "workspace-scopes", connectionId] as const,
listGroups: (connectionId: string, accountId: string) =>
[...checklyConnectionKeys.all, "groups", connectionId, accountId] as const
};
export const useChecklyConnectionListAccounts = (
@@ -35,3 +37,29 @@ export const useChecklyConnectionListAccounts = (
...options
});
};
export const useChecklyConnectionListGroups = (
connectionId: string,
accountId: string,
options?: Omit<
UseQueryOptions<
TChecklyAccount[],
unknown,
TChecklyAccount[],
ReturnType<typeof checklyConnectionKeys.listGroups>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: checklyConnectionKeys.listGroups(connectionId, accountId),
queryFn: async () => {
const { data } = await apiRequest.get<{ groups: TChecklyAccount[] }>(
`/api/v1/app-connections/checkly/${connectionId}/accounts/${accountId}/groups`
);
return data.groups;
},
...options
});
};
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TRenderService } from "./types";
import { TRenderEnvironmentGroup, TRenderService } from "./types";
const renderConnectionKeys = {
all: [...appConnectionKeys.all, "render"] as const,
listServices: (connectionId: string) =>
[...renderConnectionKeys.all, "services", connectionId] as const
[...renderConnectionKeys.all, "services", connectionId] as const,
listEnvironmentGroups: (connectionId: string) =>
[...renderConnectionKeys.all, "environment-groups", connectionId] as const
};
export const useRenderConnectionListServices = (
@@ -35,3 +37,28 @@ export const useRenderConnectionListServices = (
...options
});
};
export const useRenderConnectionListEnvironmentGroups = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TRenderEnvironmentGroup[],
unknown,
TRenderEnvironmentGroup[],
ReturnType<typeof renderConnectionKeys.listEnvironmentGroups>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: renderConnectionKeys.listEnvironmentGroups(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TRenderEnvironmentGroup[]>(
`/api/v1/app-connections/render/${connectionId}/environment-groups`
);
return data;
},
...options
});
};
@@ -2,3 +2,8 @@ export type TRenderService = {
id: string;
name: string;
};
export type TRenderEnvironmentGroup = {
id: string;
name: string;
};
@@ -104,6 +104,7 @@ export const useUpdateOrg = () => {
mutationFn: ({
name,
authEnforced,
googleSsoAuthEnforced,
scimEnabled,
slug,
orgId,
@@ -125,6 +126,7 @@ export const useUpdateOrg = () => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
authEnforced,
googleSsoAuthEnforced,
scimEnabled,
slug,
defaultMembershipRoleSlug,
@@ -9,6 +9,7 @@ export type Organization = {
createAt: string;
updatedAt: string;
authEnforced: boolean;
googleSsoAuthEnforced: boolean;
bypassOrgAuthEnabled: boolean;
orgAuthMethod: string;
scimEnabled: boolean;
@@ -34,6 +35,7 @@ export type UpdateOrgDTO = {
orgId: string;
name?: string;
authEnforced?: boolean;
googleSsoAuthEnforced?: boolean;
scimEnabled?: boolean;
slug?: string;
defaultMembershipRoleSlug?: string;
@@ -20,6 +20,7 @@ export enum ApproverType {
}
export type Approver = {
isOrgMembershipActive: boolean;
id: string;
type: ApproverType;
};
@@ -49,7 +50,7 @@ export type TCreateSecretPolicyDTO = {
name?: string;
environments: string[];
secretPath: string;
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
approvals?: number;
enforcementLevel: EnforcementLevel;
@@ -59,7 +60,7 @@ export type TCreateSecretPolicyDTO = {
export type TUpdateSecretPolicyDTO = {
id: string;
name?: string;
approvers?: Approver[];
approvers?: Omit<Approver, "isOrgMembershipActive">[];
bypassers?: Bypasser[];
secretPath?: string;
approvals?: number;
@@ -53,6 +53,7 @@ export type TSecretApprovalRequest = {
firstName: string;
lastName: string;
username: string;
isOrgMembershipActive: boolean;
}[];
workspace: string;
environment: string;
@@ -62,6 +63,7 @@ export type TSecretApprovalRequest = {
status: "open" | "close";
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
approvers: {
isOrgMembershipActive: boolean;
userId: string;
email: string;
firstName: string;
@@ -3,11 +3,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 enum ChecklySyncScope {
Global = "global",
Group = "group"
}
export type TChecklySync = TRootSecretSync & {
destination: SecretSync.Checkly;
destinationConfig: {
accountId: string;
accountName: string;
groupId?: string;
groupName?: string;
};
connection: {
app: AppConnection.Checkly;
@@ -4,12 +4,19 @@ import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/typ
export type TRenderSync = TRootSecretSync & {
destination: SecretSync.Render;
destinationConfig: {
scope: RenderSyncScope.Service;
type: RenderSyncType;
serviceId: string;
serviceName?: string;
};
destinationConfig:
| {
type: RenderSyncType;
scope: RenderSyncScope.Service;
serviceId: string;
serviceName?: string | undefined;
}
| {
type: RenderSyncType;
scope: RenderSyncScope.EnvironmentGroup;
environmentGroupId: string;
environmentGroupName?: string | undefined;
};
connection: {
app: AppConnection.Render;
@@ -23,7 +30,8 @@ export type TRenderSync = TRootSecretSync & {
};
export enum RenderSyncScope {
Service = "service"
Service = "service",
EnvironmentGroup = "environment-group"
}
export enum RenderSyncType {
@@ -48,6 +48,7 @@ export type SubscriptionPlan = {
externalKms: boolean;
pkiEst: boolean;
enforceMfa: boolean;
enforceGoogleSSO: boolean;
projectTemplates: boolean;
kmip: boolean;
secretScanning: boolean;
+4 -4
View File
@@ -26,16 +26,16 @@ export const useAddUserToWsNonE2EE = () => {
});
};
export const sendEmailVerificationCode = async (username: string) => {
export const sendEmailVerificationCode = async (token: string) => {
return apiRequest.post("/api/v2/users/me/emails/code", {
username
token
});
};
export const useSendEmailVerificationCode = () => {
return useMutation({
mutationFn: async (username: string) => {
await sendEmailVerificationCode(username);
mutationFn: async (token: string) => {
await sendEmailVerificationCode(token);
return {};
}
});
+1
View File
@@ -83,6 +83,7 @@ export type TProjectMembership = {
export type TWorkspaceUser = {
id: string;
user: {
isOrgMembershipActive: boolean;
email: string;
username: string;
firstName: string;
@@ -240,6 +240,13 @@ export const Navbar = () => {
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
@@ -82,25 +82,40 @@ export const SelectOrganizationSection = () => {
}
}
if (organization.authEnforced && !canBypassOrgAuth) {
if ((organization.authEnforced || organization.googleSsoAuthEnforced) && !canBypassOrgAuth) {
const authToken = jwtDecode(getAuthToken()) as { authMethod: AuthMethod };
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
let url = "";
if (organization.orgAuthMethod === AuthMethod.OIDC) {
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
} else {
} else if (organization.orgAuthMethod === AuthMethod.SAML) {
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
if (callbackPort) {
url += `?callback_port=${callbackPort}`;
}
} else if (
organization.googleSsoAuthEnforced &&
authToken.authMethod !== AuthMethod.GOOGLE
) {
url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`;
if (callbackPort) {
url += `&callback_port=${callbackPort}`;
}
}
window.location.href = url;
return;
// we are conditionally checking if the url is set because it may not be set if google SSO is enforced, but the user is already logged in with google SSO
// see line 103-106
if (url) {
await logout.mutateAsync();
window.location.href = url;
return;
}
}
const { token, isMfaEnabled, mfaMethod } = await selectOrg
@@ -114,7 +114,16 @@ export const EmailConfirmationStep = ({
const resendCode = async () => {
try {
await sendEmailVerificationCode(username);
const queryParams = new URLSearchParams(window.location.search);
const token = queryParams.get("token");
if (!token) {
createNotification({
text: "Failed to resend code, no token found",
type: "error"
});
return;
}
await sendEmailVerificationCode(token);
createNotification({
text: "Successfully resent code",
type: "success"
@@ -11,7 +11,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { AllProjectView } from "./components/AllProjectView";
import { MyProjectView } from "./components/MyProjectView";
import { ProjectListToggle, ProjectListView } from "./components/ProjectListToggle";
import { ProjectListView } from "./components/ProjectListToggle";
// const formatDescription = (type: ProjectType) => {
// if (type === ProjectType.SecretManager)
@@ -28,7 +28,23 @@ import { ProjectListToggle, ProjectListView } from "./components/ProjectListTogg
export const ProjectsPage = () => {
const { t } = useTranslation();
const [projectListView, setProjectListView] = useState(ProjectListView.MyProjects);
const [projectListView, setProjectListView] = useState<ProjectListView>(() => {
const storedView = localStorage.getItem("projectListView");
if (
storedView &&
(storedView === ProjectListView.AllProjects || storedView === ProjectListView.MyProjects)
) {
return storedView;
}
return ProjectListView.MyProjects;
});
const handleSetProjectListView = (value: ProjectListView) => {
localStorage.setItem("projectListView", value);
setProjectListView(value);
};
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"addNewWs",
@@ -49,11 +65,7 @@ export const ProjectsPage = () => {
</Helmet>
<div className="mb-4 flex flex-col items-start justify-start">
<PageHeader
title={
<div className="flex items-center gap-4">
<ProjectListToggle value={projectListView} onChange={setProjectListView} />
</div>
}
title="Projects"
description="Your team's complete security toolkit - organized and ready when you need them."
/>
</div>
@@ -62,12 +74,16 @@ export const ProjectsPage = () => {
onAddNewProject={() => handlePopUpOpen("addNewWs")}
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
isAddingProjectsAllowed={isAddingProjectsAllowed}
projectListView={projectListView}
onProjectListViewChange={handleSetProjectListView}
/>
) : (
<AllProjectView
onAddNewProject={() => handlePopUpOpen("addNewWs")}
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
isAddingProjectsAllowed={isAddingProjectsAllowed}
projectListView={projectListView}
onProjectListViewChange={handleSetProjectListView}
/>
)}
<NewProjectModal
@@ -49,11 +49,17 @@ import {
useSearchProjects
} from "@app/hooks/api";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import {
ProjectListToggle,
ProjectListView
} from "@app/pages/organization/ProjectsPage/components/ProjectListToggle";
type Props = {
onAddNewProject: () => void;
onUpgradePlan: () => void;
isAddingProjectsAllowed: boolean;
projectListView: ProjectListView;
onProjectListViewChange: (value: ProjectListView) => void;
};
type RequestAccessModalProps = {
@@ -106,7 +112,9 @@ const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProp
export const AllProjectView = ({
onAddNewProject,
onUpgradePlan,
isAddingProjectsAllowed
isAddingProjectsAllowed,
projectListView,
onProjectListViewChange
}: Props) => {
const navigate = useNavigate();
const [searchFilter, setSearchFilter] = useState("");
@@ -176,10 +184,10 @@ export const AllProjectView = ({
return (
<div>
<div className="flex w-full flex-row">
<div className="flex-grow" />
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
<Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
containerClassName="w-full"
containerClassName="w-full ml-2"
placeholder="Search by project name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
@@ -242,7 +250,7 @@ export const AllProjectView = ({
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<div className="ml-2 flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Tooltip content="Disabled across All Project view.">
<div className="flex cursor-not-allowed items-center justify-center">
<IconButton
@@ -44,11 +44,17 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import {
ProjectListToggle,
ProjectListView
} from "@app/pages/organization/ProjectsPage/components/ProjectListToggle";
type Props = {
onAddNewProject: () => void;
onUpgradePlan: () => void;
isAddingProjectsAllowed: boolean;
projectListView: ProjectListView;
onProjectListViewChange: (value: ProjectListView) => void;
};
enum ProjectOrderBy {
@@ -63,7 +69,9 @@ enum ProjectsViewMode {
export const MyProjectView = ({
onAddNewProject,
onUpgradePlan,
isAddingProjectsAllowed
isAddingProjectsAllowed,
projectListView,
onProjectListViewChange
}: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
@@ -371,10 +379,10 @@ export const MyProjectView = ({
return (
<div>
<div className="flex w-full flex-row">
<div className="flex-grow" />
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
<Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
containerClassName="w-full"
containerClassName="w-full ml-2"
placeholder="Search by project name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
@@ -441,7 +449,7 @@ export const MyProjectView = ({
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<div className="ml-2 flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<IconButton
variant="outline_bg"
onClick={() => {
@@ -1,7 +1,4 @@
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Select, SelectItem } from "@app/components/v2";
import { Button } from "@app/components/v2";
export enum ProjectListView {
MyProjects = "my-projects",
@@ -14,28 +11,32 @@ type Props = {
};
export const ProjectListToggle = ({ value, onChange }: Props) => {
const getDisplayText = (listView: ProjectListView) => {
return listView === ProjectListView.MyProjects ? "My Projects" : "All Projects";
};
return (
<div className="group relative flex cursor-pointer items-center gap-2">
<h1 className="text-3xl font-semibold transition-colors group-hover:text-gray-500">
{getDisplayText(value)}
</h1>
<Select
value={value}
onValueChange={onChange}
className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
position="popper"
<div className="flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Button
variant="outline_bg"
onClick={() => {
onChange(ProjectListView.MyProjects);
}}
size="xs"
className={`${
value === ProjectListView.MyProjects ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
>
<SelectItem value={ProjectListView.MyProjects}>My Projects</SelectItem>
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>
</Select>
<FontAwesomeIcon
icon={faChevronDown}
className="text-lg transition-colors group-hover:text-gray-500"
/>
My Projects
</Button>
<Button
variant="outline_bg"
onClick={() => {
onChange(ProjectListView.AllProjects);
}}
size="xs"
className={`${
value === ProjectListView.AllProjects ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
>
All Projects
</Button>
</div>
);
};
@@ -1,5 +1,6 @@
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
@@ -14,7 +15,21 @@ import {
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const OrgGeneralAuthSection = () => {
enum EnforceAuthType {
SAML = "saml",
GOOGLE = "google",
OIDC = "oidc"
}
export const OrgGeneralAuthSection = ({
isSamlConfigured,
isOidcConfigured,
isGoogleConfigured
}: {
isSamlConfigured: boolean;
isOidcConfigured: boolean;
isGoogleConfigured: boolean;
}) => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -23,27 +38,61 @@ export const OrgGeneralAuthSection = () => {
const logout = useLogoutUser();
const handleEnforceOrgAuthToggle = async (value: boolean) => {
const handleEnforceOrgAuthToggle = async (value: boolean, type: EnforceAuthType) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan");
return;
if (type === EnforceAuthType.SAML) {
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
authEnforced: value
});
} else if (type === EnforceAuthType.GOOGLE) {
if (!subscription?.enforceGoogleSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
googleSsoAuthEnforced: value
});
} else if (type === EnforceAuthType.OIDC) {
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
authEnforced: value
});
} else {
createNotification({
text: `Invalid auth enforcement type ${type}`,
type: "error"
});
}
await mutateAsync({
orgId: currentOrg?.id,
authEnforced: value
});
createNotification({
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
text: `Successfully ${value ? "enabled" : "disabled"} org-level auth`,
type: "success"
});
if (value) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`);
if (type === EnforceAuthType.SAML) {
window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`);
} else if (type === EnforceAuthType.GOOGLE) {
window.open(`/api/v1/sso/redirect/google?org_slug=${currentOrg.slug}`);
}
window.close();
}
} catch (err) {
@@ -78,45 +127,91 @@ export const OrgGeneralAuthSection = () => {
};
return (
<>
{/* <div className="py-4">
<div className="mb-2 flex justify-between">
<h3 className="text-md text-mineshaft-100">Allow users to send invites</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="allow-org-invites"
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
isChecked={currentOrg?.authEnforced ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Allow members to invite new users to this organization</p>
</div> */}
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-org-auth"
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
isChecked={currentOrg?.authEnforced ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via SAML to access this organization
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div>
<p className="text-xl font-semibold text-gray-200">SSO Enforcement</p>
<p className="mb-2 mt-1 text-gray-400">
Manage strict enforcement of specific authentication methods for your organization.
</p>
</div>
{currentOrg?.authEnforced && (
<div className="py-4">
<div className="flex flex-col gap-2 py-4">
<div className={twMerge("mt-4", !isSamlConfigured && "hidden")}>
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-saml-auth"
onCheckedChange={(value) =>
handleEnforceOrgAuthToggle(value, EnforceAuthType.SAML)
}
isChecked={currentOrg?.authEnforced ?? false}
isDisabled={!isAllowed || currentOrg?.googleSsoAuthEnforced}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via SAML to access this organization.
<br />
When this is enabled your organization members will only be able to login with SAML.
</p>
</div>
<div className={twMerge("mt-4", !isOidcConfigured && "hidden")}>
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-oidc-auth"
isChecked={currentOrg?.authEnforced ?? false}
onCheckedChange={(value) =>
handleEnforceOrgAuthToggle(value, EnforceAuthType.OIDC)
}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via OIDC to access this organization.
<br />
When this is enabled your organization members will only be able to login with OIDC.
</p>
</div>
<div className={twMerge("mt-2", !isGoogleConfigured && "hidden")}>
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce Google SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-google-sso"
onCheckedChange={(value) =>
handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE)
}
isChecked={currentOrg?.googleSsoAuthEnforced ?? false}
isDisabled={!isAllowed || currentOrg?.authEnforced}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via Google to access this organization.
<br />
When this is enabled your organization members will only be able to login with Google.
</p>
</div>
</div>
{(currentOrg?.authEnforced || currentOrg?.googleSsoAuthEnforced) && (
<div className="mt-4 py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
@@ -125,8 +220,8 @@ export const OrgGeneralAuthSection = () => {
content={
<div>
<span>
When this is enabled, we strongly recommend enforcing MFA at the organization
level.
When enabling admin SSO bypass, we highly recommend enabling MFA enforcement
at the organization-level for security reasons.
</span>
<p className="mt-4">
In case of a lockout, admins can use the{" "}
@@ -182,6 +277,6 @@ export const OrgGeneralAuthSection = () => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can enforce SAML SSO if you switch to Infisical's Pro plan."
/>
</>
</div>
);
};
@@ -95,43 +95,25 @@ export const OrgLDAPSection = (): JSX.Element => {
};
return (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="mb-4">
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP</h2>
<div className="flex">
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-gray-200">LDAP</p>
<p className="mb-2 text-gray-400">Manage LDAP authentication configuration</p>
</div>
</div>
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
</div>
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button
onClick={openLDAPGroupMapModal}
colorSchema="secondary"
isDisabled={!isAllowed}
>
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Manage how LDAP groups are mapped to internal groups in Infisical
</p>
</div>
{data && (
<div className="py-4">
<div className="pt-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable LDAP</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}>
@@ -152,6 +134,27 @@ export const OrgLDAPSection = (): JSX.Element => {
</p>
</div>
)}
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button
onClick={openLDAPGroupMapModal}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Configure
</Button>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Manage how LDAP groups are mapped to internal groups in Infisical
</p>
</div>
<LDAPModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -11,7 +11,7 @@ import {
useOrganization,
useSubscription
} from "@app/context";
import { useGetOIDCConfig, useLogoutUser, useUpdateOrg } from "@app/hooks/api";
import { useGetOIDCConfig } from "@app/hooks/api";
import { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -23,9 +23,7 @@ export const OrgOIDCSection = (): JSX.Element => {
const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateOIDCConfig();
const { mutateAsync: updateOrg } = useUpdateOrg();
const logout = useLogoutUser();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addOIDC",
"upgradePlan"
@@ -54,56 +52,6 @@ export const OrgOIDCSection = (): JSX.Element => {
}
};
const handleEnforceOrgAuthToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await updateOrg({
orgId: currentOrg?.id,
authEnforced: value
});
createNotification({
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
type: "success"
});
if (value) {
await logout.mutateAsync();
window.open(`/api/v1/sso/oidc/login?orgSlug=${currentOrg.slug}`);
window.close();
}
} catch (err) {
console.error(err);
}
};
const handleEnableBypassOrgAuthToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await updateOrg({
orgId: currentOrg?.id,
bypassOrgAuthEnabled: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} admin bypassing of org-level auth`,
type: "success"
});
} catch (err) {
console.error(err);
}
};
const handleOIDCGroupManagement = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
@@ -136,25 +84,22 @@ export const OrgOIDCSection = (): JSX.Element => {
};
return (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">OIDC</h2>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button
onClick={addOidcButtonClick}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Manage
</Button>
)}
</OrgPermissionCan>
)}
<div className="mb-4 rounded-lg border-mineshaft-600 bg-mineshaft-900">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-gray-200">OIDC</p>
<p className="mb-2 text-gray-400">Manage OIDC authentication configuration</p>
</div>
<p className="text-sm text-mineshaft-300">Manage OIDC authentication configuration</p>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button onClick={addOidcButtonClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
)}
</div>
{data && (
<div className="py-4">
@@ -178,88 +123,6 @@ export const OrgOIDCSection = (): JSX.Element => {
</p>
</div>
)}
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-org-auth"
isChecked={currentOrg?.authEnforced ?? false}
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
<span>Enforce users to authenticate via OIDC to access this organization.</span>
</p>
</div>
{currentOrg?.authEnforced && (
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
<Tooltip
className="max-w-lg"
content={
<div>
<span>
When this is enabled, we strongly recommend enforcing MFA at the organization
level.
</span>
<p className="mt-4">
In case of a lockout, admins can use the{" "}
<a
target="_blank"
className="underline underline-offset-2 hover:text-mineshaft-300"
href="https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal"
rel="noreferrer"
>
Admin Login Portal
</a>{" "}
at{" "}
<a
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-mineshaft-300"
href={`${window.location.origin}/login/admin`}
>
{window.location.origin}/login/admin
</a>
</p>
</div>
}
>
<FontAwesomeIcon
icon={faInfoCircle}
size="sm"
className="mt-0.5 inline-block text-mineshaft-400"
/>
</Tooltip>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="allow-admin-bypass"
isChecked={currentOrg?.bypassOrgAuthEnabled ?? false}
onCheckedChange={(value) => handleEnableBypassOrgAuthToggle(value)}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
<span>
Allow organization admins to bypass OIDC enforcement when SSO is unavailable,
misconfigured, or inaccessible.
</span>
</p>
</div>
)}
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="text-md flex items-center text-mineshaft-100">
@@ -79,25 +79,24 @@ export const OrgSSOSection = (): JSX.Element => {
};
return (
<>
<hr className="border-mineshaft-600" />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">SAML</h2>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button onClick={addSSOBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
)}
<div className="space-y-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-gray-200">SAML</p>
<p className="mb-2 text-gray-400">Manage SAML authentication configuration</p>
</div>
<p className="text-sm text-mineshaft-300">Manage SAML authentication configuration</p>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button onClick={addSSOBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
)}
</div>
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<div>
<div className="mb-2 flex items-center justify-between pt-4">
<h2 className="text-md text-mineshaft-100">Enable SAML</h2>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
@@ -126,6 +125,6 @@ export const OrgSSOSection = (): JSX.Element => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SAML SSO if you switch to Infisical's Pro plan."
/>
</>
</div>
);
};
@@ -49,13 +49,19 @@ export const OrgSsoTab = withPermission(
);
const areConfigsLoading = isLoadingOidcConfig || isLoadingSamlConfig || isLoadingLdapConfig;
const shouldDisplaySection = (method: LoginMethod) =>
!enabledLoginMethods || enabledLoginMethods.includes(method);
const shouldDisplaySection = (method: LoginMethod[] | LoginMethod) => {
if (Array.isArray(method)) {
return method.some((m) => !enabledLoginMethods || enabledLoginMethods.includes(m));
}
const isOidcConfigured = oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer);
return !enabledLoginMethods || enabledLoginMethods.includes(method);
};
const isOidcConfigured = Boolean(oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer));
const isSamlConfigured =
samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert);
const isLdapConfigured = ldapConfig && ldapConfig.url;
const isGoogleConfigured = shouldDisplaySection(LoginMethod.GOOGLE);
const shouldShowCreateIdentityProviderView =
!isOidcConfigured && !isSamlConfigured && !isLdapConfigured;
@@ -65,11 +71,14 @@ export const OrgSsoTab = withPermission(
shouldDisplaySection(LoginMethod.OIDC) ||
shouldDisplaySection(LoginMethod.LDAP) ? (
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<p className="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
<p className="mb-2 mt-1 text-gray-400">
Connect your identity provider to simplify user management
</p>
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div>
<p className="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
<p className="mb-2 mt-1 text-gray-400">
Connect your identity provider to simplify user management with options like SAML,
OIDC, and LDAP.
</p>
</div>
{shouldDisplaySection(LoginMethod.SAML) && (
<div
className={twMerge(
@@ -169,20 +178,27 @@ export const OrgSsoTab = withPermission(
return (
<>
{shouldShowCreateIdentityProviderView ? (
createIdentityProviderView
) : (
<>
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgGeneralAuthSection />
<OrgSSOSection />
<div className="space-y-4">
{shouldDisplaySection([LoginMethod.SAML, LoginMethod.GOOGLE]) && (
<OrgGeneralAuthSection
isSamlConfigured={isSamlConfigured}
isOidcConfigured={isOidcConfigured}
isGoogleConfigured={isGoogleConfigured}
/>
)}
{shouldShowCreateIdentityProviderView ? (
createIdentityProviderView
) : (
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div>
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && <OrgSSOSection />}
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
</div>
)}
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
</>
)}
</div>
)}
</div>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
@@ -1,5 +1,8 @@
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import {
useRenderConnectionListEnvironmentGroups,
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
@@ -9,21 +12,59 @@ type Props = {
};
export const RenderSyncDestinationCol = ({ secretSync }: Props) => {
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
const { data: services = [], isPending } = useRenderConnectionListServices(
secretSync.connectionId
secretSync.connectionId,
{
enabled: isServiceScope
}
);
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
...secretSync,
destinationConfig: {
...secretSync.destinationConfig,
serviceName: services.find((s) => s.id === secretSync.destinationConfig.serviceId)?.name
const { data: groups = [], isPending: isGroupsPending } =
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
switch (secretSync.destinationConfig.scope) {
case RenderSyncScope.Service: {
const id = secretSync.destinationConfig.serviceId;
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
...secretSync,
destinationConfig: {
...secretSync.destinationConfig,
serviceName: services.find((s) => s.id === id)?.name
}
});
if (isPending) {
return (
<SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />
);
}
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
}
});
case RenderSyncScope.EnvironmentGroup: {
const id = secretSync.destinationConfig.environmentGroupId;
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
...secretSync,
destinationConfig: {
...secretSync.destinationConfig,
environmentGroupName: groups.find((s) => s.id === id)?.name
}
});
if (isPending) {
return <SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />;
if (isGroupsPending) {
return (
<SecretSyncTableCell
primaryText="Loading environment group info..."
secondaryText="Environment Group"
/>
);
}
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
}
default:
throw new Error("Unknown render sync destination scope");
}
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -8,6 +8,7 @@ import {
} from "@app/hooks/api/secretSyncs/types/github-sync";
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
// This functional ensures parity across what is displayed in the destination column
// and the values used when search filtering
@@ -125,8 +126,15 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
secondaryText = destinationConfig.app;
break;
case SecretSync.Render:
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
secondaryText = "Service";
if (destinationConfig.scope === RenderSyncScope.Service) {
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
secondaryText = "Service";
} else {
primaryText =
destinationConfig.environmentGroupName ?? destinationConfig.environmentGroupId;
secondaryText = "Environment Group";
}
break;
case SecretSync.Flyio:
primaryText = destinationConfig.appId;
@@ -167,8 +175,8 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
secondaryText = "Railway Project";
break;
case SecretSync.Checkly:
primaryText = destinationConfig.accountName;
secondaryText = "Checkly Account";
primaryText = destinationConfig.accountName || destinationConfig.accountId;
secondaryText = destinationConfig.groupName || destinationConfig.groupId || "Checkly Account";
break;
case SecretSync.Supabase:
primaryText = destinationConfig.projectName;
@@ -4,7 +4,9 @@ import {
faCheck,
faEdit,
faHourglass,
faTriangleExclamation
faTriangleExclamation,
faUser,
faUserSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
@@ -37,7 +39,7 @@ import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
import { groupBy } from "@app/lib/fn/array";
import { EditAccessRequestModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/EditAccessRequestModal";
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
const getReviewedStatusSymbol = (status?: ApprovalStatus, isOrgMembershipActive?: boolean) => {
if (status === ApprovalStatus.APPROVED)
return (
<Badge variant="success" className="flex h-4 items-center justify-center">
@@ -50,6 +52,17 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
<FontAwesomeIcon icon={faBan} size="xs" />
</Badge>
);
if (!isOrgMembershipActive) {
return (
// Can't do a tooltip here because nested tooltips doesn't work properly as of yet.
// TODO(daniel): Fix nested tooltips in the future.
<Badge className="flex h-4 items-center justify-center bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon size="xs" icon={faUserSlash} />
</Badge>
);
}
return (
<Badge variant="primary" className="flex h-4 items-center justify-center">
<FontAwesomeIcon icon={faHourglass} size="xs" />
@@ -87,6 +100,7 @@ export const ReviewAccessRequestModal = ({
}) => {
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
const [bypassApproval, setBypassApproval] = useState(false);
const [bypassReason, setBypassReason] = useState("");
const { currentWorkspace } = useWorkspace();
const { data: groupMemberships = [] } = useListWorkspaceGroups(currentWorkspace?.id || "");
@@ -192,6 +206,7 @@ export const ReviewAccessRequestModal = ({
(acc, curr) => {
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
acc[acc.length - 1][curr.type]?.push(curr);
return acc;
}
@@ -203,6 +218,7 @@ export const ReviewAccessRequestModal = ({
? { user: [curr], group: [], sequence, approvals }
: { group: [curr], user: [], sequence, approvals }
);
return acc;
},
[] as {
@@ -216,7 +232,10 @@ export const ReviewAccessRequestModal = ({
const approvers = approversBySequence?.map((approverChain) => {
const reviewers = request.policy.approvers
.filter((el) => (el.sequence || 1) === approverChain.sequence)
.map((el) => ({ ...el, status: reviewesGroupById?.[el.userId]?.[0]?.status }));
.map((el) => ({
...el,
status: reviewesGroupById?.[el.userId]?.[0]?.status
}));
const hasApproved =
reviewers.filter((el) => el.status === "approved").length >=
(approverChain?.approvals || 1);
@@ -410,12 +429,39 @@ export const ReviewAccessRequestModal = ({
</div>
)}
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
<GenericFieldLabel className="col-span-2" label="Users">
{approver?.user
?.map(
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
)
.join(", ")}
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
{Boolean(approver.user.length) && (
<div className="flex flex-row flex-wrap gap-2">
{approver?.user?.map((el, idx) => {
const member = approverSequence?.membersGroupById?.[el.id]?.[0];
if (!member) return null;
return member.user.isOrgMembershipActive ? (
<div className="flex items-center" key={member.user.id}>
<span>{member.user.username}</span>
{idx < approver.user.length - 1 && ","}
</div>
) : (
<div className="flex items-center" key={member.user.id}>
<span className="flex items-center opacity-40">
{member.user.username}
<span className="text-xs">
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
<div>
<Badge className="pointer-events-none ml-1 mr-auto flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
Inactive
</Badge>
</div>
</Tooltip>
</span>
</span>
{idx < approver.user.length - 1 && ","}
</div>
);
})}
</div>
)}
</GenericFieldLabel>
<GenericFieldLabel className="col-span-2" label="Groups">
{approver?.group
@@ -440,8 +486,18 @@ export const ReviewAccessRequestModal = ({
key={`reviewer-${idx + 1}`}
className="flex items-center gap-2 px-2 py-2 text-sm"
>
<div className="flex-1">{el.username}</div>
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
<div
className={twMerge(
"flex-1",
!el.isOrgMembershipActive && "opacity-40"
)}
>
{el.username}
</div>
{getReviewedStatusSymbol(
el?.status as ApprovalStatus,
el.isOrgMembershipActive
)}
</div>
))}
</div>
@@ -43,6 +43,8 @@ import {
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
import { TWorkspaceUser } from "@app/hooks/api/users/types";
import { PolicyMemberOption } from "./PolicyMemberOption";
type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
@@ -59,7 +61,11 @@ const formSchema = z
secretPath: z.string().trim().min(1),
approvals: z.number().min(1).default(1),
userApprovers: z
.object({ type: z.literal(ApproverType.User), id: z.string() })
.object({
type: z.literal(ApproverType.User),
id: z.string(),
isOrgMembershipActive: z.boolean().optional()
})
.array()
.default([]),
groupApprovers: z
@@ -67,7 +73,11 @@ const formSchema = z
.array()
.default([]),
userBypassers: z
.object({ type: z.literal(BypasserType.User), id: z.string() })
.object({
type: z.literal(BypasserType.User),
id: z.string(),
isOrgMembershipActive: z.boolean().optional()
})
.array()
.default([]),
groupBypassers: z
@@ -80,7 +90,11 @@ const formSchema = z
sequenceApprovers: z
.object({
user: z
.object({ type: z.literal(ApproverType.User), id: z.string() })
.object({
type: z.literal(ApproverType.User),
id: z.string(),
isOrgMembershipActive: z.boolean().optional()
})
.array()
.default([]),
group: z
@@ -139,7 +153,11 @@ const Form = ({
userApprovers:
editValues?.approvers
?.filter((approver) => approver.type === ApproverType.User)
.map(({ id, type }) => ({ id, type: type as ApproverType.User })) || [],
.map(({ id, type, isOrgMembershipActive }) => ({
id,
type: type as ApproverType.User,
isOrgMembershipActive
})) || [],
groupApprovers:
editValues?.approvers
?.filter((approver) => approver.type === ApproverType.Group)
@@ -235,7 +253,9 @@ const Form = ({
...data,
approvers: sequenceApprovers?.flatMap((approvers, index) =>
approvers.user
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
.map(
(el) => ({ ...el, sequence: index + 1 }) as Omit<Approver, "isOrgMembershipActive">
)
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
),
approvalsRequired: sequenceApprovers?.map((el, index) => ({
@@ -291,7 +311,9 @@ const Form = ({
...data,
approvers: sequenceApprovers?.flatMap((approvers, index) =>
approvers.user
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
.map(
(el) => ({ ...el, sequence: index + 1 }) as Omit<Approver, "isOrgMembershipActive">
)
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
),
approvalsRequired: sequenceApprovers?.map((el, index) => ({
@@ -329,7 +351,8 @@ const Form = ({
() =>
members.map((member) => ({
id: member.user.id,
type: ApproverType.User
type: ApproverType.User,
isOrgMembershipActive: member.user.isOrgMembershipActive
})),
[members]
);
@@ -347,7 +370,8 @@ const Form = ({
() =>
members.map((member) => ({
id: member.user.id,
type: BypasserType.User
type: BypasserType.User,
isOrgMembershipActive: member.user.isOrgMembershipActive
})),
[members]
);
@@ -608,6 +632,7 @@ const Form = ({
isMulti
placeholder="Select members..."
options={memberOptions}
components={{ Option: PolicyMemberOption }}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => {
const member = members?.find((m) => m.user.id === option.id);
@@ -685,6 +710,7 @@ const Form = ({
menuPlacement="top"
isMulti
placeholder="Select members..."
components={{ Option: PolicyMemberOption }}
options={memberOptions}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => {
@@ -783,6 +809,7 @@ const Form = ({
menuPlacement="top"
isMulti
placeholder="Select members..."
components={{ Option: PolicyMemberOption }}
options={bypasserMemberOptions}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => {
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import {
faBan,
faClipboardCheck,
faEdit,
faEllipsisV,
@@ -19,6 +20,7 @@ import {
GenericFieldLabel,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
@@ -86,10 +88,9 @@ export const ApprovalPolicyRow = ({
return entityInSameSequence?.map((el) => {
return {
sequence: el.sequence || policy.approvals,
userLabels: members
?.filter((member) => el.user.find((i) => i.id === member.user.id))
.map((member) => getMemberLabel(member))
.join(", "),
users: members.filter((member) => el.user.find((i) => i.id === member.user.id)),
groupLabels: groups
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
.map(({ group }) => group.name)
@@ -212,7 +213,35 @@ export const ApprovalPolicyRow = ({
)}
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
{el.userLabels}
{Boolean(el.users.length) && (
<div className="flex flex-row flex-wrap gap-2">
{el.users.map((u, idx) => {
return u.user.isOrgMembershipActive ? (
<div className="flex items-center" key={u.id}>
<span>{getMemberLabel(u)}</span>
{idx < el.users.length - 1 && ","}
</div>
) : (
<div className="flex items-center" key={u.id}>
<span className="flex items-center opacity-40">
{getMemberLabel(u)}
<span className="text-xs">
<Tooltip content="This user has been deactivated and no longer has an active organization membership.">
<div>
<Badge className="pointer-events-none ml-1 mr-auto flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
Inactive
</Badge>
</div>
</Tooltip>
</span>
</span>
{idx < el.users.length - 1 && ","}
</div>
);
})}
</div>
)}
</GenericFieldLabel>
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
{el.groupLabels}
@@ -0,0 +1,40 @@
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v2";
import { BypasserType } from "@app/hooks/api/accessApproval/types";
import { ApproverType } from "@app/hooks/api/secretApproval/types";
export const PolicyMemberOption = ({
isSelected,
children,
...props
}: OptionProps<{
id: string;
type: BypasserType | ApproverType;
isOrgMembershipActive?: boolean;
}>) => {
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p
className={twMerge("truncate", !props.data.isOrgMembershipActive && "text-mineshaft-400")}
>
{children}
</p>
{!props.data.isOrgMembershipActive && (
<Badge className="pointer-events-none ml-1 mr-auto flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
Inactive
</Badge>
)}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</div>
</components.Option>
);
};
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { ReactNode } from "react";
import { Controller, useForm } from "react-hook-form";
import {
@@ -8,7 +9,8 @@ import {
faCodeBranch,
faComment,
faFolder,
faHourglass
faHourglass,
faUserSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -85,6 +87,7 @@ const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
return <FontAwesomeIcon icon={faCheck} size="xs" className="text-green" />;
if (status === ApprovalStatus.REJECTED)
return <FontAwesomeIcon icon={faBan} size="xs" className="text-red" />;
return <FontAwesomeIcon icon={faHourglass} size="xs" className="text-yellow" />;
};
@@ -162,11 +165,15 @@ export const SecretApprovalRequestChanges = ({
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
Record<string, { status: ApprovalStatus; comment: string }>
Record<string, { status: ApprovalStatus; comment: string; isOrgMembershipActive: boolean }>
>(
(prev, curr) => ({
...prev,
[curr.userId]: { status: curr.status, comment: curr.comment }
[curr.userId]: {
status: curr.status,
comment: curr.comment,
isOrgMembershipActive: curr.isOrgMembershipActive
}
}),
{}
);
@@ -226,7 +233,7 @@ export const SecretApprovalRequestChanges = ({
return (
<div className="flex flex-col space-x-6 lg:flex-row">
<div className="flex-1 lg:max-w-[calc(100%-17rem)]">
<div className="sticky top-0 z-20 flex items-center space-x-4 bg-bunker-800 pb-6 pt-2">
<div className="sticky -top-8 z-20 flex items-center space-x-4 bg-bunker-800 pb-6 pt-2">
<IconButton variant="outline_bg" ariaLabel="go-back" onClick={onGoBack}>
<FontAwesomeIcon icon={faArrowLeft} />
</IconButton>
@@ -533,26 +540,44 @@ export const SecretApprovalRequestChanges = ({
)
.map((requiredApprover) => {
const reviewer = reviewedUsers?.[requiredApprover.userId];
const { isOrgMembershipActive } = requiredApprover;
return (
<div
className="flex flex-nowrap items-center justify-between space-x-2 rounded border border-mineshaft-600 bg-mineshaft-800 px-2 py-1"
key={`required-approver-${requiredApprover.userId}`}
>
<Tooltip
content={
requiredApprover.firstName
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
: undefined
}
position="left"
sideOffset={10}
<div
className={twMerge(
"flex items-center gap-1 text-sm",
!isOrgMembershipActive && "opacity-40"
)}
>
<div className="flex text-sm">
<div>{requiredApprover?.email}</div>
<span className="text-red">*</span>
</div>
</Tooltip>
<div>
<Tooltip
content={
!isOrgMembershipActive
? "This user has been deactivated and no longer has an active organization membership."
: requiredApprover.firstName
? `${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`
: undefined
}
position="left"
sideOffset={10}
>
<div className="flex items-center">
<div>{requiredApprover?.email}</div>
<span className="text-red">*</span>
{!isOrgMembershipActive && (
<FontAwesomeIcon
icon={faUserSlash}
size="xs"
className="ml-1 text-mineshaft-300"
/>
)}
</div>
</Tooltip>
</div>
<div className="flex items-center">
{reviewer?.comment && (
<Tooltip className="max-w-lg break-words" content={reviewer.comment}>
<FontAwesomeIcon
@@ -562,9 +587,21 @@ export const SecretApprovalRequestChanges = ({
/>
</Tooltip>
)}
<Tooltip content={`Status: ${reviewer?.status || ApprovalStatus.PENDING}`}>
{getReviewedStatusSymbol(reviewer?.status)}
</Tooltip>
<div className="flex gap-2">
<Tooltip
className="relative !z-[500]"
content={
<span className="text-sm">
Status:{" "}
<span className="capitalize">
{reviewer?.status || ApprovalStatus.PENDING}
</span>
</span>
}
>
{getReviewedStatusSymbol(reviewer?.status)}
</Tooltip>
</div>
</div>
</div>
);
@@ -578,20 +615,42 @@ export const SecretApprovalRequestChanges = ({
)
.map((reviewer) => {
const status = reviewedUsers?.[reviewer.userId].status;
const { isOrgMembershipActive } = reviewer;
return (
<div
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
className="flex flex-nowrap items-center justify-between space-x-2 rounded bg-mineshaft-800 px-2 py-1"
key={`required-approver-${reviewer.userId}`}
>
<div className="flex-grow text-sm">
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
<span>{reviewer?.email} </span>
<div
className={twMerge(
"flex items-center gap-1 text-sm",
!isOrgMembershipActive && "opacity-40"
)}
>
<Tooltip
className="relative !z-[500]"
content={
!isOrgMembershipActive
? "This user has been deactivated and no longer has an active organization membership."
: `${reviewer.firstName || ""} ${reviewer.lastName || ""}`
}
>
<div className="flex items-center">
<span>{reviewer?.email} </span>
{!isOrgMembershipActive && (
<FontAwesomeIcon
icon={faUserSlash}
size="xs"
className="ml-1 text-mineshaft-300"
/>
)}
</div>
</Tooltip>
<span className="text-red">*</span>
</div>
<div>
{reviewer.comment && (
<Tooltip content={reviewer.comment}>
<Tooltip className="relative !z-[500]" content={reviewer.comment}>
<FontAwesomeIcon
icon={faComment}
size="xs"
@@ -599,7 +658,15 @@ export const SecretApprovalRequestChanges = ({
/>
</Tooltip>
)}
<Tooltip content={status || ApprovalStatus.PENDING}>
<Tooltip
className="relative !z-[500]"
content={
<span className="text-sm">
Status:{" "}
<span className="capitalize">{status || ApprovalStatus.PENDING}</span>
</span>
}
>
{getReviewedStatusSymbol(status)}
</Tooltip>
</div>
@@ -8,6 +8,7 @@ import {
faSave
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { Badge, Button, Input, Modal, ModalContent } from "@app/components/v2";
import { PendingAction } from "@app/hooks/api/secretFolders/types";
@@ -302,47 +303,61 @@ export const CommitForm: React.FC<CommitFormProps> = ({
<>
{/* Floating Panel */}
{!isModalOpen && (
<div className="fixed bottom-4 left-1/2 z-40 w-full max-w-3xl -translate-x-1/2 self-center rounded-lg border border-yellow/30 bg-mineshaft-800 shadow-2xl lg:left-auto lg:translate-x-0">
<div className="flex items-center justify-between p-4">
{/* Left Content */}
<div className="flex-1">
{/* Header */}
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-yellow-500" />
<span className="font-medium text-mineshaft-100">Pending Changes</span>
<Badge variant="primary" className="text-xs">
{totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""}
</Badge>
<div className="fixed bottom-4 left-1/2 z-40 w-full max-w-3xl -translate-x-1/2 self-center lg:left-auto lg:translate-x-0">
<AnimatePresence mode="wait">
<motion.div
key="commit-panel"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateY: 30 }}
animate={{ opacity: 1, translateY: 0 }}
exit={{ opacity: 0, translateY: -30 }}
>
<div className="rounded-lg border border-yellow/30 bg-mineshaft-800 shadow-2xl">
<div className="flex items-center justify-between p-4">
{/* Left Content */}
<div className="flex-1">
{/* Header */}
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-yellow-500" />
<span className="font-medium text-mineshaft-100">Pending Changes</span>
<Badge variant="primary" className="text-xs">
{totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""}
</Badge>
</div>
{/* Description */}
<p className="text-sm leading-5 text-mineshaft-400">
Review pending changes and commit them to apply the updates.
</p>
</div>
{/* Right Buttons */}
<div className="ml-6 mt-0.5 flex items-center gap-3">
<Button
size="sm"
onClick={() =>
clearAllPendingChanges({ workspaceId, environment, secretPath })
}
isDisabled={totalChangesCount === 0}
variant="outline_bg"
className="px-4 hover:border-red/40 hover:bg-red/[0.1]"
>
Discard
</Button>
<Button
variant="solid"
leftIcon={<FontAwesomeIcon icon={faSave} />}
onClick={() => setIsModalOpen(true)}
isDisabled={totalChangesCount === 0}
className="px-6"
>
Save Changes
</Button>
</div>
</div>
</div>
{/* Description */}
<p className="text-sm leading-5 text-mineshaft-400">
Review pending changes and commit them to apply the updates.
</p>
</div>
{/* Right Buttons */}
<div className="ml-6 mt-0.5 flex items-center gap-3">
<Button
size="sm"
onClick={() => clearAllPendingChanges({ workspaceId, environment, secretPath })}
isDisabled={totalChangesCount === 0}
variant="outline_bg"
className="px-4 hover:border-red/40 hover:bg-red/[0.1]"
>
Discard
</Button>
<Button
variant="solid"
leftIcon={<FontAwesomeIcon icon={faSave} />}
onClick={() => setIsModalOpen(true)}
isDisabled={totalChangesCount === 0}
className="px-6"
>
Save Changes
</Button>
</div>
</div>
</motion.div>
</AnimatePresence>
</div>
)}
@@ -396,9 +396,11 @@ export const CreateReminderForm = ({
open: isDatePickerOpen,
onOpenChange: setIsDatePickerOpen
}}
popUpContentProps={{}}
popUpContentProps={{
align: "end"
}}
hideTime
hidden={{ before: new Date(Date.now() + ONE_DAY_IN_MILLIS) }}
disabled={{ before: new Date(Date.now() + ONE_DAY_IN_MILLIS) }}
/>
</FormControl>
)}
@@ -426,9 +428,11 @@ export const CreateReminderForm = ({
open: isDatePickerOpen,
onOpenChange: setIsDatePickerOpen
}}
popUpContentProps={{}}
popUpContentProps={{
align: "start"
}}
hideTime
hidden={{ before: new Date(Date.now() + ONE_DAY_IN_MILLIS) }}
disabled={{ before: new Date(Date.now() + ONE_DAY_IN_MILLIS) }}
/>
</FormControl>
</div>
@@ -479,15 +483,15 @@ export const CreateReminderForm = ({
/>
{/* Action Buttons */}
<div className="flex items-center space-x-4 pt-4">
<div className="flex items-center space-x-4">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
className=""
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />}
type="submit"
>
{isEditMode ? "Update" : "Create"} reminder
{isEditMode ? "Update" : "Create"} Reminder
</Button>
{isEditMode && (
@@ -499,7 +503,7 @@ export const CreateReminderForm = ({
type="button"
isDisabled={isSubmitting}
>
Delete reminder
Delete Reminder
</Button>
)}
@@ -8,5 +8,12 @@ type Props = {
export const ChecklySyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
return <GenericFieldLabel label="Account">{destinationConfig.accountName}</GenericFieldLabel>;
return (
<>
<GenericFieldLabel label="Account">{destinationConfig.accountName}</GenericFieldLabel>
{destinationConfig.groupId && (
<GenericFieldLabel label="Group">{destinationConfig.groupName}</GenericFieldLabel>
)}
</>
);
};
@@ -1,23 +1,50 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import {
useRenderConnectionListEnvironmentGroups,
useRenderConnectionListServices
} from "@app/hooks/api/appConnections/render";
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
type Props = {
secretSync: TRenderSync;
};
export const RenderSyncDestinationSection = ({ secretSync }: Props) => {
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
const { data: services = [], isPending } = useRenderConnectionListServices(
secretSync.connectionId
secretSync.connectionId,
{
enabled: isServiceScope
}
);
const {
destinationConfig: { serviceId }
} = secretSync;
if (isPending) {
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
const { data: groups = [], isPending: isGroupsPending } =
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
switch (secretSync.destinationConfig.scope) {
case RenderSyncScope.Service: {
const id = secretSync.destinationConfig.serviceId;
if (isPending) {
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
}
const serviceName = services.find((service) => service.id === id)?.name;
return <GenericFieldLabel label="Service">{serviceName ?? id}</GenericFieldLabel>;
}
case RenderSyncScope.EnvironmentGroup: {
const id = secretSync.destinationConfig.environmentGroupId;
if (isGroupsPending) {
return <GenericFieldLabel label="Environment Group">Loading...</GenericFieldLabel>;
}
const envName = groups.find((g) => g.id === id)?.name;
return <GenericFieldLabel label="Environment Group">{envName ?? id}</GenericFieldLabel>;
}
default:
throw new Error("Unknown render sync destination scope");
}
const serviceName = services.find((service) => service.id === serviceId)?.name;
return <GenericFieldLabel label="Service">{serviceName ?? serviceId}</GenericFieldLabel>;
};