mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 18:29:28 +00:00
Merge remote-tracking branch 'origin/main' into feat/azurePkiConnector
This commit is contained in:
+46
-2
@@ -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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+70
-24
@@ -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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-2
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-4
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+9
-1
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-1
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+147
-52
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+29
-26
@@ -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}
|
||||
|
||||
+16
-153
@@ -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">
|
||||
|
||||
+17
-18
@@ -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)}
|
||||
|
||||
+54
-13
@@ -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} />;
|
||||
};
|
||||
|
||||
+12
-4
@@ -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;
|
||||
|
||||
+67
-11
@@ -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>
|
||||
|
||||
+35
-8
@@ -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) => {
|
||||
|
||||
+34
-5
@@ -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}
|
||||
|
||||
+40
@@ -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>
|
||||
);
|
||||
};
|
||||
+95
-28
@@ -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>
|
||||
|
||||
+55
-40
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
+12
-8
@@ -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
-1
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+38
-11
@@ -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>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user