Fix merge conflicts

This commit is contained in:
Tuan Dang
2025-05-02 13:06:18 -07:00
233 changed files with 8799 additions and 1213 deletions
+37
View File
@@ -31,6 +31,25 @@ const codeInputProps = {
}
} as const;
const codeInputPropsPhone = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "40px",
borderRadius: "5px",
fontSize: "24px",
height: "40px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
type Props = {
successCallback: () => void | Promise<void>;
closeMfa?: () => void;
@@ -172,6 +191,24 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
</div>
)}
</div>
<div className="mx-auto mt-4 block w-max min-w-[18rem] md:hidden">
{method === MfaMethod.EMAIL && (
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
className="mb-2 mt-2"
{...codeInputPropsPhone}
/>
)}
{method === MfaMethod.TOTP && (
<div className="mb-4 mt-2">
<Input value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} />
</div>
)}
</div>
{typeof triesLeft === "number" && (
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
)}
@@ -0,0 +1,86 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2";
import { useHCVaultConnectionListMounts } from "@app/hooks/api/appConnections/hc-vault";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const HCVaultSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.HCVault }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: mounts, isLoading: isMountsLoading } = useHCVaultConnectionListMounts(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.mount", "");
setValue("destinationConfig.path", "");
}}
/>
<Controller
name="destinationConfig.mount"
control={control}
render={({ field: { onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Secrets Engine Mount"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the Secrets Engine mount exists and that your App Role / Access Token has permission to access it. Infisical only supports version 2 KV Secrets Engine mounts. If you're using Hashicorp Cloud Platform, ensure that you correctly defined your 'namespace' when creating the App Connection."
>
<div>
<span>Don&#39;t see the mount you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isMountsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
onChange={(option) =>
onChange((option as SingleValue<{ value: string }>)?.value ?? null)
}
options={mounts?.map((v) => ({ label: v, value: v }))}
placeholder="Select a Secrets Engine Mount..."
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.path"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-sm"
tooltipText="The Secrets Engine mount path where secrets should be synced to. If the path does not exist, it will be created."
isError={Boolean(error)}
errorText={error?.message}
label="Path"
>
<Input value={value} onChange={onChange} placeholder="dev/example" />
</FormControl>
)}
/>
</>
);
};
@@ -11,6 +11,7 @@ import { CamundaSyncFields } from "./CamundaSyncFields";
import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
import { HCVaultSyncFields } from "./HCVaultSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { TeamCitySyncFields } from "./TeamCitySyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
@@ -47,6 +48,8 @@ export const SecretSyncDestinationFields = () => {
return <VercelSyncFields />;
case SecretSync.Windmill:
return <WindmillSyncFields />;
case SecretSync.HCVault:
return <HCVaultSyncFields />;
case SecretSync.TeamCity:
return <TeamCitySyncFields />;
default:
@@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
case SecretSync.HCVault:
case SecretSync.TeamCity:
AdditionalSyncOptionsFieldsComponent = null;
break;
@@ -0,0 +1,18 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const HCVaultSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.HCVault }>();
const mount = watch("destinationConfig.mount");
const path = watch("destinationConfig.path");
return (
<>
<GenericFieldLabel label="Secrets Engine Mount">{mount}</GenericFieldLabel>
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
</>
);
};
@@ -21,6 +21,7 @@ import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
@@ -89,6 +90,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Windmill:
DestinationFieldsComponent = <WindmillSyncReviewFields />;
break;
case SecretSync.HCVault:
DestinationFieldsComponent = <HCVaultSyncReviewFields />;
break;
case SecretSync.TeamCity:
DestinationFieldsComponent = <TeamCitySyncReviewFields />;
break;
@@ -0,0 +1,22 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const HCVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.HCVault),
destinationConfig: z.object({
mount: z.string().trim().min(1, "Secrets Engine Mount required"),
path: z
.string()
.trim()
.min(1, "Path required")
.transform((val) => val.trim().replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes
.refine((val) => /^([a-zA-Z0-9._-]+\/)*[a-zA-Z0-9._-]+$/.test(val), {
message:
"Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments."
})
})
})
);
@@ -8,6 +8,7 @@ import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema";
import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema";
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
@@ -27,6 +28,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
CamundaSyncDestinationSchema,
VercelSyncDestinationSchema,
WindmillSyncDestinationSchema,
HCVaultSyncDestinationSchema,
TeamCitySyncDestinationSchema
]);
@@ -47,8 +47,10 @@ AccordionTrigger.displayName = "AccordionTrigger";
export const AccordionContent = forwardRef<
HTMLDivElement,
AccordionPrimitive.AccordionContentProps
>(({ children, className, ...props }, forwardedRef) => (
AccordionPrimitive.AccordionContentProps & {
childrenClassName?: string;
}
>(({ children, className, childrenClassName, ...props }, forwardedRef) => (
<AccordionPrimitive.Content
className={twMerge(
"overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown",
@@ -57,7 +59,7 @@ export const AccordionContent = forwardRef<
{...props}
ref={forwardedRef}
>
<div className="px-4 py-2 text-sm">{children}</div>
<div className={twMerge("px-4 py-2 text-sm", childrenClassName)}>{children}</div>
</AccordionPrimitive.Content>
));
+7 -1
View File
@@ -17,6 +17,12 @@ export const ROUTE_PATHS = Object.freeze({
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
},
Organization: {
Settings: {
OauthCallbackPage: setRoute(
"/organization/settings/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
)
},
SecretScanning: setRoute(
"/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
@@ -31,7 +37,7 @@ export const ROUTE_PATHS = Object.freeze({
),
SettingsPage: setRoute(
"/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
),
GroupDetailsByIDPage: setRoute(
"/organization/groups/$groupId",
+5
View File
@@ -19,6 +19,7 @@ import {
DatabricksConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
HCVaultConnectionMethod,
HumanitecConnectionMethod,
LdapConnectionMethod,
MsSqlConnectionMethod,
@@ -58,6 +59,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" },
[AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" },
[AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 },
[AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 },
[AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 },
[AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }
};
@@ -88,11 +90,14 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
return { name: "Username & Password", icon: faLock };
case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken:
case WindmillConnectionMethod.AccessToken:
return { name: "Access Token", icon: faKey };
case Auth0ConnectionMethod.ClientCredentials:
return { name: "Client Credentials", icon: faServer };
case HCVaultConnectionMethod.AppRole:
return { name: "App Role", icon: faUser };
case LdapConnectionMethod.SimpleBind:
return { name: "Simple Bind", icon: faLink };
default:
+5
View File
@@ -40,6 +40,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
name: "Windmill",
image: "Windmill.png"
},
[SecretSync.HCVault]: {
name: "Hashicorp Vault",
image: "Vault.png"
},
[SecretSync.TeamCity]: {
name: "TeamCity",
image: "TeamCity.png"
@@ -59,6 +63,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel,
[SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault,
[SecretSync.TeamCity]: AppConnection.TeamCity
};
+1 -2
View File
@@ -4,13 +4,12 @@ export {
useAdminRemoveIdentitySuperAdminAccess,
useCreateAdminUser,
useRemoveUserServerAdminAccess,
useUpdateAdminSlackConfig,
useUpdateServerConfig,
useUpdateServerEncryptionStrategy
} from "./mutation";
export {
useAdminGetUsers,
useGetAdminSlackConfig,
useGetAdminIntegrationsConfig,
useGetServerConfig,
useGetServerRootKmsEncryptionDetails
} from "./queries";
+3 -24
View File
@@ -6,11 +6,10 @@ import { organizationKeys } from "../organization/queries";
import { User } from "../users/types";
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import {
AdminSlackConfig,
RootKeyEncryptionStrategy,
TCreateAdminUserDTO,
TServerConfig,
TUpdateAdminSlackConfigDTO
TUpdateServerConfigDTO
} from "./types";
export const useCreateAdminUser = () => {
@@ -34,11 +33,7 @@ export const useCreateAdminUser = () => {
export const useUpdateServerConfig = () => {
const queryClient = useQueryClient();
return useMutation<
TServerConfig,
object,
Partial<TServerConfig & { slackClientId: string; slackClientSecret: string }>
>({
return useMutation<TServerConfig, object, TUpdateServerConfigDTO>({
mutationFn: async (opt) => {
const { data } = await apiRequest.patch<{ config: TServerConfig }>(
"/api/v1/admin/config",
@@ -48,6 +43,7 @@ export const useUpdateServerConfig = () => {
},
onSuccess: (data) => {
queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminIntegrationsConfig() });
queryClient.invalidateQueries({ queryKey: adminQueryKeys.serverConfig() });
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
}
@@ -119,23 +115,6 @@ export const useAdminGrantServerAdminAccess = () => {
});
};
export const useUpdateAdminSlackConfig = () => {
const queryClient = useQueryClient();
return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.put<AdminSlackConfig>(
"/api/v1/admin/integrations/slack/config",
dto
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminSlackConfig() });
}
});
};
export const useUpdateServerEncryptionStrategy = () => {
const queryClient = useQueryClient();
return useMutation({
+5 -7
View File
@@ -7,7 +7,7 @@ import { User } from "../types";
import {
AdminGetIdentitiesFilters,
AdminGetUsersFilters,
AdminSlackConfig,
AdminIntegrationsConfig,
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
@@ -22,7 +22,7 @@ export const adminQueryKeys = {
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getIdentities: (filters: AdminGetIdentitiesFilters) =>
[adminStandaloneKeys.getIdentities, { filters }] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
};
@@ -95,13 +95,11 @@ export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => {
});
};
export const useGetAdminSlackConfig = () => {
export const useGetAdminIntegrationsConfig = () => {
return useQuery({
queryKey: adminQueryKeys.getAdminSlackConfig(),
queryKey: adminQueryKeys.getAdminIntegrationsConfig(),
queryFn: async () => {
const { data } = await apiRequest.get<AdminSlackConfig>(
"/api/v1/admin/integrations/slack/config"
);
const { data } = await apiRequest.get<AdminIntegrationsConfig>("/api/v1/admin/integrations");
return data;
}
+18 -8
View File
@@ -26,6 +26,14 @@ export type TServerConfig = {
pageFrameContent?: string;
};
export type TUpdateServerConfigDTO = {
slackClientId?: string;
slackClientSecret?: string;
microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string;
} & Partial<TServerConfig>;
export type TCreateAdminUserDTO = {
email: string;
password: string;
@@ -42,11 +50,6 @@ export type TCreateAdminUserDTO = {
salt: string;
};
export type TUpdateAdminSlackConfigDTO = {
clientId: string;
clientSecret: string;
};
export type AdminGetUsersFilters = {
limit: number;
searchTerm: string;
@@ -58,9 +61,16 @@ export type AdminGetIdentitiesFilters = {
searchTerm: string;
};
export type AdminSlackConfig = {
clientId: string;
clientSecret: string;
export type AdminIntegrationsConfig = {
slack: {
clientId: string;
clientSecret: string;
};
microsoftTeams: {
appId: string;
clientSecret: string;
botId: string;
};
};
export type TGetServerRootKmsEncryptionDetails = {
@@ -14,6 +14,7 @@ export enum AppConnection {
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0",
HCVault = "hashicorp-vault",
LDAP = "ldap",
TeamCity = "teamcity"
}
@@ -0,0 +1 @@
export * from "./queries";
@@ -0,0 +1,36 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
const hcVaultConnectionKeys = {
all: [...appConnectionKeys.all, "hcvault"] as const,
listMounts: (connectionId: string) =>
[...hcVaultConnectionKeys.all, "mounts", connectionId] as const
};
export const useHCVaultConnectionListMounts = (
connectionId: string,
options?: Omit<
UseQueryOptions<
string[],
unknown,
string[],
ReturnType<typeof hcVaultConnectionKeys.listMounts>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: hcVaultConnectionKeys.listMounts(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<string[]>(
`/api/v1/app-connections/hashicorp-vault/${connectionId}/mounts`
);
return data;
},
...options
});
};
@@ -72,6 +72,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Auth0;
};
export type THCVaultConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.HCVault;
};
export type TLdapConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.LDAP;
};
@@ -96,6 +100,7 @@ export type TAppConnectionOption =
| TCamundaConnectionOption
| TWindmillConnectionOption
| TAuth0ConnectionOption
| THCVaultConnectionOption
| TTeamCityConnectionOption;
export type TAppConnectionOptionMap = {
@@ -114,6 +119,7 @@ export type TAppConnectionOptionMap = {
[AppConnection.Camunda]: TCamundaConnectionOption;
[AppConnection.Windmill]: TWindmillConnectionOption;
[AppConnection.Auth0]: TAuth0ConnectionOption;
[AppConnection.HCVault]: THCVaultConnectionOption;
[AppConnection.LDAP]: TLdapConnectionOption;
[AppConnection.TeamCity]: TTeamCityConnectionOption;
};
@@ -0,0 +1,27 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum HCVaultConnectionMethod {
AccessToken = "access-token",
AppRole = "app-role"
}
export type THCVaultConnection = TRootAppConnection & { app: AppConnection.HCVault } & (
| {
method: HCVaultConnectionMethod.AccessToken;
credentials: {
instanceUrl: string;
namespace?: string;
accessToken: string;
};
}
| {
method: HCVaultConnectionMethod.AppRole;
credentials: {
instanceUrl: string;
namespace?: string;
roleId: string;
secretId: string;
};
}
);
@@ -9,6 +9,7 @@ import { TCamundaConnection } from "./camunda-connection";
import { TDatabricksConnection } from "./databricks-connection";
import { TGcpConnection } from "./gcp-connection";
import { TGitHubConnection } from "./github-connection";
import { THCVaultConnection } from "./hc-vault-connection";
import { THumanitecConnection } from "./humanitec-connection";
import { TLdapConnection } from "./ldap-connection";
import { TMsSqlConnection } from "./mssql-connection";
@@ -27,6 +28,7 @@ export * from "./camunda-connection";
export * from "./databricks-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export * from "./hc-vault-connection";
export * from "./humanitec-connection";
export * from "./ldap-connection";
export * from "./mssql-connection";
@@ -52,6 +54,7 @@ export type TAppConnection =
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
| THCVaultConnection
| TLdapConnection
| TTeamCityConnection;
@@ -96,6 +99,7 @@ export type TAppConnectionMap = {
[AppConnection.Camunda]: TCamundaConnection;
[AppConnection.Windmill]: TWindmillConnection;
[AppConnection.Auth0]: TAuth0Connection;
[AppConnection.HCVault]: THCVaultConnection;
[AppConnection.LDAP]: TLdapConnection;
[AppConnection.TeamCity]: TTeamCityConnection;
};
+20 -3
View File
@@ -94,8 +94,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
"Create certificate template EST configuration",
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
"Update certificate template EST configuration",
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
[EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
"Update project workflow integration configuration",
[EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
"Get project workflow integration configuration",
[EventType.INTEGRATION_SYNCED]: "Integration sync",
[EventType.CREATE_SHARED_SECRET]: "Create shared secret",
[EventType.DELETE_SHARED_SECRET]: "Delete shared secret",
@@ -163,7 +165,22 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_SECRET_ROTATION]: "Create Secret Rotation",
[EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation",
[EventType.DELETE_SECRET_ROTATION]: "Delete Secret Rotation",
[EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated"
[EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated",
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get Project Slack Config",
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update Project Slack Config",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE]:
"Create Microsoft Teams Workflow Integration",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE]:
"Delete Microsoft Teams Workflow Integration",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE]:
"Update Microsoft Teams Workflow Integration",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS]:
"Microsoft Teams Workflow Integration Check Installation Status",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS]: "Get Microsoft Teams tenant teams",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET]: "Get Microsoft Teams Workflow Integration",
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: "List Microsoft Teams Workflow Integration"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+14 -3
View File
@@ -98,8 +98,8 @@ export enum EventType {
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
INTEGRATION_SYNCED = "integration-synced",
CREATE_SHARED_SECRET = "create-shared-secret",
DELETE_SHARED_SECRET = "delete-shared-secret",
@@ -163,5 +163,16 @@ export enum EventType {
CREATE_SECRET_ROTATION = "create-secret-rotation",
UPDATE_SECRET_ROTATION = "update-secret-rotation",
DELETE_SECRET_ROTATION = "delete-secret-rotation",
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets"
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list"
}
+10 -7
View File
@@ -1,6 +1,7 @@
import { CaStatus } from "../ca";
import { IdentityTrustedIp } from "../identities/types";
import { PkiItemType } from "../pkiCollections/constants";
import { WorkflowIntegration } from "../workflowIntegrations/types";
import { ActorType, EventType, UserAgentType } from "./enums";
export type TGetAuditLogsFilter = {
@@ -786,11 +787,12 @@ interface GetCertificateTemplateEstConfig {
};
}
interface UpdateProjectSlackConfig {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
interface UpdateProjectWorkflowIntegrationConfig {
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: {
id: string;
slackIntegrationId: string;
integrationId: string;
integration: WorkflowIntegration;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
@@ -798,10 +800,11 @@ interface UpdateProjectSlackConfig {
};
}
interface GetProjectSlackConfig {
type: EventType.GET_PROJECT_SLACK_CONFIG;
interface GetProjectWorkflowIntegrationConfig {
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: {
id: string;
integration: WorkflowIntegration;
};
}
@@ -898,8 +901,8 @@ export type Event =
| UpdateCertificateTemplateEstConfig
| CreateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig
| UpdateProjectSlackConfig
| GetProjectSlackConfig
| UpdateProjectWorkflowIntegrationConfig
| GetProjectWorkflowIntegrationConfig
| IntegrationSyncedEvent;
export type AuditLog = {
+1
View File
@@ -107,6 +107,7 @@ export type CompleteAccountSignupDTO = CompleteAccountDTO & {
providerAuthToken?: string;
attributionSource?: string;
organizationName: string;
useDefaultOrg?: boolean;
};
export type VerifySignupInviteDTO = {
@@ -11,6 +11,7 @@ export enum SecretSync {
Camunda = "camunda",
Vercel = "vercel",
Windmill = "windmill",
HCVault = "hashicorp-vault",
TeamCity = "teamcity"
}
@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type THCVaultSync = TRootSecretSync & {
destination: SecretSync.HCVault;
destinationConfig: {
mount: string;
path: string;
};
connection: {
app: AppConnection.HCVault;
name: string;
id: string;
};
};
@@ -9,6 +9,7 @@ import { TCamundaSync } from "./camunda-sync";
import { TDatabricksSync } from "./databricks-sync";
import { TGcpSync } from "./gcp-sync";
import { TGitHubSync } from "./github-sync";
import { THCVaultSync } from "./hc-vault-sync";
import { THumanitecSync } from "./humanitec-sync";
import { TTeamCitySync } from "./teamcity-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync";
@@ -34,6 +35,7 @@ export type TSecretSync =
| TCamundaSync
| TVercelSync
| TWindmillSync
| THCVaultSync
| TTeamCitySync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
@@ -1,13 +1,2 @@
export {
useDeleteSlackIntegration,
useUpdateProjectSlackConfig,
useUpdateSlackIntegration
} from "./mutation";
export {
fetchSlackInstallUrl,
fetchSlackReinstallUrl,
useGetSlackIntegrationById,
useGetSlackIntegrationChannels,
useGetSlackIntegrations,
useGetWorkflowIntegrations
} from "./queries";
export * from "./mutation";
export * from "./queries";
@@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/query-keys";
import { workflowIntegrationKeys } from "./queries";
import {
TCheckMicrosoftTeamsIntegrationInstallationStatusDTO,
TCreateMicrosoftTeamsIntegrationDTO,
TDeleteMicrosoftTeamsIntegrationDTO,
TDeleteProjectWorkflowIntegrationDTO,
TDeleteSlackIntegrationDTO,
TUpdateProjectSlackConfigDTO,
TUpdateMicrosoftTeamsIntegrationDTO,
TUpdateProjectWorkflowIntegrationConfigDTO,
TUpdateSlackIntegrationDTO
} from "./types";
@@ -28,6 +33,47 @@ export const useUpdateSlackIntegration = () => {
});
};
export const useUpdateMicrosoftTeamsIntegration = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TUpdateMicrosoftTeamsIntegrationDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.patch(
`/api/v1/workflow-integrations/microsoft-teams/${dto.id}`,
dto
);
return data;
},
onSuccess: (_, { orgId, id }) => {
queryClient.invalidateQueries({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id)
});
queryClient.invalidateQueries({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
});
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
}
});
};
export const useCreateMicrosoftTeamsIntegration = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TCreateMicrosoftTeamsIntegrationDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post("/api/v1/workflow-integrations/microsoft-teams", dto);
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
});
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
}
});
};
export const useDeleteSlackIntegration = () => {
const queryClient = useQueryClient();
@@ -44,21 +90,86 @@ export const useDeleteSlackIntegration = () => {
});
};
export const useUpdateProjectSlackConfig = () => {
export const useDeleteMicrosoftTeamsIntegration = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TDeleteMicrosoftTeamsIntegrationDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.delete(
`/api/v1/workflow-integrations/microsoft-teams/${dto.id}`
);
return data;
},
onSuccess: (_, { orgId, id }) => {
queryClient.invalidateQueries({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id)
});
queryClient.invalidateQueries({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
});
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
}
});
};
export const useUpdateProjectWorkflowIntegrationConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: TUpdateProjectSlackConfigDTO) => {
mutationFn: async (dto: TUpdateProjectWorkflowIntegrationConfigDTO) => {
const { data } = await apiRequest.put(
`/api/v1/workspace/${dto.workspaceId}/slack-config`,
`/api/v1/workspace/${dto.workspaceId}/workflow-integration`,
dto
);
return data;
},
onSuccess: (_, { workspaceId }) => {
onSuccess: (_, { workspaceId, integration }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId)
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration)
});
}
});
};
export const useDeleteProjectWorkflowIntegration = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: TDeleteProjectWorkflowIntegrationDTO) => {
const { data } = await apiRequest.delete(
`/api/v1/workspace/${dto.projectId}/workflow-integration/${dto.integration}/${dto.integrationId}`
);
return data;
},
onSuccess: (_, { projectId, integration }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(projectId, integration)
});
}
});
};
export const useCheckMicrosoftTeamsIntegrationInstallationStatus = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: TCheckMicrosoftTeamsIntegrationInstallationStatusDTO) => {
const { data } = await apiRequest.post(
`/api/v1/workflow-integrations/microsoft-teams/${dto.workflowIntegrationId}/installation-status`
);
return data;
},
onSuccess: (_, { workflowIntegrationId, orgId }) => {
queryClient.invalidateQueries({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(workflowIntegrationId)
});
queryClient.invalidateQueries({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
});
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
}
});
};
@@ -2,13 +2,29 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { SlackIntegration, SlackIntegrationChannel, WorkflowIntegration } from "./types";
import {
MicrosoftTeamsIntegration,
MicrosoftTeamsIntegrationTeam,
SlackIntegration,
SlackIntegrationChannel,
WorkflowIntegration
} from "./types";
export const workflowIntegrationKeys = {
getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"],
getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"],
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"]
getMicrosoftTeamsIntegrations: (orgId?: string) => [
{ orgId },
"microsoft-teams-workflow-integrations"
],
getMicrosoftTeamsIntegration: (id?: string) => [{ id }, "microsoft-teams-workflow-integration"],
getMicrosoftTeamsIntegrationTeams: (id?: string) => [
{ id },
"microsoft-teams-workflow-integration-teams"
],
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"],
getMicrosoftTeamsClientId: () => ["microsoft-teams-client-id"]
};
export const fetchSlackInstallUrl = async ({
@@ -66,13 +82,58 @@ export const fetchWorkflowIntegrations = async () => {
return data;
};
export const useGetSlackIntegrations = (orgId?: string) =>
export const fetchMicrosoftTeamsIntegrations = async () => {
const { data } = await apiRequest.get<MicrosoftTeamsIntegration[]>(
"/api/v1/workflow-integrations/microsoft-teams"
);
return data;
};
export const fetchMicrosoftTeamsIntegrationById = async (id?: string) => {
const { data } = await apiRequest.get<MicrosoftTeamsIntegration>(
`/api/v1/workflow-integrations/microsoft-teams/${id}`
);
return data;
};
export const fetchMicrosoftTeamsIntegrationTeams = async (id?: string) => {
const { data } = await apiRequest.get<MicrosoftTeamsIntegrationTeam[]>(
`/api/v1/workflow-integrations/microsoft-teams/${id}/teams`
);
return data;
};
export const fetchMicrosoftTeamsClientId = async () => {
const { data } = await apiRequest.get<{ clientId: string }>(
"/api/v1/workflow-integrations/microsoft-teams/client-id"
);
return data;
};
export const useGetMicrosoftTeamsIntegrations = (orgId?: string) =>
useQuery({
queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId),
queryFn: () => fetchSlackIntegrations(),
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId),
queryFn: () => fetchMicrosoftTeamsIntegrations(),
enabled: Boolean(orgId)
});
export const useGetMicrosoftTeamsIntegrationById = (id?: string) =>
useQuery({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id),
queryFn: () => fetchMicrosoftTeamsIntegrationById(id),
enabled: Boolean(id)
});
export const useGetMicrosoftTeamsIntegrationTeams = (id?: string) =>
useQuery({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrationTeams(id),
queryFn: () => fetchMicrosoftTeamsIntegrationTeams(id),
enabled: Boolean(id)
});
export const useGetSlackIntegrationById = (id?: string) =>
useQuery({
queryKey: workflowIntegrationKeys.getSlackIntegration(id),
@@ -93,3 +154,10 @@ export const useGetWorkflowIntegrations = (id?: string) =>
queryFn: () => fetchWorkflowIntegrations(),
enabled: Boolean(id)
});
export const useGetMicrosoftTeamsClientId = () =>
useQuery({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsClientId(),
queryFn: () => fetchMicrosoftTeamsClientId(),
enabled: true
});
@@ -1,14 +1,25 @@
export enum WorkflowIntegrationPlatform {
SLACK = "slack"
SLACK = "slack",
MICROSOFT_TEAMS = "microsoft-teams"
}
export type WorkflowIntegration = {
id: string;
slug: string;
description: string;
status: WorkflowIntegrationStatus;
integration: WorkflowIntegrationPlatform;
};
export type MicrosoftTeamsIntegrationTeam = {
teamId: string;
teamName: string;
channels: {
channelId: string;
channelName: string;
}[];
};
export type SlackIntegration = {
id: string;
slug: string;
@@ -16,6 +27,18 @@ export type SlackIntegration = {
teamName: string;
};
export enum WorkflowIntegrationStatus {
Pending = "pending",
Installed = "installed"
}
export type MicrosoftTeamsIntegration = {
id: string;
slug: string;
description: string;
tenantId: string;
};
export type SlackIntegrationChannel = {
id: string;
name: string;
@@ -28,25 +51,90 @@ export type TUpdateSlackIntegrationDTO = {
description?: string;
};
export type TUpdateMicrosoftTeamsIntegrationDTO = {
id: string;
orgId: string;
slug?: string;
description?: string;
};
export type TCreateMicrosoftTeamsIntegrationDTO = {
code: string;
tenantId: string;
slug: string;
description?: string;
redirectUri: string;
orgId: string;
};
export type TDeleteSlackIntegrationDTO = {
id: string;
orgId: string;
};
export type ProjectSlackConfig = {
export type TDeleteMicrosoftTeamsIntegrationDTO = {
id: string;
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
orgId: string;
};
export type TUpdateProjectSlackConfigDTO = {
workspaceId: string;
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
export type ProjectWorkflowIntegrationConfig =
| {
id: string;
integration: WorkflowIntegrationPlatform.SLACK;
integrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
}
| {
id: string;
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
integrationId: string;
isAccessRequestNotificationEnabled: boolean;
isSecretRequestNotificationEnabled: boolean;
accessRequestChannels: {
teamId: string;
channelIds: string[];
};
secretRequestChannels: {
teamId: string;
channelIds: string[];
};
};
export type TUpdateProjectWorkflowIntegrationConfigDTO =
| {
integration: WorkflowIntegrationPlatform.SLACK;
workspaceId: string;
integrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
}
| {
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
workspaceId: string;
integrationId: string;
isAccessRequestNotificationEnabled: boolean;
isSecretRequestNotificationEnabled: boolean;
accessRequestChannels?: {
teamId: string;
channelIds: string[];
};
secretRequestChannels?: {
teamId: string;
channelIds: string[];
};
};
export type TDeleteProjectWorkflowIntegrationDTO = {
projectId: string;
integration: WorkflowIntegrationPlatform;
integrationId: string;
};
export type TCheckMicrosoftTeamsIntegrationInstallationStatusDTO = {
workflowIntegrationId: string;
orgId: string;
};
+1 -1
View File
@@ -26,9 +26,9 @@ export {
useGetWorkspaceIndexStatus,
useGetWorkspaceIntegrations,
useGetWorkspaceSecrets,
useGetWorkspaceSlackConfig,
useGetWorkspaceUserDetails,
useGetWorkspaceUsers,
useGetWorkspaceWorkflowIntegrationConfig,
useListWorkspaceCas,
useListWorkspaceCertificates,
useListWorkspaceCertificateTemplates,
+23 -6
View File
@@ -21,7 +21,10 @@ import { TSshHost } from "../sshHost/types";
import { TSshHostGroup } from "../sshHostGroup/types";
import { userKeys } from "../users/query-keys";
import { TWorkspaceUser } from "../users/types";
import { ProjectSlackConfig } from "../workflowIntegrations/types";
import {
ProjectWorkflowIntegrationConfig,
WorkflowIntegrationPlatform
} from "../workflowIntegrations/types";
import { workspaceKeys } from "./query-keys";
import {
CreateEnvironmentDTO,
@@ -899,13 +902,27 @@ export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
});
};
export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => {
export const useGetWorkspaceWorkflowIntegrationConfig = ({
workspaceId,
integration
}: {
workspaceId: string;
integration: WorkflowIntegrationPlatform;
}) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId),
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration),
queryFn: async () => {
const { data } = await apiRequest.get<ProjectSlackConfig>(
`/api/v1/workspace/${workspaceId}/slack-config`
);
const { data } = await apiRequest
.get<ProjectWorkflowIntegrationConfig>(
`/api/v1/workspace/${workspaceId}/workflow-integration-config/${integration}`
)
.catch((err) => {
if (err.response.status === 404) {
return { data: null };
}
throw err;
});
return data;
},
@@ -1,6 +1,7 @@
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types";
import type { CaStatus } from "../ca";
import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types";
export const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const,
@@ -54,8 +55,10 @@ export const workspaceKeys = {
[{ workspaceId }, "workspace-pki-collections"] as const,
getWorkspaceCertificateTemplates: (workspaceId: string) =>
[{ workspaceId }, "workspace-certificate-templates"] as const,
getWorkspaceSlackConfig: (workspaceId: string) =>
[{ workspaceId }, "workspace-slack-config"] as const,
getWorkspaceWorkflowIntegrationConfig: (
workspaceId: string,
integration: WorkflowIntegrationPlatform
) => [{ workspaceId, integration }, "workspace-workflow-integration-config"] as const,
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
allWorkspaceSshCertificates: (projectId: string) =>
[{ projectId }, "workspace-ssh-certificates"] as const,
+1 -1
View File
@@ -197,4 +197,4 @@ html {
position: absolute;
top: 0.5rem;
@apply text-sm text-gray-500 opacity-50;
}
}
@@ -235,7 +235,7 @@ export const OverviewPage = () => {
Default organization
</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select the default organization you want to set for SAML/LDAP/OIDC based
Select the default organization you want to set for SAML/LDAP/OIDC/Github
logins. When selected, user logins will be automatically scoped to the
selected organization.
</div>
@@ -1,166 +1,24 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api";
const slackFormSchema = z.object({
clientId: z.string(),
clientSecret: z.string()
});
type TSlackForm = z.infer<typeof slackFormSchema>;
const getCustomSlackAppCreationUrl = () =>
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
JSON.stringify({
display_information: {
name: "Infisical",
description: "Get real-time Infisical updates in Slack",
background_color: "#c2d62b",
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
},
features: {
app_home: {
home_tab_enabled: false,
messages_tab_enabled: false,
messages_tab_read_only_enabled: true
},
bot_user: {
display_name: "Infisical",
always_online: true
}
},
oauth_config: {
redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect`],
scopes: {
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"]
}
},
settings: {
org_deploy_enabled: false,
socket_mode_enabled: false,
token_rotation_enabled: false
}
})
)}`;
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm";
export const IntegrationPanel = () => {
const {
control,
handleSubmit,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TSlackForm>({
resolver: zodResolver(slackFormSchema)
});
const { data: adminSlackConfig } = useGetAdminSlackConfig();
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
useEffect(() => {
if (adminSlackConfig) {
setValue("clientId", adminSlackConfig.clientId);
setValue("clientSecret", adminSlackConfig.clientSecret);
}
}, [adminSlackConfig]);
const onSlackFormSubmit = async (data: TSlackForm) => {
await updateAdminServerConfig({
slackClientId: data.clientId,
slackClientSecret: data.clientSecret
});
createNotification({
text: "Updated admin slack configuration",
type: "success"
});
};
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
return (
<form
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onSlackFormSubmit)}
>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Slack Integration</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 1: Create your Infisical Slack App
<div className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<div className="text-xl font-semibold text-mineshaft-100">Integrations</div>
<div className="text-sm text-mineshaft-300">
Configure your instance-wide settings to enable integration with Slack and Microsoft
Teams.
</div>
<div className="mb-6">
<Button
colorSchema="secondary"
onClick={() => window.open(getCustomSlackAppCreationUrl())}
>
Create Slack App
</Button>
</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 2: Configure your instance-wide settings to enable integration with Slack. Copy the
values from the App Credentials page of your custom Slack App.
</div>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client ID"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientIdFocused ? "text" : "password"}
onFocus={() => setIsSlackClientIdFocused.on()}
onBlur={() => setIsSlackClientIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientSecretFocused ? "text" : "password"}
onFocus={() => setIsSlackClientSecretFocused.on()}
onBlur={() => setIsSlackClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</form>
<div className="flex flex-col gap-2">
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
</div>
</div>
);
};
@@ -0,0 +1,183 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { BsMicrosoftTeams } from "react-icons/bs";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
FormControl,
Input
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useUpdateServerConfig } from "@app/hooks/api";
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
const microsoftTeamsFormSchema = z.object({
appId: z.string(),
clientSecret: z.string(),
botId: z.string()
});
type TMicrosoftTeamsForm = z.infer<typeof microsoftTeamsFormSchema>;
type Props = {
adminIntegrationsConfig?: AdminIntegrationsConfig;
};
export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
const [isMicrosoftTeamsAppIdFocused, setIsMicrosoftTeamsAppIdFocused] = useToggle();
const [isMicrosoftTeamsClientSecretFocused, setIsMicrosoftTeamsClientSecretFocused] = useToggle();
const [isMicrosoftTeamsBotIdFocused, setIsMicrosoftTeamsBotIdFocused] = useToggle();
const {
control,
handleSubmit,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TMicrosoftTeamsForm>({
resolver: zodResolver(microsoftTeamsFormSchema)
});
const onSubmit = async (data: TMicrosoftTeamsForm) => {
await updateAdminServerConfig({
microsoftTeamsAppId: data.appId,
microsoftTeamsClientSecret: data.clientSecret,
microsoftTeamsBotId: data.botId
});
createNotification({
text: "Updated admin Microsoft Teams configuration",
type: "success"
});
};
useEffect(() => {
if (adminIntegrationsConfig) {
setValue("appId", adminIntegrationsConfig.microsoftTeams.appId);
setValue("clientSecret", adminIntegrationsConfig.microsoftTeams.clientSecret);
setValue("botId", adminIntegrationsConfig.microsoftTeams.botId);
}
}, [adminIntegrationsConfig]);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Accordion type="single" collapsible className="w-full">
<AccordionItem
value="microsoft-teams-integration"
className="data-[state=open]:border-none"
>
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
<div className="text-md group order-1 ml-3 flex items-center gap-2">
<BsMicrosoftTeams className="text-lg group-hover:text-primary-400" />
<div className="text-[15px] font-semibold">Microsoft Teams Integration</div>
</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
<div className="mb-2 max-w-lg text-sm text-mineshaft-300">
Step 1: Create and configure Microsoft Teams bot and Azure Resources. Please refer
to the documentation below for more information.
</div>
<div className="mb-6">
<a
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
target="_blank"
rel="noopener noreferrer"
>
<Button colorSchema="secondary">Documentation</Button>
</a>
</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 2: Configure your instance-wide settings to enable integration with Microsoft
Teams. Copy the App ID and Client Secret from your Microsoft Teams bot&apos;s App
Registration page. The Client Secret is the password for the bot.
</div>
<Controller
control={control}
name="appId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Application (Client) ID"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isMicrosoftTeamsAppIdFocused ? "text" : "password"}
onFocus={() => setIsMicrosoftTeamsAppIdFocused.on()}
onBlur={() => setIsMicrosoftTeamsAppIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
tooltipText="You can find your Client Secret in the Certificates & Secrets section of your Microsoft Teams bot's App Registration."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isMicrosoftTeamsClientSecretFocused ? "text" : "password"}
onFocus={() => setIsMicrosoftTeamsClientSecretFocused.on()}
onBlur={() => setIsMicrosoftTeamsClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="botId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams App ID"
tooltipText="You can find the Microsoft Teams App ID in the overview of your Microsoft Teams App inside the Microsoft Teams Developer Portal."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isMicrosoftTeamsBotIdFocused ? "text" : "password"}
onFocus={() => setIsMicrosoftTeamsBotIdFocused.on()}
onBlur={() => setIsMicrosoftTeamsBotIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</form>
);
};
@@ -0,0 +1,189 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { BsSlack } from "react-icons/bs";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
FormControl,
Input
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useUpdateServerConfig } from "@app/hooks/api";
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
const getCustomSlackAppCreationUrl = () =>
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
JSON.stringify({
display_information: {
name: "Infisical",
description: "Get real-time Infisical updates in Slack",
background_color: "#c2d62b",
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
},
features: {
app_home: {
home_tab_enabled: false,
messages_tab_enabled: false,
messages_tab_read_only_enabled: true
},
bot_user: {
display_name: "Infisical",
always_online: true
}
},
oauth_config: {
redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect`],
scopes: {
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"]
}
},
settings: {
org_deploy_enabled: false,
socket_mode_enabled: false,
token_rotation_enabled: false
}
})
)}`;
const slackFormSchema = z.object({
clientId: z.string(),
clientSecret: z.string()
});
type TSlackForm = z.infer<typeof slackFormSchema>;
type Props = {
adminIntegrationsConfig?: AdminIntegrationsConfig;
};
export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
const {
control,
handleSubmit,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TSlackForm>({
resolver: zodResolver(slackFormSchema)
});
const onSubmit = async (data: TSlackForm) => {
await updateAdminServerConfig({
slackClientId: data.clientId,
slackClientSecret: data.clientSecret
});
createNotification({
text: "Updated admin slack configuration",
type: "success"
});
};
useEffect(() => {
if (adminIntegrationsConfig) {
setValue("clientId", adminIntegrationsConfig.slack.clientId);
setValue("clientSecret", adminIntegrationsConfig.slack.clientSecret);
}
}, [adminIntegrationsConfig]);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="slack-integration" className="data-[state=open]:border-none">
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
<div className="text-md group order-1 ml-3 flex items-center gap-2">
<BsSlack className="text-lg group-hover:text-primary-400" />
<div className="text-[15px] font-semibold">Slack Integration</div>
</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 1: Create your Infisical Slack App
</div>
<div className="mb-6">
<Button
colorSchema="secondary"
onClick={() => window.open(getCustomSlackAppCreationUrl())}
>
Create Slack App
</Button>
</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 2: Configure your instance-wide settings to enable integration with Slack. Copy
the values from the App Credentials page of your custom Slack App.
</div>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client ID"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientIdFocused ? "text" : "password"}
onFocus={() => setIsSlackClientIdFocused.on()}
onBlur={() => setIsSlackClientIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientSecretFocused ? "text" : "password"}
onFocus={() => setIsSlackClientSecretFocused.on()}
onBlur={() => setIsSlackClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</form>
);
};
@@ -13,6 +13,7 @@ export const SignupSsoPage = () => {
const { t } = useTranslation();
const search = useSearch({ from: ROUTE_PATHS.Auth.SignUpSsoPage.id });
const token = search.token as string;
const defaultOrgAllowed = search.defaultOrgAllowed as boolean | undefined;
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
@@ -57,6 +58,7 @@ export const SignupSsoPage = () => {
password={password}
setPassword={setPassword}
providerAuthToken={token}
forceDefaultOrg={defaultOrgAllowed}
/>
);
default:
@@ -30,6 +30,7 @@ type Props = {
name: string;
providerOrganizationName: string;
providerAuthToken?: string;
forceDefaultOrg?: boolean;
};
/**
@@ -51,7 +52,8 @@ export const UserInfoSSOStep = ({
providerOrganizationName,
password,
setPassword,
providerAuthToken
providerAuthToken,
forceDefaultOrg
}: Props) => {
const [nameError, setNameError] = useState(false);
const [organizationName, setOrganizationName] = useState("");
@@ -84,7 +86,7 @@ export const UserInfoSSOStep = ({
} else {
setNameError(false);
}
if (!organizationName) {
if (!organizationName && !forceDefaultOrg) {
setOrganizationNameError(true);
errorCheck = true;
} else {
@@ -160,7 +162,8 @@ export const UserInfoSSOStep = ({
salt: result.salt,
verifier: result.verifier,
organizationName,
attributionSource
attributionSource,
useDefaultOrg: forceDefaultOrg
});
// unset signup JWT token and set JWT token
@@ -267,7 +270,7 @@ export const UserInfoSSOStep = ({
</p>
)}
</div>
{providerOrganizationName === undefined && (
{!forceDefaultOrg && providerOrganizationName === undefined && (
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Organization Name
@@ -279,7 +282,7 @@ export const UserInfoSSOStep = ({
isRequired
className="h-12"
maxLength={64}
disabled
isDisabled={forceDefaultOrg}
/>
{organizationNameError && (
<p className="ml-1 mt-1 w-full text-left text-xs text-red-600">
@@ -5,7 +5,8 @@ import { z } from "zod";
import { SignupSsoPage } from "./SignUpSsoPage";
const SignupSSOPageQueryParamsSchema = z.object({
token: z.string()
token: z.string(),
defaultOrgAllowed: z.boolean().optional()
});
export const Route = createFileRoute("/_restrict-login-signup/signup/sso")({
@@ -97,7 +97,11 @@ export const Route = createFileRoute("/_restrict-login-signup")({
}
if (!data.organizationId) {
if (location.pathname.endsWith("select-organization")) return;
if (
location.pathname.endsWith("select-organization") ||
location.pathname.endsWith("verify-email")
)
return;
throw redirect({ to: "/login/select-organization" });
}
throw redirect({
@@ -18,6 +18,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
@@ -94,6 +95,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <WindmillConnectionForm onSubmit={onSubmit} />;
case AppConnection.Auth0:
return <Auth0ConnectionForm onSubmit={onSubmit} />;
case AppConnection.HCVault:
return <HCVaultConnectionForm onSubmit={onSubmit} />;
case AppConnection.LDAP:
return <LdapConnectionForm onSubmit={onSubmit} />;
case AppConnection.TeamCity:
@@ -164,6 +167,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Auth0:
return <Auth0ConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.HCVault:
return <HCVaultConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.LDAP:
return <LdapConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.TeamCity:
@@ -0,0 +1,224 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: THCVaultConnection;
onSubmit: (formData: FormData) => Promise<void>;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.HCVault)
});
const InstanceUrlSchema = z
.string()
.trim()
.min(1, "Instance URL required")
.url("Invalid Instance URL");
const NamespaceSchema = z.string().trim().optional();
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(HCVaultConnectionMethod.AccessToken),
credentials: z.object({
instanceUrl: InstanceUrlSchema,
namespace: NamespaceSchema,
accessToken: z.string().trim().min(1, "Access Token required")
})
}),
rootSchema.extend({
method: z.literal(HCVaultConnectionMethod.AppRole),
credentials: z.object({
instanceUrl: InstanceUrlSchema,
namespace: NamespaceSchema,
roleId: z.string().trim().min(1, "Role ID required"),
secretId: z.string().trim().min(1, "Secret ID required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.HCVault,
method: HCVaultConnectionMethod.AppRole
}
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.HCVault].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(HCVaultConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
{method === HCVaultConnectionMethod.AppRole ? " (Recommended)" : ""}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.instanceUrl"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Instance URL"
tooltipClassName="max-w-sm"
tooltipText="The URL at which your Hashicorp Vault instance is hosted."
>
<Input {...field} placeholder="https://vault.example.com" />
</FormControl>
)}
/>
<Controller
name="credentials.namespace"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Namespace"
isOptional
tooltipClassName="max-w-sm"
tooltipText="On self-hosted and enterprise clusters there may not be namespaces."
>
<Input {...field} placeholder="admin" />
</FormControl>
)}
/>
{selectedMethod === HCVaultConnectionMethod.AccessToken ? (
<Controller
name="credentials.accessToken"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Access Token"
>
<SecretInput
{...field}
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
/>
</FormControl>
)}
/>
) : (
<>
<Controller
name="credentials.roleId"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Role ID"
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
</FormControl>
)}
/>
<Controller
name="credentials.secretId"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Secret ID"
>
<SecretInput
{...field}
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
/>
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Hashicorp Vault"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,97 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ContentLoader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization } from "@app/context";
import { useCreateMicrosoftTeamsIntegration } from "@app/hooks/api";
const stateSchema = z.object({
redirectUri: z.string(),
tenantId: z.string(),
slug: z.string(),
description: z.string().optional(),
csrfToken: z.string(),
clientId: z.string()
});
export const OAuthCallbackPage = () => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const [isReady, setIsReady] = useState(false);
const search = useSearch({
from: ROUTE_PATHS.Organization.Settings.OauthCallbackPage.id
});
const createMicrosoftTeamsWorkflowIntegration = useCreateMicrosoftTeamsIntegration();
const { state: rawState, code } = search;
const state = stateSchema.parse(rawState);
const clearState = () => {
if (state.csrfToken !== localStorage.getItem("latestCSRFToken")) {
throw new Error("Invalid CSRF token");
}
localStorage.removeItem("latestCSRFToken");
};
const handleMicrosoftTeams = useCallback(async () => {
clearState();
if (!code) {
throw new Error("No code provided");
}
await createMicrosoftTeamsWorkflowIntegration.mutateAsync({
orgId: currentOrg.id,
tenantId: state.tenantId,
code,
slug: state.slug,
description: state.description ?? "",
redirectUri: state.redirectUri
});
navigate({
to: ROUTE_PATHS.Organization.SettingsPage.path
});
}, []);
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
useEffect(() => {
if (!isReady) {
setIsReady(!!localStorage.length);
}
}, [localStorage.length]);
useEffect(() => {
if (!isReady) return;
(async () => {
try {
await handleMicrosoftTeams();
createNotification({
text: "Successfully created Microsoft Teams workflow integration",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create Microsoft Teams workflow integration",
type: "error"
});
}
})();
}, [isReady]);
return (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader text="Please wait! Authentication in process." />
</div>
);
};
@@ -0,0 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { OAuthCallbackPage } from "./OauthCallbackPage";
const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
state: z
.object({
clientId: z.string(),
tenantId: z.string(),
slug: z.string(),
description: z.string().optional(),
redirectUri: z.string(),
csrfToken: z.string()
})
.nullable()
.catch(null),
code: z.string().catch("")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
)({
component: OAuthCallbackPage,
validateSearch: zodValidator(SettingsOAuthCallbackPageQueryParamsSchema)
});
@@ -1,4 +1,5 @@
import { useState } from "react";
import { BsMicrosoftTeams } from "react-icons/bs";
import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
@@ -6,6 +7,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm";
type Props = {
@@ -20,9 +22,14 @@ enum WizardSteps {
const PLATFORM_LIST = [
{
icon: faSlack,
icon: <FontAwesomeIcon icon={faSlack} size="lg" />,
platform: WorkflowIntegrationPlatform.SLACK,
title: "Slack"
},
{
icon: <BsMicrosoftTeams className="text-lg" />,
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
title: "Microsoft Teams"
}
];
@@ -38,7 +45,10 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
<ModalContent title={`Add a ${selectedPlatform ?? "workflow"} integration`} className="my-4">
<ModalContent
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
className="my-4"
>
<AnimatePresence mode="wait">
{wizardStep === WizardSteps.SelectPlatform && (
<motion.div
@@ -67,7 +77,7 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
}
}}
>
<FontAwesomeIcon icon={icon} size="lg" />
<div>{icon}</div>
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
</div>
))}
@@ -86,6 +96,18 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
<SlackIntegrationForm onClose={() => onToggle(false)} />
</motion.div>
)}
{wizardStep === WizardSteps.PlatformInputs &&
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<motion.div
key="microsoft-teams-platform"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<MicrosoftTeamsIntegrationForm onClose={() => onToggle(false)} />
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>
@@ -1,6 +1,7 @@
import { Modal, ModalContent } from "@app/components/v2";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm";
type Props = {
@@ -20,6 +21,9 @@ export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatf
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
)}
{workflowPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<MicrosoftTeamsIntegrationForm id={id} onClose={() => onOpenChange(false)} />
)}
</ModalContent>
</Modal>
);
@@ -0,0 +1,192 @@
import crypto from "crypto";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetMicrosoftTeamsClientId,
useGetMicrosoftTeamsIntegrationById,
useUpdateMicrosoftTeamsIntegration
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
type Props = {
id?: string;
onClose: () => void;
};
const microsoftTeamsFormSchema = z.object({
slug: slugSchema({ min: 1, field: "Alias" }),
tenantId: z
.string()
.min(1, { message: "Tenant ID is required" })
.trim()
.uuid("Tenant ID must be a valid UUID"),
description: z.string().optional()
});
type TMicrosoftTeamsFormData = z.infer<typeof microsoftTeamsFormSchema>;
export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
const {
control,
handleSubmit,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TMicrosoftTeamsFormData>({
resolver: zodResolver(microsoftTeamsFormSchema)
});
const { currentOrg } = useOrganization();
const { data: microsoftTeamsIntegration } = useGetMicrosoftTeamsIntegrationById(id);
const { mutateAsync: updateMicrosoftTeamsIntegration } = useUpdateMicrosoftTeamsIntegration();
const { data: microsoftTeamsClientId } = useGetMicrosoftTeamsClientId();
useEffect(() => {
if (microsoftTeamsIntegration) {
setValue("slug", microsoftTeamsIntegration.slug);
setValue("description", microsoftTeamsIntegration.description ?? "");
setValue("tenantId", microsoftTeamsIntegration.tenantId);
}
}, [microsoftTeamsIntegration]);
const handleMicrosoftTeamsFormSubmit = async ({
slug,
description,
tenantId
}: TMicrosoftTeamsFormData) => {
if (!microsoftTeamsClientId) {
createNotification({
text: "Microsoft Teams client ID is not set. Please contact your instance administrator.",
type: "error"
});
return;
}
if (id && microsoftTeamsIntegration) {
if (!currentOrg) {
return;
}
if (tenantId !== microsoftTeamsIntegration.tenantId) {
createNotification({
text: "Tenant ID cannot be changed",
type: "error"
});
return;
}
await updateMicrosoftTeamsIntegration({
id,
orgId: currentOrg.id,
slug,
description
});
createNotification({
text: "Successfully updated Microsoft Teams integration",
type: "success"
});
onClose();
} else {
const csrfToken = crypto.randomBytes(32).toString("hex");
localStorage.setItem("latestCSRFToken", csrfToken);
const state = {
redirectUri: `${window.location.origin}/organization/settings/oauth/callback`,
tenantId,
slug,
description,
csrfToken,
clientId: microsoftTeamsClientId.clientId
};
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?
client_id=${microsoftTeamsClientId.clientId}
&redirect_uri=${state.redirectUri}
&response_type=code
&response_mode=query
&scope=https://graph.microsoft.com/.default
&state=${encodeURIComponent(JSON.stringify(state))}
&prompt=consent
&admin_consent=true`;
window.location.href = url;
}
};
return (
<form onSubmit={handleSubmit(handleMicrosoftTeamsFormSubmit)} autoComplete="off">
<div className="mb-4 text-xs text-mineshaft-200">
For seamless installations, ensure that the Infisical bot is already installed in your
Microsoft Teams tenant. For more information, please refer to the{" "}
<a
className="text-primary-500"
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
target="_blank"
rel="noopener noreferrer"
>
Microsoft Teams Workflow Integration Documentation
</a>
, which will guide you through the download and installation process.
</div>
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" isRequired errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
{!microsoftTeamsIntegration && (
<Controller
control={control}
name="tenantId"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Tenant ID"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
)}
<Controller
control={control}
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
{microsoftTeamsIntegration && (
<FormControl label="Connected Microsoft Teams Tenant">
<Input
value={microsoftTeamsIntegration.tenantId}
isReadOnly
className="bg-white/[0.07]"
/>
</FormControl>
)}
<div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={!isDirty || isSubmitting}>
{id && microsoftTeamsIntegration ? "Save" : "Create Microsoft Teams Integration"}
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
);
};
@@ -1,5 +1,5 @@
import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
import { faEllipsis, faGear, faInfoCircle, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios";
import { twMerge } from "tailwind-merge";
@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
@@ -20,6 +21,7 @@ import {
TBody,
Td,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
@@ -27,14 +29,31 @@ import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
fetchSlackReinstallUrl,
useCheckMicrosoftTeamsIntegrationInstallationStatus,
useDeleteMicrosoftTeamsIntegration,
useDeleteSlackIntegration,
useGetWorkflowIntegrations
} from "@app/hooks/api";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import {
WorkflowIntegrationPlatform,
WorkflowIntegrationStatus
} from "@app/hooks/api/workflowIntegrations/types";
import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
import { IntegrationFormDetails } from "./IntegrationFormDetails";
const renderStatus = (status: WorkflowIntegrationStatus) => {
if (status === WorkflowIntegrationStatus.Installed) {
return <Badge variant="success">Installed</Badge>;
}
if (status === WorkflowIntegrationStatus.Pending) {
return <Badge variant="primary">Pending</Badge>;
}
return <Badge variant="danger">Failed</Badge>;
};
export const OrgWorkflowIntegrationTab = withPermission(
() => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
@@ -48,6 +67,9 @@ export const OrgWorkflowIntegrationTab = withPermission(
useGetWorkflowIntegrations(currentOrg?.id);
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
const { mutateAsync: deleteMicrosoftTeamsIntegration } = useDeleteMicrosoftTeamsIntegration();
const { mutateAsync: checkMicrosoftTeamsInstallationStatus } =
useCheckMicrosoftTeamsIntegrationInstallationStatus();
const handleRemoveIntegration = async () => {
if (!currentOrg) {
@@ -62,6 +84,13 @@ export const OrgWorkflowIntegrationTab = withPermission(
});
}
if (platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
await deleteMicrosoftTeamsIntegration({
id,
orgId: currentOrg?.id
});
}
handlePopUpClose("removeIntegration");
createNotification({
text: "Successfully deleted integration",
@@ -69,27 +98,37 @@ export const OrgWorkflowIntegrationTab = withPermission(
});
};
const triggerReinstall = async (platform: WorkflowIntegrationPlatform, id: string) => {
if (platform === WorkflowIntegrationPlatform.SLACK) {
try {
const slackReinstallUrl = await fetchSlackReinstallUrl({
id
});
const triggerSlackReinstall = async (id: string) => {
try {
const slackReinstallUrl = await fetchSlackReinstallUrl({
id
});
if (slackReinstallUrl) {
window.location.href = slackReinstallUrl;
}
} catch (err) {
if (axios.isAxiosError(err)) {
createNotification({
text: (err.response?.data as { message: string })?.message,
type: "error"
});
}
if (slackReinstallUrl) {
window.location.href = slackReinstallUrl;
}
} catch (err) {
if (axios.isAxiosError(err)) {
createNotification({
text: (err.response?.data as { message: string })?.message,
type: "error"
});
}
}
};
const triggerMicrosoftTeamsInstallationStatusCheck = async (id: string) => {
await checkMicrosoftTeamsInstallationStatus({
workflowIntegrationId: id,
orgId: currentOrg?.id
});
createNotification({
text: "The Microsoft Teams bot is successfully installed in your Microsoft Teams tenant",
type: "success"
});
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
@@ -117,6 +156,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
<Tr>
<Td>Provider</Td>
<Td>Alias</Td>
<Td>Status</Td>
</Tr>
</THead>
<TBody>
@@ -134,11 +174,23 @@ export const OrgWorkflowIntegrationTab = withPermission(
)}
{workflowIntegrations?.map((workflowIntegration) => (
<Tr key={workflowIntegration.id}>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
<FontAwesomeIcon icon={faSlack} />
<div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div>
<Td className="flex max-w-xs items-center gap-2 overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK ? (
<BsSlack />
) : (
<BsMicrosoftTeams />
)}
<div className="capitalize">
{workflowIntegration.integration.replaceAll("-", " ")}
</div>
{workflowIntegration.description && (
<Tooltip content={workflowIntegration.description}>
<FontAwesomeIcon icon={faInfoCircle} className="text-gray-400" size="sm" />
</Tooltip>
)}
</Td>
<Td>{workflowIntegration.slug}</Td>
<Td>{renderStatus(workflowIntegration.status)}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
@@ -159,29 +211,56 @@ export const OrgWorkflowIntegrationTab = withPermission(
>
More details
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionActions.Create}
an={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
triggerReinstall(
workflowIntegration.integration,
workflowIntegration.id
);
}}
>
Reinstall
</DropdownMenuItem>
)}
</OrgPermissionCan>
{workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK && (
<OrgPermissionCan
I={OrgPermissionActions.Create}
an={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async (e) => {
e.stopPropagation();
await triggerSlackReinstall(workflowIntegration.id);
}}
>
Reinstall
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
{workflowIntegration.integration ===
WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
an={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async (e) => {
e.stopPropagation();
await triggerMicrosoftTeamsInstallationStatusCheck(
workflowIntegration.id
);
}}
>
Check Installation Status
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
<OrgPermissionCan
I={OrgPermissionActions.Delete}
an={OrgPermissionSubjects.Settings}
@@ -11,7 +11,7 @@ const SettingsPageQueryParams = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
)({
component: SettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams),
@@ -0,0 +1,14 @@
import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: THCVaultSync;
};
export const HCVaultSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -8,6 +8,7 @@ import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
@@ -44,6 +45,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <VercelSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Windmill:
return <WindmillSyncDestinationCol secretSync={secretSync} />;
case SecretSync.HCVault:
return <HCVaultSyncDestinationCol secretSync={secretSync} />;
case SecretSync.TeamCity:
return <TeamCitySyncDestinationCol secretSync={secretSync} />;
default:
@@ -94,6 +94,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.workspace;
secondaryText = destinationConfig.path;
break;
case SecretSync.HCVault:
primaryText = destinationConfig.mount;
secondaryText = destinationConfig.path;
break;
case SecretSync.TeamCity:
primaryText = destinationConfig.project;
secondaryText = destinationConfig.buildConfig;
@@ -0,0 +1,19 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync";
type Props = {
secretSync: THCVaultSync;
};
export const HCVaultSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { path, mount }
} = secretSync;
return (
<>
<GenericFieldLabel label="Secrets Engine Mount">{mount}</GenericFieldLabel>
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
</>
);
};
@@ -18,6 +18,7 @@ import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection";
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection";
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
@@ -35,7 +36,7 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
const app = APP_CONNECTION_MAP[connection.app].name;
let DestinationComponents: ReactNode;
switch (secretSync.destination) {
switch (destination) {
case SecretSync.AWSParameterStore:
DestinationComponents = <AwsParameterStoreSyncDestinationSection secretSync={secretSync} />;
break;
@@ -74,6 +75,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Windmill:
DestinationComponents = <WindmillSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.HCVault:
DestinationComponents = <HCVaultSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.TeamCity:
DestinationComponents = <TeamCitySyncDestinationSection secretSync={secretSync} />;
break;
@@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
case SecretSync.HCVault:
case SecretSync.TeamCity:
AdditionalSyncOptionsComponent = null;
break;
@@ -1,351 +1,163 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
import { faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
ContentLoader,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DeleteActionModal,
EmptyState,
FormControl,
Input,
Select,
SelectItem,
Switch
Table,
TableContainer,
TBody,
Td,
THead,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useGetSlackIntegrationChannels,
useGetSlackIntegrations,
useGetWorkspaceSlackConfig,
useUpdateProjectSlackConfig
useDeleteProjectWorkflowIntegration,
useGetWorkspaceWorkflowIntegrationConfig
} from "@app/hooks/api";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
const formSchema = z.object({
slackIntegrationId: z.string(),
isSecretRequestNotificationEnabled: z.boolean(),
secretRequestChannels: z.string().array(),
isAccessRequestNotificationEnabled: z.boolean(),
accessRequestChannels: z.string().array()
});
import { AddWorkflowIntegrationModal } from "./components/AddWorkflowIntegrationModal";
import { EditWorkflowIntegrationModal } from "./components/EditWorkflowIntegrationModal";
import { MicrosoftTeamsConfigRow } from "./components/MicrosoftTeamsConfigRow";
import { SlackConfigRow } from "./components/SlackConfigRow";
type TSlackConfigForm = z.infer<typeof formSchema>;
export const renderProvider = (integration: WorkflowIntegrationPlatform) => {
if (integration === WorkflowIntegrationPlatform.SLACK) {
return <BsSlack />;
}
if (integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
return <BsMicrosoftTeams />;
}
return null;
};
export const WorkflowIntegrationTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"addWorkflowIntegration",
"removeIntegration",
"editIntegration"
] as const);
const { currentWorkspace } = useWorkspace();
const { data: slackConfig, isPending: isSlackConfigLoading } = useGetWorkspaceSlackConfig({
workspaceId: currentWorkspace?.id ?? ""
});
const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId);
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig();
const { data: slackConfig, isPending: isSlackConfigLoading } =
useGetWorkspaceWorkflowIntegrationConfig({
workspaceId: currentWorkspace?.id ?? "",
integration: WorkflowIntegrationPlatform.SLACK
});
const {
control,
watch,
handleSubmit,
setValue,
formState: { isDirty, isSubmitting }
} = useForm<TSlackConfigForm>({
resolver: zodResolver(formSchema),
defaultValues: {
isAccessRequestNotificationEnabled: false,
accessRequestChannels: [],
isSecretRequestNotificationEnabled: false,
secretRequestChannels: []
}
});
const { data: microsoftTeamsConfig, isPending: isMicrosoftTeamsConfigLoading } =
useGetWorkspaceWorkflowIntegrationConfig({
workspaceId: currentWorkspace?.id ?? "",
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
});
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
const selectedSlackIntegrationId = watch("slackIntegrationId");
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
const { mutateAsync: deleteIntegration } = useDeleteProjectWorkflowIntegration();
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
const slackChannelIdToName = Object.fromEntries(
(slackChannels || []).map((channel) => [channel.id, channel.name])
);
const sortedSlackChannels = slackChannels?.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
);
const handleIntegrationSave = async (data: TSlackConfigForm) => {
if (!currentWorkspace) {
const handleRemoveIntegration = async (
integrationType: WorkflowIntegrationPlatform,
integrationId: string
) => {
if (!currentWorkspace.id) {
return;
}
await updateProjectSlackConfig({
workspaceId: currentWorkspace.id,
...data,
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
await deleteIntegration({
projectId: currentWorkspace?.id ?? "",
integration: integrationType,
integrationId
});
createNotification({
type: "success",
text: "Successfully updated slack integration"
text: `Successfully removed ${integrationType.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase())} integration`
});
};
useEffect(() => {
if (slackConfig) {
setValue("slackIntegrationId", slackConfig.slackIntegrationId);
setValue(
"isSecretRequestNotificationEnabled",
slackConfig.isSecretRequestNotificationEnabled
);
setValue(
"isAccessRequestNotificationEnabled",
slackConfig.isAccessRequestNotificationEnabled
);
if (slackChannels) {
setValue(
"secretRequestChannels",
slackConfig.secretRequestChannels
.split(", ")
.filter((channel) => channel in slackChannelIdToName)
);
setValue(
"accessRequestChannels",
slackConfig.accessRequestChannels
.split(", ")
.filter((channel) => channel in slackChannelIdToName)
);
}
}
}, [slackConfig, slackChannels]);
if (isSlackConfigLoading) {
return <ContentLoader />;
}
return !slackIntegrations?.length ? (
<EmptyState title="You do not have any integrations configured.">
<Link
to="/organization/settings"
search={{
selectedTab: "workflow-integrations"
}}
>
<div className="mt-2 underline decoration-primary-800 underline-offset-4 duration-200 hover:cursor-pointer hover:text-mineshaft-100 hover:decoration-primary-600">
Create one now
</div>
</Link>
</EmptyState>
) : (
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
<p className="text-xl font-semibold text-mineshaft-100">Workflow Integrations</p>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
onClick={() => {
handlePopUpOpen("addWorkflowIntegration");
}}
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add
</Button>
)}
</OrgPermissionCan>
</div>
<p className="mb-4 text-gray-400">
This integration allows you to send notifications to your Slack workspace in response to
events in your project.
Connect Infisical to other platforms for notification and workflow integrations.
</p>
<form onSubmit={handleSubmit(handleIntegrationSave)}>
<div className="max-w-md">
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Select
{...field}
isDisabled={!isAllowed}
placeholder="None"
onValueChange={onChange}
defaultValue={slackConfig?.slackIntegrationId}
className="w-3/4 bg-mineshaft-600"
>
{slackIntegrations?.map((slackIntegration) => (
<SelectItem
value={slackIntegration.id}
key={`slack-integration-${slackIntegration.id}`}
>
{slackIntegration.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="slackIntegrationId"
<TableContainer>
{!!slackConfig || !!microsoftTeamsConfig ? (
<Table>
<THead>
<Tr>
<Td>Provider</Td>
<Td>Access Request Notifications Destination</Td>
<Td>Secret Request Notifications Destination</Td>
<Td />
</Tr>
</THead>
<TBody>
<SlackConfigRow
handlePopUpOpen={handlePopUpOpen}
isSlackConfigLoading={isSlackConfigLoading}
slackConfig={slackConfig}
/>
)}
</ProjectPermissionCan>
</div>
{selectedSlackIntegrationId && (
<>
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
<Controller
control={control}
name="isSecretRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mb-2 mt-3"
>
<Switch
id="secret-approval-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Secret Approval Requests</p>
</Switch>
</FormControl>
);
}}
<MicrosoftTeamsConfigRow
handlePopUpOpen={handlePopUpOpen}
isMicrosoftTeamsConfigLoading={isMicrosoftTeamsConfigLoading}
microsoftTeamsConfig={microsoftTeamsConfig}
/>
</TBody>
</Table>
) : (
<div className="flex h-full w-full items-center justify-center">
<EmptyState
title="No project workflow integrations configured. Add a new workflow integration to get started"
icon={faGear}
/>
{secretRequestNotifState && (
<Controller
control={control}
name="secretRequestChannels"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Slack channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value
?.filter(Boolean)
.map((entry) => slackChannelIdToName[entry])
.join(", ")}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{sortedSlackChannels?.map((slackChannel) => {
const isChecked = value?.includes(slackChannel.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== slackChannel.id)
: [...(value || []), slackChannel.id]
);
}}
key={`secret-requests-slack-channel-${slackChannel.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{slackChannel.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="isAccessRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
<Switch
id="access-request-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Access Requests</p>
</Switch>
</FormControl>
);
}}
/>
{accessRequestNotifState && (
<Controller
control={control}
name="accessRequestChannels"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Slack channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value
?.filter(Boolean)
.map((entry) => slackChannelIdToName[entry])
.join(", ")}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{sortedSlackChannels?.map((slackChannel) => {
const isChecked = value?.includes(slackChannel.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== slackChannel.id)
: [...(value || []), slackChannel.id]
);
}}
key={`access-requests-slack-channel-${slackChannel.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{slackChannel.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
<Button
colorSchema="secondary"
className="mt-4"
type="submit"
isDisabled={!isDirty}
isLoading={isSubmitting}
>
Save
</Button>
</>
</div>
)}
</form>
</TableContainer>
<AddWorkflowIntegrationModal
isOpen={popUp.addWorkflowIntegration.isOpen}
onToggle={(state) => handlePopUpToggle("addWorkflowIntegration", state)}
/>
<DeleteActionModal
isOpen={popUp.removeIntegration.isOpen}
title="Are you sure want to remove this integration?"
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await handleRemoveIntegration(
popUp.removeIntegration.data?.integration,
popUp.removeIntegration.data?.integrationId
);
handlePopUpClose("removeIntegration");
}}
/>
<EditWorkflowIntegrationModal
isOpen={popUp.editIntegration.isOpen}
onClose={() => handlePopUpClose("editIntegration")}
integration={popUp.editIntegration.data?.integration}
/>
</div>
);
};
@@ -0,0 +1,153 @@
import { useState } from "react";
import { BsMicrosoftTeams } from "react-icons/bs";
import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetWorkspaceWorkflowIntegrationConfig } from "@app/hooks/api";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm";
type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
};
enum WizardSteps {
SelectPlatform = "select-platform",
PlatformInputs = "platform-inputs"
}
const PLATFORM_LIST = [
{
icon: <FontAwesomeIcon icon={faSlack} size="lg" />,
platform: WorkflowIntegrationPlatform.SLACK,
title: "Slack"
},
{
icon: <BsMicrosoftTeams className="text-lg" />,
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
title: "Microsoft Teams"
}
];
export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform);
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
const { currentWorkspace } = useWorkspace();
const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({
workspaceId: currentWorkspace?.id ?? "",
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
});
const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({
workspaceId: currentWorkspace?.id ?? "",
integration: WorkflowIntegrationPlatform.SLACK
});
const microsoftTeamsConfigured = !!microsoftTeamsConfig;
const slackConfigured = !!slackConfig;
const handleFormReset = (state: boolean = false) => {
onToggle(state);
setWizardStep(WizardSteps.SelectPlatform);
setSelectedPlatform(null);
};
return (
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
<ModalContent
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
className="my-4"
>
<AnimatePresence mode="wait">
{wizardStep === WizardSteps.SelectPlatform && (
<motion.div
key="select-type-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<div className="mb-4 text-mineshaft-300">Select a workflow integration</div>
<div className="flex items-center space-x-4">
{PLATFORM_LIST.map(({ icon, platform, title }) => {
const isConfigured =
(platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS &&
microsoftTeamsConfigured) ||
(platform === WorkflowIntegrationPlatform.SLACK && slackConfigured);
return (
<div className="relative" key={platform}>
<div
className={twMerge(
"flex h-32 w-36 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white",
isConfigured && "border-primary/50 opacity-60"
)}
role="button"
tabIndex={0}
onClick={() => {
setSelectedPlatform(platform);
setWizardStep(WizardSteps.PlatformInputs);
}}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
setSelectedPlatform(platform);
setWizardStep(WizardSteps.PlatformInputs);
}
}}
>
<div>{icon}</div>
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
</div>
{isConfigured && (
<div className="absolute bottom-0 left-0 right-0 z-30 h-full">
<div className="relative h-full">
<div className="absolute bottom-0 right-0 w-full flex-row items-center overflow-hidden whitespace-nowrap rounded-b-md bg-primary px-2 py-0.5 text-center text-xs text-black">
Already Configured
</div>
</div>
</div>
)}
</div>
);
})}
</div>
</motion.div>
)}
{wizardStep === WizardSteps.PlatformInputs &&
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
<motion.div
key="platform-inputs-step-slack"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<SlackIntegrationForm onClose={() => handleFormReset(false)} />
</motion.div>
)}
{wizardStep === WizardSteps.PlatformInputs &&
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<motion.div
key="platform-inputs-step-ms-teams"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<MicrosoftTeamsIntegrationForm onClose={() => handleFormReset(false)} />
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,33 @@
import { Modal, ModalContent } from "@app/components/v2";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm";
type Props = {
isOpen?: boolean;
onClose: () => void;
integration: WorkflowIntegrationPlatform;
};
export const EditWorkflowIntegrationModal = ({ isOpen, onClose, integration }: Props) => {
const handleFormReset = () => {
onClose();
};
return (
<Modal isOpen={isOpen} onOpenChange={handleFormReset}>
<ModalContent
title={`Edit ${integration?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
>
{integration === WorkflowIntegrationPlatform.SLACK && (
<SlackIntegrationForm onClose={handleFormReset} />
)}
{integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<MicrosoftTeamsIntegrationForm onClose={handleFormReset} />
)}
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,148 @@
/* eslint-disable no-nested-ternary */
import { BsMicrosoftTeams } from "react-icons/bs";
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Spinner,
Td,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useGetMicrosoftTeamsIntegrationTeams } from "@app/hooks/api";
import {
ProjectWorkflowIntegrationConfig,
WorkflowIntegrationPlatform
} from "@app/hooks/api/workflowIntegrations/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
microsoftTeamsConfig?: ProjectWorkflowIntegrationConfig | null;
isMicrosoftTeamsConfigLoading: boolean;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>,
data?: {
integrationId?: string;
integration: WorkflowIntegrationPlatform;
}
) => void;
};
export const MicrosoftTeamsConfigRow = ({
handlePopUpOpen,
isMicrosoftTeamsConfigLoading,
microsoftTeamsConfig
}: Props) => {
const { data: microsoftTeamsChannels, isPending: isMicrosoftTeamsChannelsLoading } =
useGetMicrosoftTeamsIntegrationTeams(microsoftTeamsConfig?.integrationId);
const microsoftTeamsChannelIdToName = Object.fromEntries(
microsoftTeamsChannels?.flatMap((team) =>
team.channels.map((channel) => [channel.channelId, channel.channelName])
) ?? []
);
if (microsoftTeamsConfig?.integration !== WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
return null;
}
const isLoadingConfig = isMicrosoftTeamsChannelsLoading || isMicrosoftTeamsConfigLoading;
return (
<Tr>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis">
<div className="flex items-center gap-2">
<BsMicrosoftTeams />
Microsoft Teams
</div>
</Td>
<Td>
{microsoftTeamsConfig.isAccessRequestNotificationEnabled &&
!isLoadingConfig &&
microsoftTeamsConfig.accessRequestChannels?.channelIds?.length > 0 ? (
<Badge>
{microsoftTeamsConfig.accessRequestChannels.channelIds
.map((channel) => microsoftTeamsChannelIdToName[channel])
.join(", ")}
</Badge>
) : isLoadingConfig ? (
<Spinner size="xs" />
) : (
<Badge variant="danger">Disabled</Badge>
)}
</Td>
<Td>
{microsoftTeamsConfig.isSecretRequestNotificationEnabled &&
!isLoadingConfig &&
microsoftTeamsConfig.secretRequestChannels?.channelIds?.length > 0 ? (
<Badge>
{microsoftTeamsConfig.secretRequestChannels.channelIds
.map((channel) => microsoftTeamsChannelIdToName[channel])
.join(", ")}
</Badge>
) : isLoadingConfig ? (
<Spinner size="xs" />
) : (
<Badge variant="danger">Disabled</Badge>
)}
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("editIntegration", {
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
});
}}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeIntegration", {
integrationId: microsoftTeamsConfig.integrationId,
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
});
}}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
};
@@ -0,0 +1,556 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
Input,
Select,
SelectItem,
Switch
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext";
import {
useGetMicrosoftTeamsIntegrations,
useGetMicrosoftTeamsIntegrationTeams,
useGetWorkspaceWorkflowIntegrationConfig,
useUpdateProjectWorkflowIntegrationConfig
} from "@app/hooks/api";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
const formSchema = z
.object({
microsoftTeamsIntegrationId: z.string(),
isSecretRequestNotificationEnabled: z.boolean(),
isAccessRequestNotificationEnabled: z.boolean(),
secretRequestChannels: z
.object({
teamId: z.string(),
channelIds: z.string().array()
})
.optional(),
accessRequestChannels: z
.object({
teamId: z.string(),
channelIds: z.string().array()
})
.optional()
})
.superRefine((data, ctx) => {
if (data.isSecretRequestNotificationEnabled) {
if (!data?.secretRequestChannels?.teamId) {
ctx.addIssue({
path: ["secretRequestChannels", "teamId"],
code: z.ZodIssueCode.custom,
message: "Team is required"
});
}
if (!data?.secretRequestChannels?.channelIds?.length) {
ctx.addIssue({
path: ["secretRequestChannels", "channelIds"],
code: z.ZodIssueCode.custom,
message: "At least one channel is required"
});
}
}
if (data.isAccessRequestNotificationEnabled) {
if (!data?.accessRequestChannels?.teamId) {
ctx.addIssue({
path: ["accessRequestChannels", "teamId"],
code: z.ZodIssueCode.custom,
message: "Team is required"
});
}
if (!data?.accessRequestChannels?.channelIds?.length) {
ctx.addIssue({
path: ["accessRequestChannels", "channelIds"],
code: z.ZodIssueCode.custom,
message: "At least one channel is required"
});
}
}
});
type TMicrosoftTeamsConfigForm = z.infer<typeof formSchema>;
type Props = {
onClose: () => void;
};
export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({
workspaceId: currentWorkspace?.id ?? "",
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
});
const { data: microsoftTeamsIntegrations } = useGetMicrosoftTeamsIntegrations(
currentWorkspace?.orgId
);
const { mutateAsync: updateProjectMicrosoftTeamsConfig } =
useUpdateProjectWorkflowIntegrationConfig();
const {
control,
watch,
handleSubmit,
setValue,
formState: { isDirty, isSubmitting }
} = useForm<TMicrosoftTeamsConfigForm>({
resolver: zodResolver(formSchema),
defaultValues: {
isAccessRequestNotificationEnabled: false,
isSecretRequestNotificationEnabled: false,
accessRequestChannels: {
teamId: "",
channelIds: []
},
secretRequestChannels: {
teamId: "",
channelIds: []
}
}
});
const handleIntegrationSave = async (data: TMicrosoftTeamsConfigForm) => {
try {
if (!currentWorkspace) {
return;
}
await updateProjectMicrosoftTeamsConfig({
workspaceId: currentWorkspace.id,
isAccessRequestNotificationEnabled: data.isAccessRequestNotificationEnabled,
isSecretRequestNotificationEnabled: data.isSecretRequestNotificationEnabled,
...(data.isAccessRequestNotificationEnabled && {
accessRequestChannels: data.accessRequestChannels
}),
...(data.isSecretRequestNotificationEnabled && {
secretRequestChannels: data.secretRequestChannels
}),
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
integrationId: data.microsoftTeamsIntegrationId
});
createNotification({
type: "success",
text: "Successfully created microsoft teams integration"
});
onClose();
} catch {
createNotification({
type: "error",
text: "Failed to create microsoft teams integration"
});
}
};
const selectedAccessRequestTeamId = watch("accessRequestChannels.teamId");
const selectedSecretRequestTeamId = watch("secretRequestChannels.teamId");
const selectedMicrosoftTeamsIntegrationId = watch("microsoftTeamsIntegrationId");
const accessRequestNotificationsEnabled = watch("isAccessRequestNotificationEnabled");
const secretRequestNotificationsEnabled = watch("isSecretRequestNotificationEnabled");
const {
data: microsoftTeamsIntegrationTeams,
isPending: isLoadingMicrosoftTeamsIntegrationTeams
} = useGetMicrosoftTeamsIntegrationTeams(selectedMicrosoftTeamsIntegrationId);
const sortedMicrosoftTeamsIntegrationTeams = microsoftTeamsIntegrationTeams?.sort((a, b) =>
a.teamName.toLowerCase().localeCompare(b.teamName.toLowerCase())
);
const selectableAccessRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams
?.filter((team) => team.teamId === selectedAccessRequestTeamId)
.map((team) => team.channels)
.flat();
const selectableSecretRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams
?.filter((team) => team.teamId === selectedSecretRequestTeamId)
.map((team) => team.channels)
.flat();
const channelIdToName = [
...(selectableAccessRequestChannelIds || []),
...(selectableSecretRequestChannelIds || [])
].reduce(
(acc, channel) => {
acc[channel.channelId] = channel.channelName;
return acc;
},
{} as Record<string, string>
);
useEffect(() => {
if (microsoftTeamsConfig) {
setValue("microsoftTeamsIntegrationId", microsoftTeamsConfig.integrationId);
setValue(
"isSecretRequestNotificationEnabled",
microsoftTeamsConfig.isSecretRequestNotificationEnabled
);
setValue(
"isAccessRequestNotificationEnabled",
microsoftTeamsConfig.isAccessRequestNotificationEnabled
);
if (microsoftTeamsConfig.integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
if (microsoftTeamsConfig.secretRequestChannels) {
if (Object.entries(microsoftTeamsConfig.accessRequestChannels).length) {
setValue("accessRequestChannels", microsoftTeamsConfig.accessRequestChannels);
}
if (Object.entries(microsoftTeamsConfig.secretRequestChannels).length) {
setValue("secretRequestChannels", microsoftTeamsConfig.secretRequestChannels);
}
}
}
}
}, [microsoftTeamsConfig]);
return (
<form onSubmit={handleSubmit(handleIntegrationSave)}>
<div className="flex w-full flex-col justify-start">
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Controller
control={control}
name="microsoftTeamsIntegrationId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams Integration"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
isDisabled={!isAllowed}
placeholder="None"
onValueChange={(v) => {
onChange(v);
setValue("isAccessRequestNotificationEnabled", false);
setValue("isSecretRequestNotificationEnabled", false);
setValue("accessRequestChannels", {
teamId: "",
channelIds: []
});
setValue("secretRequestChannels", {
teamId: "",
channelIds: []
});
}}
className="w-full"
defaultValue={microsoftTeamsConfig?.integrationId}
>
{microsoftTeamsIntegrations?.map((microsoftTeamsIntegration) => (
<SelectItem
value={microsoftTeamsIntegration.id}
key={`microsoft-teams-integration-${microsoftTeamsIntegration.id}`}
>
{microsoftTeamsIntegration.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
</ProjectPermissionCan>
</div>
{selectedMicrosoftTeamsIntegrationId && (
<>
<h2 className="mb-2 flex-1 text-sm text-mineshaft-400">Configure Events</h2>
<Controller
control={control}
name="isSecretRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mb-2 mt-3"
>
<Switch
id="secret-approval-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Secret Approval Requests</p>
</Switch>
</FormControl>
);
}}
/>
{secretRequestNotificationsEnabled && (
<>
<Controller
control={control}
name="secretRequestChannels.teamId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Secret Approval Requests Notifications Team"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
placeholder={
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
}
className="w-full"
onValueChange={(value) => onChange(value)}
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
>
{!isLoadingMicrosoftTeamsIntegrationTeams &&
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
<SelectItem
key={`secret-requests-microsoft-teams-team-${team.teamId}`}
value={team.teamId}
>
{team.teamName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretRequestChannels.channelIds"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger
className={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
? "opacity-50"
: ""
}
asChild
disabled={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
}
>
{selectedSecretRequestTeamId ? (
<Input
isReadOnly
value={
isLoadingMicrosoftTeamsIntegrationTeams
? "Loading..."
: value
?.filter(Boolean)
.map((entry) => channelIdToName[entry])
.join(", ")
}
className="text-left"
/>
) : (
<Input
isReadOnly
value="Select a team..."
className="py-2 text-left text-sm"
/>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{selectableSecretRequestChannelIds?.map((channel) => {
const isChecked = value?.includes(channel.channelId);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== channel.channelId)
: [...(value || []), channel.channelId]
);
}}
key={`secret-requests-microsoft-teams-channel-${channel.channelId}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{channel.channelName}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="isAccessRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
<Switch
id="access-request-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Access Requests</p>
</Switch>
</FormControl>
);
}}
/>
{accessRequestNotificationsEnabled && (
<>
<Controller
control={control}
name="accessRequestChannels.teamId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Access Requests Notifications Team"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
placeholder={
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
}
className="w-full"
onValueChange={(value) => onChange(value)}
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
>
{!isLoadingMicrosoftTeamsIntegrationTeams &&
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
<SelectItem
key={`access-requests-microsoft-teams-team-${team.teamId}`}
value={team.teamId}
>
{team.teamName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="accessRequestChannels.channelIds"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger
className={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
? "opacity-50"
: ""
}
asChild
disabled={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
}
>
{selectedAccessRequestTeamId ? (
<Input
isReadOnly
value={
isLoadingMicrosoftTeamsIntegrationTeams
? "Loading..."
: value
?.filter(Boolean)
.map((entry) => channelIdToName[entry])
.join(", ")
}
className="text-left"
/>
) : (
<Input
isReadOnly
value="Select a team..."
className="py-2 text-left text-sm"
/>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{selectableAccessRequestChannelIds?.map((channel) => {
const isChecked = value?.includes(channel.channelId);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== channel.channelId)
: [...(value || []), channel.channelId]
);
}}
key={`access-requests-slack-channel-${channel.channelId}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{channel.channelName}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</>
)}
<Button
colorSchema="secondary"
className="mt-4"
type="submit"
disabled={!isDirty}
isDisabled={!isDirty}
isLoading={isSubmitting}
>
Save
</Button>
</>
)}
</form>
);
};
@@ -0,0 +1,145 @@
/* eslint-disable no-nested-ternary */
import { BsSlack } from "react-icons/bs";
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Spinner,
Td,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useGetSlackIntegrationChannels } from "@app/hooks/api";
import {
ProjectWorkflowIntegrationConfig,
WorkflowIntegrationPlatform
} from "@app/hooks/api/workflowIntegrations/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
slackConfig?: ProjectWorkflowIntegrationConfig | null;
isSlackConfigLoading: boolean;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>,
data?: {
integrationId?: string;
integration: WorkflowIntegrationPlatform;
}
) => void;
};
export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackConfig }: Props) => {
const { data: slackChannels, isPending: isSlackChannelsLoading } = useGetSlackIntegrationChannels(
slackConfig?.integrationId
);
const slackChannelIdToName = Object.fromEntries(
(slackChannels || []).map((channel) => [channel.id, channel.name])
);
if (slackConfig?.integration !== WorkflowIntegrationPlatform.SLACK) {
return null;
}
const isLoadingConfig = isSlackChannelsLoading || isSlackConfigLoading;
return (
<Tr>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis">
<div className="flex items-center gap-2">
<BsSlack />
Slack
</div>
</Td>
<Td>
{slackConfig.isAccessRequestNotificationEnabled &&
!isLoadingConfig &&
slackConfig.accessRequestChannels.length > 0 ? (
<Badge>
{slackConfig.accessRequestChannels
.split(", ")
.map((channel) => slackChannelIdToName[channel])
.join(", ")}
</Badge>
) : isLoadingConfig ? (
<Spinner size="xs" />
) : (
<Badge variant="danger">Disabled</Badge>
)}
</Td>
<Td>
{slackConfig.isSecretRequestNotificationEnabled &&
!isLoadingConfig &&
slackConfig.secretRequestChannels.length > 0 ? (
<Badge>
{slackConfig.secretRequestChannels
.split(", ")
.map((channel) => slackChannelIdToName[channel])
.join(", ")}
</Badge>
) : isLoadingConfig ? (
<Spinner size="xs" />
) : (
<Badge variant="danger">Disabled</Badge>
)}
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("editIntegration", {
integration: WorkflowIntegrationPlatform.SLACK
});
}}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeIntegration", {
integrationId: slackConfig.integrationId,
integration: WorkflowIntegrationPlatform.SLACK
});
}}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
};
@@ -0,0 +1,355 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
Input,
Select,
SelectItem,
Switch
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext";
import {
useGetSlackIntegrationChannels,
useGetWorkflowIntegrations,
useGetWorkspaceWorkflowIntegrationConfig,
useUpdateProjectWorkflowIntegrationConfig
} from "@app/hooks/api";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
const formSchema = z.object({
slackIntegrationId: z.string(),
isSecretRequestNotificationEnabled: z.boolean(),
secretRequestChannels: z.string().array(),
isAccessRequestNotificationEnabled: z.boolean(),
accessRequestChannels: z.string().array()
});
type TSlackConfigForm = z.infer<typeof formSchema>;
type Props = {
onClose: () => void;
};
export const SlackIntegrationForm = ({ onClose }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({
workspaceId: currentWorkspace?.id ?? "",
integration: WorkflowIntegrationPlatform.SLACK
});
const { data: workflowIntegrations } = useGetWorkflowIntegrations(currentWorkspace?.orgId);
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectWorkflowIntegrationConfig();
const slackIntegrations = workflowIntegrations?.filter(
(integration) => integration.integration === WorkflowIntegrationPlatform.SLACK
);
const {
control,
watch,
handleSubmit,
setValue,
formState: { isDirty, isSubmitting }
} = useForm<TSlackConfigForm>({
resolver: zodResolver(formSchema),
defaultValues: {
isAccessRequestNotificationEnabled: false,
accessRequestChannels: [],
isSecretRequestNotificationEnabled: false,
secretRequestChannels: []
}
});
const handleIntegrationSave = async (data: TSlackConfigForm) => {
try {
if (!currentWorkspace) {
return;
}
await updateProjectSlackConfig({
...data,
workspaceId: currentWorkspace.id,
integration: WorkflowIntegrationPlatform.SLACK,
integrationId: data.slackIntegrationId,
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
});
createNotification({
type: "success",
text: "Successfully created slack integration"
});
onClose();
} catch {
createNotification({
type: "error",
text: "Failed to create slack integration"
});
}
};
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
const selectedSlackIntegrationId = watch("slackIntegrationId");
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
const slackChannelIdToName = Object.fromEntries(
(slackChannels || []).map((channel) => [channel.id, channel.name])
);
const sortedSlackChannels = slackChannels?.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
);
useEffect(() => {
if (slackConfig) {
setValue("slackIntegrationId", slackConfig.integrationId);
setValue(
"isSecretRequestNotificationEnabled",
slackConfig.isSecretRequestNotificationEnabled
);
setValue(
"isAccessRequestNotificationEnabled",
slackConfig.isAccessRequestNotificationEnabled
);
if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
if (slackChannels) {
setValue(
"secretRequestChannels",
slackConfig.secretRequestChannels
.split(", ")
.filter((channel) => channel in slackChannelIdToName)
);
setValue(
"accessRequestChannels",
slackConfig.accessRequestChannels
.split(", ")
.filter((channel) => channel in slackChannelIdToName)
);
}
}
}
}, [slackConfig, slackChannels]);
return (
<form onSubmit={handleSubmit(handleIntegrationSave)}>
<div className="flex w-full flex-col justify-start">
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Controller
control={control}
name="slackIntegrationId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Slack Integration"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
isDisabled={!isAllowed}
placeholder="None"
onValueChange={(val) => {
onChange(val);
}}
className="w-full"
defaultValue={slackConfig?.integrationId}
>
{slackIntegrations?.map((slackIntegration) => (
<SelectItem
value={slackIntegration.id}
key={`slack-integration-${slackIntegration.id}`}
>
{slackIntegration.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
</ProjectPermissionCan>
</div>
{selectedSlackIntegrationId && (
<>
<h2 className="mb-2 flex-1 text-sm text-mineshaft-400">Configure Events</h2>
<Controller
control={control}
name="isSecretRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mb-2 mt-3"
>
<Switch
id="secret-approval-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Secret Approval Requests</p>
</Switch>
</FormControl>
);
}}
/>
{secretRequestNotifState && (
<Controller
control={control}
name="secretRequestChannels"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Slack channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value
?.filter(Boolean)
.map((entry) => slackChannelIdToName[entry])
.join(", ")}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{sortedSlackChannels?.map((slackChannel) => {
const isChecked = value?.includes(slackChannel.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== slackChannel.id)
: [...(value || []), slackChannel.id]
);
}}
key={`secret-requests-slack-channel-${slackChannel.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{slackChannel.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="isAccessRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
<Switch
id="access-request-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Access Requests</p>
</Switch>
</FormControl>
);
}}
/>
{accessRequestNotifState && (
<Controller
control={control}
name="accessRequestChannels"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Slack channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value
?.filter(Boolean)
.map((entry) => slackChannelIdToName[entry])
.join(", ")}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{sortedSlackChannels?.map((slackChannel) => {
const isChecked = value?.includes(slackChannel.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== slackChannel.id)
: [...(value || []), slackChannel.id]
);
}}
key={`access-requests-slack-channel-${slackChannel.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{slackChannel.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
<Button
colorSchema="secondary"
className="mt-4"
type="submit"
isDisabled={!isDirty}
isLoading={isSubmitting}
>
Save
</Button>
</>
)}
</form>
);
};
+104 -29
View File
@@ -42,7 +42,6 @@ import { Route as adminLayoutImport } from './pages/admin/layout'
import { Route as authProviderSuccessPageRouteImport } from './pages/auth/ProviderSuccessPage/route'
import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route'
import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route'
import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route'
import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route'
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
@@ -62,6 +61,7 @@ import { Route as organizationKmsOverviewPageRouteImport } from './pages/organiz
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route'
import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
@@ -87,6 +87,7 @@ import { Route as secretManagerSecretRotationPageRouteImport } from './pages/sec
import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route'
import { Route as secretManagerSecretApprovalsPageRouteImport } from './pages/secret-manager/SecretApprovalsPage/route'
import { Route as secretManagerIPAllowlistPageRouteImport } from './pages/secret-manager/IPAllowlistPage/route'
import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from './pages/organization/SettingsPage/OauthCallbackPage/route'
import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/route'
import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route'
import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route'
@@ -218,6 +219,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/settings',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing',
@@ -484,6 +489,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport.update({
id: '/secret-sharing',
@@ -522,14 +535,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
const organizationSettingsPageRouteRoute =
organizationSettingsPageRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationSecretScanningPageRouteRoute =
organizationSecretScanningPageRouteImport.update({
id: '/secret-scanning',
@@ -671,6 +676,14 @@ const organizationCertManagerOverviewPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationSettingsPageRouteRoute =
organizationSettingsPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute,
} as any)
const organizationSecretSharingPageRouteRoute =
organizationSecretSharingPageRouteImport.update({
id: '/',
@@ -865,6 +878,14 @@ const secretManagerIPAllowlistPageRouteRoute =
getParentRoute: () => secretManagerLayoutRoute,
} as any)
const organizationSettingsPageOauthCallbackPageRouteRoute =
organizationSettingsPageOauthCallbackPageRouteImport.update({
id: '/oauth/callback',
path: '/oauth/callback',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute,
} as any)
const kmsSettingsPageRouteRoute = kmsSettingsPageRouteImport.update({
id: '/settings',
path: '/settings',
@@ -1960,13 +1981,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationSecretScanningPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/settings': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/settings'
path: '/settings'
fullPath: '/organization/settings'
preLoaderRoute: typeof organizationSettingsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
path: '/cert-manager/$projectId'
@@ -2002,6 +2016,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/settings': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/settings'
path: '/settings'
fullPath: '/organization/settings'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
path: '/secret-manager/$projectId'
@@ -2037,6 +2058,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationSecretSharingPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/settings/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/settings/'
path: '/'
fullPath: '/organization/settings/'
preLoaderRoute: typeof organizationSettingsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
path: '/cert-manager/overview'
@@ -2177,6 +2205,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof kmsSettingsPageRouteImport
parentRoute: typeof kmsLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback'
path: '/oauth/callback'
fullPath: '/organization/settings/oauth/callback'
preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist'
path: '/allowlist'
@@ -3105,16 +3140,33 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChi
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren {
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
organizationSettingsPageOauthCallbackPageRouteRoute: typeof organizationSettingsPageOauthCallbackPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren =
{
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
organizationSettingsPageOauthCallbackPageRouteRoute:
organizationSettingsPageOauthCallbackPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -3134,13 +3186,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
organizationSecretScanningPageRouteRoute:
organizationSecretScanningPageRouteRoute,
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren,
organizationCertManagerOverviewPageRouteRoute:
organizationCertManagerOverviewPageRouteRoute,
organizationGroupDetailsByIDPageRouteRoute:
@@ -3827,17 +3880,18 @@ export interface FileRoutesByFullPath {
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
'/organization/settings': typeof organizationSettingsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
'/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
'/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/organization/settings/': typeof organizationSettingsPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -3854,6 +3908,7 @@ export interface FileRoutesByFullPath {
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
'/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
@@ -4008,7 +4063,6 @@ export interface FileRoutesByTo {
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
'/organization/settings': typeof organizationSettingsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
@@ -4016,6 +4070,7 @@ export interface FileRoutesByTo {
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
'/organization/settings': typeof organizationSettingsPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -4032,6 +4087,7 @@ export interface FileRoutesByTo {
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
'/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
@@ -4195,17 +4251,18 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof organizationSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -4226,6 +4283,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings': typeof kmsSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute
@@ -4386,17 +4444,18 @@ export interface FileRouteTypes {
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/secret-scanning'
| '/organization/settings'
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/organization/app-connections'
| '/organization/gateways'
| '/organization/secret-sharing'
| '/organization/settings'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections/'
| '/organization/gateways/'
| '/organization/secret-sharing/'
| '/organization/settings/'
| '/organization/cert-manager/overview'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
@@ -4413,6 +4472,7 @@ export interface FileRouteTypes {
| '/kms/$projectId/kmip'
| '/kms/$projectId/overview'
| '/kms/$projectId/settings'
| '/organization/settings/oauth/callback'
| '/secret-manager/$projectId/allowlist'
| '/secret-manager/$projectId/approval'
| '/secret-manager/$projectId/overview'
@@ -4566,7 +4626,6 @@ export interface FileRouteTypes {
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/secret-scanning'
| '/organization/settings'
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/secret-manager/$projectId'
@@ -4574,6 +4633,7 @@ export interface FileRouteTypes {
| '/organization/app-connections'
| '/organization/gateways'
| '/organization/secret-sharing'
| '/organization/settings'
| '/organization/cert-manager/overview'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
@@ -4590,6 +4650,7 @@ export interface FileRouteTypes {
| '/kms/$projectId/kmip'
| '/kms/$projectId/overview'
| '/kms/$projectId/settings'
| '/organization/settings/oauth/callback'
| '/secret-manager/$projectId/allowlist'
| '/secret-manager/$projectId/approval'
| '/secret-manager/$projectId/overview'
@@ -4751,17 +4812,18 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/organization/billing'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings/'
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
@@ -4782,6 +4844,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview'
@@ -5141,10 +5204,10 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/organization/billing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/gateways",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
@@ -5186,10 +5249,6 @@ export const routeTree = rootRoute
"filePath": "organization/SecretScanningPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/settings": {
"filePath": "organization/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@@ -5227,6 +5286,14 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/settings": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/settings/",
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
]
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@@ -5253,6 +5320,10 @@ export const routeTree = rootRoute
"filePath": "organization/SecretSharingPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
},
"/_authenticate/_inject-org-details/_org-layout/organization/settings/": {
"filePath": "organization/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings"
},
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
"filePath": "organization/CertManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
@@ -5379,6 +5450,10 @@ export const routeTree = rootRoute
"filePath": "kms/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout"
},
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback": {
"filePath": "organization/SettingsPage/OauthCallbackPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings"
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist": {
"filePath": "secret-manager/IPAllowlistPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout"
+4 -1
View File
@@ -20,7 +20,10 @@ const organizationRoutes = route("/organization", [
index("organization/SecretSharingPage/route.tsx"),
route("/settings", "organization/SecretSharingSettingsPage/route.tsx")
]),
route("/settings", "organization/SettingsPage/route.tsx"),
route("/settings", [
index("organization/SettingsPage/route.tsx"),
route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx")
]),
route("/secret-scanning", "organization/SecretScanningPage/route.tsx"),
route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"),
route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"),