mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 17:40:44 +00:00
Merge branch 'main' into ENG-2661
This commit is contained in:
@@ -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.`} />
|
||||
)}
|
||||
|
||||
+86
@@ -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't see the mount you'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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
@@ -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:
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
+18
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
@@ -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;
|
||||
|
||||
+22
@@ -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>
|
||||
));
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -298,6 +304,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
SshCaByIDPage: setRoute(
|
||||
"/ssh/$projectId/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId"
|
||||
),
|
||||
SshHostGroupDetailsByIDPage: setRoute(
|
||||
"/ssh/$projectId/ssh-host-groups/$sshHostGroupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
|
||||
)
|
||||
},
|
||||
Public: {
|
||||
|
||||
@@ -175,6 +175,7 @@ export enum ProjectPermissionSub {
|
||||
SshCertificateTemplates = "ssh-certificate-templates",
|
||||
SshCertificates = "ssh-certificates",
|
||||
SshHosts = "ssh-hosts",
|
||||
SshHostGroups = "ssh-host-groups",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms",
|
||||
@@ -272,6 +273,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||
| [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ export {
|
||||
useAdminRemoveIdentitySuperAdminAccess,
|
||||
useCreateAdminUser,
|
||||
useRemoveUserServerAdminAccess,
|
||||
useUpdateAdminSlackConfig,
|
||||
useUpdateServerConfig,
|
||||
useUpdateServerEncryptionStrategy
|
||||
} from "./mutation";
|
||||
export {
|
||||
useAdminGetUsers,
|
||||
useGetAdminSlackConfig,
|
||||
useGetAdminIntegrationsConfig,
|
||||
useGetServerConfig,
|
||||
useGetServerRootKmsEncryptionDetails
|
||||
} from "./queries";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -95,8 +95,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",
|
||||
@@ -164,7 +166,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 } = {
|
||||
|
||||
@@ -99,8 +99,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",
|
||||
@@ -164,5 +164,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"
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
@@ -794,11 +795,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;
|
||||
@@ -806,10 +808,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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -907,8 +910,8 @@ export type Event =
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| UpdateProjectWorkflowIntegrationConfig
|
||||
| GetProjectWorkflowIntegrationConfig
|
||||
| IntegrationSyncedEvent;
|
||||
|
||||
export type AuditLog = {
|
||||
|
||||
@@ -107,6 +107,7 @@ export type CompleteAccountSignupDTO = CompleteAccountDTO & {
|
||||
providerAuthToken?: string;
|
||||
attributionSource?: string;
|
||||
organizationName: string;
|
||||
useDefaultOrg?: boolean;
|
||||
};
|
||||
|
||||
export type VerifySignupInviteDTO = {
|
||||
|
||||
@@ -44,6 +44,7 @@ export * from "./serviceTokens";
|
||||
export * from "./sshCa";
|
||||
export * from "./sshCertificateTemplates";
|
||||
export * from "./sshHost";
|
||||
export * from "./sshHostGroup";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./subscriptions";
|
||||
export * from "./tags";
|
||||
|
||||
@@ -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,3 +1,16 @@
|
||||
export enum LoginMappingSource {
|
||||
HOST = "host",
|
||||
HOST_GROUP = "hostGroup"
|
||||
}
|
||||
|
||||
export type TLoginMapping = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
source: LoginMappingSource;
|
||||
};
|
||||
|
||||
export type TSshHost = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -5,26 +18,15 @@ export type TSshHost = {
|
||||
alias: string | null;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings: TLoginMapping[];
|
||||
};
|
||||
|
||||
export type TCreateSshHostDTO = {
|
||||
projectId: string;
|
||||
hostname: string;
|
||||
alias?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TUpdateSshHostDTO = {
|
||||
@@ -33,12 +35,7 @@ export type TUpdateSshHostDTO = {
|
||||
alias?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings?: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TDeleteSshHostDTO = {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
useAddHostToSshHostGroup,
|
||||
useCreateSshHostGroup,
|
||||
useDeleteSshHostGroup,
|
||||
useRemoveHostFromSshHostGroup,
|
||||
useUpdateSshHostGroup
|
||||
} from "./mutations";
|
||||
export { useGetSshHostGroupById, useListSshHostGroupHosts } from "./queries";
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/query-keys";
|
||||
import { sshHostGroupKeys } from "./queries";
|
||||
import {
|
||||
TCreateSshHostGroupDTO,
|
||||
TDeleteSshHostGroupDTO,
|
||||
TSshHostGroup,
|
||||
TUpdateSshHostGroupDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshHostGroup, object, TCreateSshHostGroupDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data: hostGroup } = await apiRequest.post("/api/v1/ssh/host-groups", body);
|
||||
return hostGroup;
|
||||
},
|
||||
onSuccess: ({ projectId, id }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(id)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshHostGroup, object, TUpdateSshHostGroupDTO>({
|
||||
mutationFn: async ({ sshHostGroupId, ...body }) => {
|
||||
const { data: hostGroup } = await apiRequest.patch(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}`,
|
||||
body
|
||||
);
|
||||
return hostGroup;
|
||||
},
|
||||
onSuccess: ({ projectId }, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshHostGroup, object, TDeleteSshHostGroupDTO>({
|
||||
mutationFn: async ({ sshHostGroupId }) => {
|
||||
const { data: hostGroup } = await apiRequest.delete(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}`
|
||||
);
|
||||
return hostGroup;
|
||||
},
|
||||
onSuccess: ({ projectId }, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddHostToSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, object, { sshHostGroupId: string; sshHostId: string }>({
|
||||
mutationFn: async ({ sshHostGroupId, sshHostId }) => {
|
||||
await apiRequest.post(`/api/v1/ssh/host-groups/${sshHostGroupId}/hosts/${sshHostId}`);
|
||||
},
|
||||
onSuccess: (_, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.forSshHostGroupHosts(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRemoveHostFromSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, object, { sshHostGroupId: string; sshHostId: string }>({
|
||||
mutationFn: async ({ sshHostGroupId, sshHostId }) => {
|
||||
await apiRequest.delete(`/api/v1/ssh/host-groups/${sshHostGroupId}/hosts/${sshHostId}`);
|
||||
},
|
||||
onSuccess: (_, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.forSshHostGroupHosts(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { EHostGroupMembershipFilter, TListSshHostGroupHostsResponse, TSshHostGroup } from "./types";
|
||||
|
||||
export const sshHostGroupKeys = {
|
||||
getSshHostGroupById: (sshHostGroupId: string) => [{ sshHostGroupId }, "ssh-host-group"],
|
||||
allSshHostGroupHosts: () => ["ssh-host-group-hosts"] as const,
|
||||
forSshHostGroupHosts: (sshHostGroupId: string) =>
|
||||
[...sshHostGroupKeys.allSshHostGroupHosts(), sshHostGroupId] as const,
|
||||
specificSshHostGroupHosts: ({
|
||||
sshHostGroupId,
|
||||
filter
|
||||
}: {
|
||||
sshHostGroupId: string;
|
||||
filter?: EHostGroupMembershipFilter;
|
||||
}) => [...sshHostGroupKeys.forSshHostGroupHosts(sshHostGroupId), { filter }] as const
|
||||
};
|
||||
|
||||
export const useGetSshHostGroupById = (sshHostGroupId: string) => {
|
||||
return useQuery({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId),
|
||||
queryFn: async () => {
|
||||
const { data: sshHostGroup } = await apiRequest.get<TSshHostGroup>(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}`
|
||||
);
|
||||
return sshHostGroup;
|
||||
},
|
||||
enabled: Boolean(sshHostGroupId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListSshHostGroupHosts = ({
|
||||
sshHostGroupId,
|
||||
filter
|
||||
}: {
|
||||
sshHostGroupId: string;
|
||||
filter?: EHostGroupMembershipFilter;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: sshHostGroupKeys.specificSshHostGroupHosts({ sshHostGroupId, filter }),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
...(filter ? { filter } : {})
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<TListSshHostGroupHostsResponse>(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}/hosts`,
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(sshHostGroupId),
|
||||
staleTime: 0,
|
||||
gcTime: 0
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { TLoginMapping, TSshHost } from "../sshHost/types";
|
||||
|
||||
export type TSshHostGroup = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
loginMappings: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TCreateSshHostGroupDTO = {
|
||||
projectId: string;
|
||||
name: string;
|
||||
loginMappings: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TUpdateSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
name?: string;
|
||||
loginMappings?: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TDeleteSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
};
|
||||
|
||||
export type TListSshHostGroupHostsResponse = {
|
||||
hosts: (TSshHost & { joinedGroupAt: string; isPartOfGroup: boolean })[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export enum EHostGroupMembershipFilter {
|
||||
GROUP_MEMBERS = "group-members",
|
||||
NON_GROUP_MEMBERS = "non-group-members"
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export type SubscriptionPlan = {
|
||||
workspacesUsed: number;
|
||||
environmentLimit: number;
|
||||
samlSSO: boolean;
|
||||
sshHostGroups: boolean;
|
||||
secretAccessInsights: boolean;
|
||||
hsm: boolean;
|
||||
oidcSSO: boolean;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -26,9 +26,9 @@ export {
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceSlackConfig,
|
||||
useGetWorkspaceUserDetails,
|
||||
useGetWorkspaceUsers,
|
||||
useGetWorkspaceWorkflowIntegrationConfig,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates,
|
||||
useListWorkspaceCertificateTemplates,
|
||||
@@ -38,6 +38,7 @@ export {
|
||||
useListWorkspaceSshCas,
|
||||
useListWorkspaceSshCertificates,
|
||||
useListWorkspaceSshCertificateTemplates,
|
||||
useListWorkspaceSshHostGroups,
|
||||
useListWorkspaceSshHosts,
|
||||
useNameWorkspaceSecrets,
|
||||
useSearchProjects,
|
||||
|
||||
@@ -18,9 +18,13 @@ import { EncryptedSecret } from "../secrets/types";
|
||||
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
|
||||
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
|
||||
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,
|
||||
@@ -870,6 +874,21 @@ export const useListWorkspaceSshHosts = (projectId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceSshHostGroups = (projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { groups }
|
||||
} = await apiRequest.get<{ groups: (TSshHostGroup & { hostCount: number })[] }>(
|
||||
`/api/v2/workspace/${projectId}/ssh-host-groups`
|
||||
);
|
||||
return groups;
|
||||
},
|
||||
enabled: Boolean(projectId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId),
|
||||
@@ -883,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,12 +55,16 @@ 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,
|
||||
getWorkspaceSshHosts: (projectId: string) => [{ projectId }, "workspace-ssh-hosts"] as const,
|
||||
getWorkspaceSshHostGroups: (projectId: string) =>
|
||||
[{ projectId }, "workspace-ssh-host-groups"] as const,
|
||||
specificWorkspaceSshCertificates: ({
|
||||
offset,
|
||||
limit,
|
||||
|
||||
@@ -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'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:
|
||||
|
||||
+8
-5
@@ -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({
|
||||
|
||||
+5
@@ -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:
|
||||
|
||||
+224
@@ -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)
|
||||
});
|
||||
+25
-3
@@ -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>
|
||||
|
||||
+4
@@ -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>
|
||||
);
|
||||
|
||||
+192
@@ -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>
|
||||
);
|
||||
};
|
||||
+123
-44
@@ -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),
|
||||
|
||||
+12
-1
@@ -234,6 +234,7 @@ export const projectRoleFormSchema = z.object({
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.SshHostGroups]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
||||
@@ -380,7 +381,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.Kms,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
].includes(subject)
|
||||
) {
|
||||
// from above statement we are sure it won't be undefined
|
||||
@@ -1064,6 +1066,15 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SshHostGroups]: {
|
||||
title: "SSH Host Groups",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PkiCollections]: {
|
||||
title: "PKI Collections",
|
||||
actions: [
|
||||
|
||||
+14
@@ -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} />;
|
||||
};
|
||||
+3
@@ -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:
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+19
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+5
-1
@@ -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;
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
+125
-313
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+153
@@ -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>
|
||||
);
|
||||
};
|
||||
+33
@@ -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>
|
||||
);
|
||||
};
|
||||
+148
@@ -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>
|
||||
);
|
||||
};
|
||||
+556
@@ -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>
|
||||
);
|
||||
};
|
||||
+145
@@ -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>
|
||||
);
|
||||
};
|
||||
+355
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
PageHeader,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useDeleteSshHostGroup, useGetSshHostGroupById } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SshHostGroupModal } from "../SshHostsPage/components/SshHostGroupModal";
|
||||
import { SshHostGroupDetailsSection, SshHostGroupHostsSection } from "./components";
|
||||
|
||||
const Page = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const navigate = useNavigate();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
const sshHostGroupId = useParams({
|
||||
from: ROUTE_PATHS.Ssh.SshHostGroupDetailsByIDPage.id,
|
||||
select: (el) => el.sshHostGroupId
|
||||
});
|
||||
const { data } = useGetSshHostGroupById(sshHostGroupId);
|
||||
|
||||
const { mutateAsync: deleteSshHostGroup } = useDeleteSshHostGroup();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"sshHostGroup",
|
||||
"deleteSshHostGroup"
|
||||
] as const);
|
||||
|
||||
const onRemoveSshGroupSubmit = async (groupIdToDelete: string) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
|
||||
await deleteSshHostGroup({ sshHostGroupId: groupIdToDelete });
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted SSH group",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteSshHostGroup");
|
||||
navigate({
|
||||
to: `/${ProjectType.SSH}/$projectId/overview` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete SSH group",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<Button variant="outline_bg">More</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteSshHostGroup", {
|
||||
groupId: data.id,
|
||||
name: data.name
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete SSH Group
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<SshHostGroupDetailsSection
|
||||
sshHostGroupId={sshHostGroupId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<SshHostGroupHostsSection sshHostGroupId={sshHostGroupId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSshHostGroup.isOpen}
|
||||
title={`Are you sure want to remove the SSH group: ${
|
||||
(popUp?.deleteSshHostGroup?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveSshGroupSubmit((popUp?.deleteSshHostGroup?.data as { groupId: string })?.groupId)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SshHostGroupDetailsByIDPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "SSH Group" })}</title>
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
passThrough={false}
|
||||
renderGuardBanner
|
||||
>
|
||||
<Page />
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useAddHostToSshHostGroup, useListSshHostGroupHosts } from "@app/hooks/api";
|
||||
import { EHostGroupMembershipFilter } from "@app/hooks/api/sshHostGroup/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["addHostGroupMembers"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["addHostGroupMembers"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const AddHostGroupMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const popUpData = popUp?.addHostGroupMembers?.data as {
|
||||
sshHostGroupId: string;
|
||||
};
|
||||
|
||||
const { data, isPending } = useListSshHostGroupHosts({
|
||||
sshHostGroupId: popUpData?.sshHostGroupId,
|
||||
filter: EHostGroupMembershipFilter.NON_GROUP_MEMBERS
|
||||
});
|
||||
const { mutateAsync: addHostToSshHostGroup, isPending: isAddingHostToSshHostGroup } =
|
||||
useAddHostToSshHostGroup();
|
||||
|
||||
const handleAddHost = async (sshHostId: string) => {
|
||||
try {
|
||||
if (!popUpData?.sshHostGroupId) {
|
||||
createNotification({
|
||||
text: "Some data is missing, please refresh the page and try again",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await addHostToSshHostGroup({
|
||||
sshHostGroupId: popUpData.sshHostGroupId,
|
||||
sshHostId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully added host to the group",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to add host to the group",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addHostGroupMembers?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addHostGroupMembers", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Hosts to Group">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Alias</Th>
|
||||
<Th>Hostname</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="ssh-hosts" />}
|
||||
{!isPending &&
|
||||
data?.hosts?.map((host) => {
|
||||
return (
|
||||
<Tr className="items-center" key={`host-${host.id}`}>
|
||||
<Td>{host.alias ?? "-"}</Td>
|
||||
<Td>{host.hostname}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isLoading={isAddingHostToSshHostGroup}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="button"
|
||||
onClick={() => handleAddHost(host.id)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && !data?.hosts?.length && (
|
||||
<EmptyState title="No hosts available to add to the SSH host group" icon={faServer} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useGetSshHostGroupById } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
sshHostGroupId: string;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["sshHostGroup"]>, data?: object) => void;
|
||||
};
|
||||
|
||||
export const SshHostGroupDetailsSection = ({ sshHostGroupId, handlePopUpOpen }: Props) => {
|
||||
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
const { data: sshHostGroup } = useGetSshHostGroupById(sshHostGroupId);
|
||||
|
||||
return sshHostGroup ? (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">SSH Host Group Details</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Tooltip content="Edit SSH Host Group">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="edit icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("sshHostGroup", {
|
||||
sshHostGroupId: sshHostGroup.id
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">SSH Host Group ID</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm text-mineshaft-300">{sshHostGroup.id}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextId}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(sshHostGroup.id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{sshHostGroup.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useRemoveHostFromSshHostGroup } from "@app/hooks/api";
|
||||
|
||||
import { AddHostGroupMemberModal } from "./AddHostGroupMemberModal";
|
||||
import { SshHostGroupHostsTable } from "./SshHostGroupHostsTable";
|
||||
|
||||
type Props = {
|
||||
sshHostGroupId: string;
|
||||
};
|
||||
|
||||
export const SshHostGroupHostsSection = ({ sshHostGroupId }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"removeHostFromSshHostGroup",
|
||||
"addHostGroupMembers",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: removeHostFromGroup } = useRemoveHostFromSshHostGroup();
|
||||
|
||||
const handleAddSshHostModal = () => {
|
||||
if (!subscription?.sshHostGroups) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can manage hosts more efficiently with SSH host groups if you upgrade your Infisical plan to an Enterprise license."
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("addHostGroupMembers", {
|
||||
sshHostGroupId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveSshHostSubmit = async (sshHostId: string) => {
|
||||
try {
|
||||
await removeHostFromGroup({
|
||||
sshHostId,
|
||||
sshHostGroupId
|
||||
});
|
||||
|
||||
await createNotification({
|
||||
text: "Successfully removed host from SSH group",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("removeHostFromSshHostGroup");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to remove host from SSH group",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">SSH Hosts</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="add host"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => handleAddSshHostModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<SshHostGroupHostsTable sshHostGroupId={sshHostGroupId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<AddHostGroupMemberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeHostFromSshHostGroup.isOpen}
|
||||
title={`Are you sure want to remove ${
|
||||
(popUp?.removeHostFromSshHostGroup?.data as { hostname: string; alias?: string })
|
||||
?.alias ||
|
||||
(popUp?.removeHostFromSshHostGroup?.data as { hostname: string; alias?: string })
|
||||
?.hostname ||
|
||||
""
|
||||
} from this host group?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeHostFromSshHostGroup", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveSshHostSubmit(
|
||||
(popUp?.removeHostFromSshHostGroup?.data as { sshHostId: string })?.sshHostId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useListSshHostGroupHosts } from "@app/hooks/api";
|
||||
import { EHostGroupMembershipFilter } from "@app/hooks/api/sshHostGroup/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
sshHostGroupId: string;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeHostFromSshHostGroup"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const SshHostGroupHostsTable = ({ sshHostGroupId, handlePopUpOpen }: Props) => {
|
||||
const { data, isPending } = useListSshHostGroupHosts({
|
||||
sshHostGroupId,
|
||||
filter: EHostGroupMembershipFilter.GROUP_MEMBERS
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Alias</Th>
|
||||
<Th>Hostname</Th>
|
||||
<Th>Added On</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="ssh-host-group-hosts" />}
|
||||
{!isPending &&
|
||||
data?.hosts.map((host) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`host-${host.id}`}>
|
||||
<Td>{host.alias ?? "-"}</Td>
|
||||
<Td>{host.hostname}</Td>
|
||||
<Td>{new Date(host.joinedGroupAt).toLocaleDateString()}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip content="Remove host from group">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Remove host from group"
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeHostFromSshHostGroup", {
|
||||
sshHostId: host.id,
|
||||
alias: host.alias,
|
||||
hostname: host.hostname
|
||||
})
|
||||
}
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && !data?.hosts?.length && (
|
||||
<EmptyState title="No hosts have been added to this SSH host group" icon={faServer} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SshHostGroupHostsTable;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { SshHostGroupDetailsSection } from "./SshHostGroupDetailsSection";
|
||||
export { SshHostGroupHostsSection } from "./SshHostGroupHostsSection";
|
||||
export { SshHostGroupHostsTable } from "./SshHostGroupHostsTable";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { SshHostGroupDetailsByIDPage } from "./SshHostGroupDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
|
||||
)({
|
||||
component: SshHostGroupDetailsByIDPage
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { SshHostsSection } from "./components";
|
||||
import { SshHostGroupsSection, SshHostsSection } from "./components";
|
||||
|
||||
export const SshHostsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,6 +19,7 @@ export const SshHostsPage = () => {
|
||||
title="Hosts"
|
||||
description="Manage your SSH hosts, configure access policies, and define login behavior for secure connections."
|
||||
/>
|
||||
<SshHostGroupsSection />
|
||||
<SshHostsSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight, faPlus, faTrash } 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 {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useCreateSshHostGroup,
|
||||
useGetSshHostGroupById,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceSshHostGroups,
|
||||
useUpdateSshHostGroup
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["sshHostGroup"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshHostGroup"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(64),
|
||||
loginMappings: z
|
||||
.object({
|
||||
loginUser: z.string().trim().min(1),
|
||||
allowedPrincipals: z.array(z.string().trim()).default([])
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
const { data: sshHostGroups } = useListWorkspaceSshHostGroups(currentWorkspace.id);
|
||||
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
||||
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
|
||||
|
||||
const { data: sshHostGroup } = useGetSshHostGroupById(
|
||||
(popUp?.sshHostGroup?.data as { sshHostGroupId: string })?.sshHostGroupId || ""
|
||||
);
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateSshHostGroup();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateSshHostGroup();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
loginMappings: []
|
||||
}
|
||||
});
|
||||
|
||||
const loginMappingsFormFields = useFieldArray({
|
||||
control,
|
||||
name: "loginMappings"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (sshHostGroup) {
|
||||
reset({
|
||||
name: sshHostGroup.name,
|
||||
loginMappings: sshHostGroup.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: allowedPrincipals.usernames
|
||||
}))
|
||||
});
|
||||
|
||||
setExpandedMappings(
|
||||
Object.fromEntries(sshHostGroup.loginMappings.map((_, index) => [index, false]))
|
||||
);
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
loginMappings: []
|
||||
});
|
||||
}
|
||||
}, [sshHostGroup]);
|
||||
|
||||
const onFormSubmit = async ({ name, loginMappings }: FormData) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
|
||||
// check if there is already a different host group with the same name
|
||||
const existingNames =
|
||||
sshHostGroups?.filter((h) => h.id !== sshHostGroup?.id).map((h) => h.name) || [];
|
||||
|
||||
if (existingNames.includes(name.trim())) {
|
||||
createNotification({
|
||||
text: "A host group with this name already exists.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sshHostGroup) {
|
||||
await updateMutateAsync({
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
name,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
projectId,
|
||||
name,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("sshHostGroup", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${sshHostGroup ? "updated" : "created"} SSH host group`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${sshHostGroup ? "update" : "create"} SSH host group`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMapping = (index: number) => {
|
||||
setExpandedMappings((prev) => ({
|
||||
...prev,
|
||||
[index]: !prev[index]
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.sshHostGroup?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
reset();
|
||||
handlePopUpToggle("sshHostGroup", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${sshHostGroup ? "View" : "Add"} SSH host group`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{sshHostGroup && (
|
||||
<FormControl label="SSH Host Group ID">
|
||||
<Input value={sshHostGroup.id} isDisabled className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="production" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<FormLabel label="Login Mappings" />
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const newIndex = loginMappingsFormFields.fields.length;
|
||||
loginMappingsFormFields.append({ loginUser: "", allowedPrincipals: [""] });
|
||||
setExpandedMappings((prev) => ({
|
||||
...prev,
|
||||
[newIndex]: true
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Add Login Mapping
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-4 flex flex-col space-y-4">
|
||||
{loginMappingsFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||
<div
|
||||
key={metadataFieldId}
|
||||
className="flex flex-col space-y-2 rounded-md border border-mineshaft-600 p-4"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center py-1 text-sm text-mineshaft-200"
|
||||
onClick={() => toggleMapping(i)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={expandedMappings[i] ? faChevronDown : faChevronRight}
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`loginMappings.${i}.loginUser`}
|
||||
render={({ field }) => (
|
||||
<span className="text-sm font-medium leading-tight">
|
||||
{field.value || "New Login Mapping"}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<IconButton
|
||||
ariaLabel="delete login mapping"
|
||||
variant="plain"
|
||||
onClick={() => loginMappingsFormFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{expandedMappings[i] && (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-xs text-mineshaft-400">Login User</span>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`loginMappings.${i}.loginUser`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="ec2-user"
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
const loginMappings = getValues("loginMappings");
|
||||
const isDuplicate = loginMappings.some(
|
||||
(mapping, index) => index !== i && mapping.loginUser === newValue
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
createNotification({
|
||||
text: "This login user already exists",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(e);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<FormLabel
|
||||
label="Allowed Principals"
|
||||
className="text-xs text-mineshaft-400"
|
||||
/>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
||||
}}
|
||||
>
|
||||
Add Principal
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`loginMappings.${i}.allowedPrincipals`}
|
||||
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
{(value.length === 0 ? [""] : value).map(
|
||||
(principal: string, principalIndex: number) => (
|
||||
<div
|
||||
key={`${metadataFieldId}-principal-${principal}`}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={principal}
|
||||
onValueChange={(newValue) => {
|
||||
if (value.includes(newValue)) {
|
||||
createNotification({
|
||||
text: "This principal is already added",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newPrincipals = [...value];
|
||||
newPrincipals[principalIndex] = newValue;
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
placeholder="Select a member"
|
||||
className="w-full"
|
||||
>
|
||||
{members.map((member) => (
|
||||
<SelectItem
|
||||
key={member.user.id}
|
||||
value={member.user.username}
|
||||
>
|
||||
{member.user.username}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<IconButton
|
||||
size="sm"
|
||||
ariaLabel="delete principal"
|
||||
variant="plain"
|
||||
className="h-9"
|
||||
onClick={() => {
|
||||
const newPrincipals = value.filter(
|
||||
(_, idx) => idx !== principalIndex
|
||||
);
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{error && <span className="text-sm text-red-500">{error.message}</span>}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.sshHostGroup?.data ? "Update" : "Add"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("sshHostGroup", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import { useDeleteSshHostGroup } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SshHostGroupModal } from "./SshHostGroupModal";
|
||||
import { SshHostGroupsTable } from "./SshHostGroupsTable";
|
||||
|
||||
export const SshHostGroupsSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { mutateAsync: deleteSshHostGroup } = useDeleteSshHostGroup();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"sshHostGroup",
|
||||
"deleteSshHostGroup",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const handleAddSshHostGroupModal = () => {
|
||||
if (!subscription?.sshHostGroups) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can manage hosts more efficiently with SSH host groups if you upgrade your Infisical plan to an Enterprise license."
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("sshHostGroup");
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveSshHostGroupSubmit = async (sshHostGroupId: string) => {
|
||||
try {
|
||||
const hostGroup = await deleteSshHostGroup({ sshHostGroupId });
|
||||
|
||||
createNotification({
|
||||
text: `Successfully deleted SSH host group: ${hostGroup.name}`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteSshHostGroup");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete SSH host group",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Host Groups</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="button"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddSshHostGroupModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Group
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<SshHostGroupsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSshHostGroup.isOpen}
|
||||
title={`Are you sure you want to remove the SSH host group: ${popUp?.deleteSshHostGroup?.data?.name}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveSshHostGroupSubmit(
|
||||
(popUp?.deleteSshHostGroup?.data as { sshHostGroupId: string })?.sshHostGroupId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import { faEllipsis, faPencil, faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useListWorkspaceSshHostGroups } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteSshHostGroup", "sshHostGroup"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isPending } = useListWorkspaceSshHostGroups(currentWorkspace?.id || "");
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table className="w-full table-fixed">
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th># Hosts in Group</Th>
|
||||
<Th>Login User - Authorized Principals Mapping</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="org-ssh-cas" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map((group) => {
|
||||
return (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`ssh-host-group-${group.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: `/${ProjectType.SSH}/$projectId/ssh-host-groups/$sshHostGroupId` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
sshHostGroupId: group.id
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{group.name}</Td>
|
||||
<Td>{group.hostCount}</Td>
|
||||
<Td>
|
||||
{group.loginMappings.length === 0 ? (
|
||||
<span className="italic text-mineshaft-400">None</span>
|
||||
) : (
|
||||
group.loginMappings.map(({ loginUser, allowedPrincipals }) => (
|
||||
<div key={`${group.id}-${loginUser}`} className="mb-2">
|
||||
<div className="text-mineshaft-200">{loginUser}</div>
|
||||
{allowedPrincipals.usernames.map((username) => (
|
||||
<div key={`${group.id}-${loginUser}-${username}`} className="ml-4">
|
||||
└─ {username}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</Td>
|
||||
<Td className="text-right align-middle">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("sshHostGroup", {
|
||||
sshHostGroupId: group.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faPencil} />}
|
||||
>
|
||||
Edit Host Group
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: `/${ProjectType.SSH}/$projectId/ssh-host-groups/$sshHostGroupId` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
sshHostGroupId: group.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faServer} />}
|
||||
>
|
||||
Manage Hosts
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteSshHostGroup", {
|
||||
sshHostGroupId: group.id,
|
||||
name: group.name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Host Group
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && data?.length === 0 && (
|
||||
<EmptyState title="No SSH host groups have been created" icon={faServer} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
useListWorkspaceSshHosts,
|
||||
useUpdateSshHost
|
||||
} from "@app/hooks/api";
|
||||
import { LoginMappingSource } from "@app/hooks/api/sshHost/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -48,7 +49,8 @@ const schema = z
|
||||
loginMappings: z
|
||||
.object({
|
||||
loginUser: z.string().trim().min(1),
|
||||
allowedPrincipals: z.array(z.string().trim()).default([])
|
||||
allowedPrincipals: z.array(z.string().trim()).default([]),
|
||||
source: z.nativeEnum(LoginMappingSource)
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
@@ -99,9 +101,10 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
hostname: sshHost.hostname,
|
||||
alias: sshHost.alias ?? "",
|
||||
userCertTtl: sshHost.userCertTtl,
|
||||
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals, source }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: allowedPrincipals.usernames
|
||||
allowedPrincipals: allowedPrincipals.usernames,
|
||||
source
|
||||
}))
|
||||
});
|
||||
|
||||
@@ -122,6 +125,11 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
|
||||
// Filter out login mappings that are from host groups
|
||||
const hostLoginMappings = loginMappings.filter(
|
||||
(mapping) => mapping.source === LoginMappingSource.HOST
|
||||
);
|
||||
|
||||
// check if there is already a different host with the same hostname
|
||||
const existingHostnames =
|
||||
sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || [];
|
||||
@@ -157,7 +165,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
hostname,
|
||||
alias: trimmedAlias,
|
||||
userCertTtl,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
@@ -170,7 +178,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
hostname,
|
||||
alias: trimmedAlias,
|
||||
userCertTtl,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
@@ -265,7 +273,11 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const newIndex = loginMappingsFormFields.fields.length;
|
||||
loginMappingsFormFields.append({ loginUser: "", allowedPrincipals: [""] });
|
||||
loginMappingsFormFields.append({
|
||||
loginUser: "",
|
||||
allowedPrincipals: [""],
|
||||
source: LoginMappingSource.HOST
|
||||
});
|
||||
setExpandedMappings((prev) => ({
|
||||
...prev,
|
||||
[newIndex]: true
|
||||
@@ -298,6 +310,12 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
render={({ field }) => (
|
||||
<span className="text-sm font-medium leading-tight">
|
||||
{field.value || "New Login Mapping"}
|
||||
{loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP && (
|
||||
<span className="ml-2 text-xs text-mineshaft-400">
|
||||
(inherited from host group)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
@@ -306,6 +324,9 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
ariaLabel="delete login mapping"
|
||||
variant="plain"
|
||||
onClick={() => loginMappingsFormFields.remove(i)}
|
||||
isDisabled={
|
||||
loginMappingsFormFields.fields[i].source === LoginMappingSource.HOST_GROUP
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
@@ -327,11 +348,24 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="ec2-user"
|
||||
isDisabled={
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
)
|
||||
return;
|
||||
|
||||
const newValue = e.target.value;
|
||||
const loginMappings = getValues("loginMappings");
|
||||
const isDuplicate = loginMappings.some(
|
||||
(mapping, index) => index !== i && mapping.loginUser === newValue
|
||||
(mapping, index) =>
|
||||
index !== i &&
|
||||
mapping.loginUser === newValue &&
|
||||
mapping.source === LoginMappingSource.HOST
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
@@ -355,17 +389,20 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
label="Allowed Principals"
|
||||
className="text-xs text-mineshaft-400"
|
||||
/>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
||||
}}
|
||||
>
|
||||
Add Principal
|
||||
</Button>
|
||||
{loginMappingsFormFields.fields[i].source === LoginMappingSource.HOST && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const current =
|
||||
getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
||||
}}
|
||||
>
|
||||
Add Principal
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -382,6 +419,12 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Select
|
||||
value={principal}
|
||||
onValueChange={(newValue) => {
|
||||
if (
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
)
|
||||
return;
|
||||
|
||||
if (value.includes(newValue)) {
|
||||
createNotification({
|
||||
text: "This principal is already added",
|
||||
@@ -395,6 +438,10 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}}
|
||||
placeholder="Select a member"
|
||||
className="w-full"
|
||||
isDisabled={
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
}
|
||||
>
|
||||
{members.map((member) => (
|
||||
<SelectItem
|
||||
@@ -412,11 +459,21 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
variant="plain"
|
||||
className="h-9"
|
||||
onClick={() => {
|
||||
if (
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
)
|
||||
return;
|
||||
|
||||
const newPrincipals = value.filter(
|
||||
(_, idx) => idx !== principalIndex
|
||||
);
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
isDisabled={
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api";
|
||||
import { LoginMappingSource } from "@app/hooks/api/sshHost/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -63,7 +64,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Table className="w-full table-fixed">
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Alias</Th>
|
||||
@@ -90,16 +91,73 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
{host.loginMappings.length === 0 ? (
|
||||
<span className="italic text-mineshaft-400">None</span>
|
||||
) : (
|
||||
host.loginMappings.map(({ loginUser, allowedPrincipals }) => (
|
||||
<div key={`${host.id}-${loginUser}`} className="mb-2">
|
||||
<div className="text-mineshaft-200">{loginUser}</div>
|
||||
{allowedPrincipals.usernames.map((username) => (
|
||||
<div key={`${host.id}-${loginUser}-${username}`} className="ml-4">
|
||||
└─ {username}
|
||||
(() => {
|
||||
const hostMappings = host.loginMappings.filter(
|
||||
(m) => m.source !== LoginMappingSource.HOST_GROUP
|
||||
);
|
||||
const groupMappings = host.loginMappings.filter(
|
||||
(m) => m.source === LoginMappingSource.HOST_GROUP
|
||||
);
|
||||
|
||||
const hostLoginUserToPrincipals = hostMappings.reduce(
|
||||
(acc, { loginUser, allowedPrincipals }) => {
|
||||
acc[loginUser] = new Set(allowedPrincipals.usernames);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Set<string>>
|
||||
);
|
||||
|
||||
const entriesFromHost = hostMappings.map(
|
||||
({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
source: LoginMappingSource.HOST,
|
||||
usernames: allowedPrincipals.usernames
|
||||
})
|
||||
);
|
||||
|
||||
const entriesFromGroup = groupMappings
|
||||
.map(({ loginUser, allowedPrincipals }) => {
|
||||
const existing = hostLoginUserToPrincipals[loginUser] || new Set();
|
||||
const filteredUsernames = allowedPrincipals.usernames.filter(
|
||||
(u) => !existing.has(u)
|
||||
);
|
||||
return filteredUsernames.length > 0
|
||||
? {
|
||||
loginUser,
|
||||
source: LoginMappingSource.HOST_GROUP,
|
||||
usernames: filteredUsernames
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean) as {
|
||||
loginUser: string;
|
||||
source: LoginMappingSource;
|
||||
usernames: string[];
|
||||
}[];
|
||||
|
||||
return [...entriesFromHost, ...entriesFromGroup]
|
||||
.sort((a, b) => a.loginUser.localeCompare(b.loginUser))
|
||||
.map(({ loginUser, usernames, source }) => (
|
||||
<div key={`${host.id}-${loginUser}-${source}`} className="mb-2">
|
||||
<div className="text-mineshaft-200">
|
||||
{loginUser}
|
||||
{source === LoginMappingSource.HOST_GROUP && (
|
||||
<span className="ml-2 text-xs text-mineshaft-400">
|
||||
(inherited from host group)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{usernames.map((username) => (
|
||||
<div
|
||||
key={`${host.id}-${loginUser}-${source}-${username}`}
|
||||
className="ml-4"
|
||||
>
|
||||
└─ {username}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
));
|
||||
})()
|
||||
)}
|
||||
</Td>
|
||||
<Td className="text-right align-middle">
|
||||
@@ -139,7 +197,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faPencil} />}
|
||||
>
|
||||
Edit SSH host
|
||||
Edit Host
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -161,7 +219,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete SSH host
|
||||
Delete Host
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { SshHostGroupsSection } from "./SshHostGroupsSection";
|
||||
export { SshHostsSection } from "./SshHostsSection";
|
||||
|
||||
+133
-29
@@ -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'
|
||||
@@ -107,6 +108,7 @@ import { Route as projectRoleDetailsBySlugPageRouteCertManagerImport } from './p
|
||||
import { Route as certManagerPkiCollectionDetailsByIDPageRoutesImport } from './pages/cert-manager/PkiCollectionDetailsByIDPage/routes'
|
||||
import { Route as projectMemberDetailsByIDPageRouteCertManagerImport } from './pages/project/MemberDetailsByIDPage/route-cert-manager'
|
||||
import { Route as projectIdentityDetailsByIDPageRouteCertManagerImport } from './pages/project/IdentityDetailsByIDPage/route-cert-manager'
|
||||
import { Route as sshSshHostGroupDetailsByIDPageRouteImport } from './pages/ssh/SshHostGroupDetailsByIDPage/route'
|
||||
import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/route'
|
||||
import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route'
|
||||
import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route'
|
||||
@@ -217,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',
|
||||
@@ -483,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',
|
||||
@@ -521,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',
|
||||
@@ -670,6 +676,14 @@ const organizationCertManagerOverviewPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const organizationSettingsPageRouteRoute =
|
||||
organizationSettingsPageRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute,
|
||||
} as any)
|
||||
|
||||
const organizationSecretSharingPageRouteRoute =
|
||||
organizationSecretSharingPageRouteImport.update({
|
||||
id: '/',
|
||||
@@ -864,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',
|
||||
@@ -1001,6 +1023,13 @@ const projectIdentityDetailsByIDPageRouteCertManagerRoute =
|
||||
getParentRoute: () => certManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const sshSshHostGroupDetailsByIDPageRouteRoute =
|
||||
sshSshHostGroupDetailsByIDPageRouteImport.update({
|
||||
id: '/ssh-host-groups/$sshHostGroupId',
|
||||
path: '/ssh-host-groups/$sshHostGroupId',
|
||||
getParentRoute: () => sshLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const sshSshCaByIDPageRouteRoute = sshSshCaByIDPageRouteImport.update({
|
||||
id: '/ca/$caId',
|
||||
path: '/ca/$caId',
|
||||
@@ -1952,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'
|
||||
@@ -1994,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'
|
||||
@@ -2029,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'
|
||||
@@ -2169,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'
|
||||
@@ -2379,6 +2422,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof sshSshCaByIDPageRouteImport
|
||||
parentRoute: typeof sshLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId'
|
||||
path: '/ssh-host-groups/$sshHostGroupId'
|
||||
fullPath: '/ssh/$projectId/ssh-host-groups/$sshHostGroupId'
|
||||
preLoaderRoute: typeof sshSshHostGroupDetailsByIDPageRouteImport
|
||||
parentRoute: typeof sshLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId'
|
||||
path: '/identities/$identityId'
|
||||
@@ -3090,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
|
||||
@@ -3119,13 +3186,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
|
||||
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
|
||||
organizationSecretScanningPageRouteRoute:
|
||||
organizationSecretScanningPageRouteRoute,
|
||||
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren,
|
||||
organizationCertManagerOverviewPageRouteRoute:
|
||||
organizationCertManagerOverviewPageRouteRoute,
|
||||
organizationGroupDetailsByIDPageRouteRoute:
|
||||
@@ -3546,6 +3614,7 @@ interface sshLayoutRouteChildren {
|
||||
sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute
|
||||
projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute
|
||||
sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute
|
||||
sshSshHostGroupDetailsByIDPageRouteRoute: typeof sshSshHostGroupDetailsByIDPageRouteRoute
|
||||
projectIdentityDetailsByIDPageRouteSshRoute: typeof projectIdentityDetailsByIDPageRouteSshRoute
|
||||
projectMemberDetailsByIDPageRouteSshRoute: typeof projectMemberDetailsByIDPageRouteSshRoute
|
||||
projectRoleDetailsBySlugPageRouteSshRoute: typeof projectRoleDetailsBySlugPageRouteSshRoute
|
||||
@@ -3558,6 +3627,8 @@ const sshLayoutRouteChildren: sshLayoutRouteChildren = {
|
||||
sshSettingsPageRouteRoute: sshSettingsPageRouteRoute,
|
||||
projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute,
|
||||
sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute,
|
||||
sshSshHostGroupDetailsByIDPageRouteRoute:
|
||||
sshSshHostGroupDetailsByIDPageRouteRoute,
|
||||
projectIdentityDetailsByIDPageRouteSshRoute:
|
||||
projectIdentityDetailsByIDPageRouteSshRoute,
|
||||
projectMemberDetailsByIDPageRouteSshRoute:
|
||||
@@ -3809,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
|
||||
@@ -3836,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
|
||||
@@ -3866,6 +3939,7 @@ export interface FileRoutesByFullPath {
|
||||
'/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||
'/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
'/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute
|
||||
'/ssh/$projectId/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
||||
'/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute
|
||||
'/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute
|
||||
@@ -3989,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
|
||||
@@ -3997,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
|
||||
@@ -4013,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
|
||||
@@ -4042,6 +4117,7 @@ export interface FileRoutesByTo {
|
||||
'/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||
'/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
'/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute
|
||||
'/ssh/$projectId/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
||||
'/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute
|
||||
'/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute
|
||||
@@ -4175,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
|
||||
@@ -4206,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
|
||||
@@ -4236,6 +4314,7 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId': typeof sshSshCaByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute
|
||||
@@ -4365,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'
|
||||
@@ -4392,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'
|
||||
@@ -4422,6 +4503,7 @@ export interface FileRouteTypes {
|
||||
| '/secret-manager/$projectId/integrations/select-integration-auth'
|
||||
| '/secret-manager/$projectId/secrets/$envSlug'
|
||||
| '/ssh/$projectId/ca/$caId'
|
||||
| '/ssh/$projectId/ssh-host-groups/$sshHostGroupId'
|
||||
| '/cert-manager/$projectId/identities/$identityId'
|
||||
| '/cert-manager/$projectId/members/$membershipId'
|
||||
| '/cert-manager/$projectId/pki-collections/$collectionId'
|
||||
@@ -4544,7 +4626,6 @@ export interface FileRouteTypes {
|
||||
| '/organization/audit-logs'
|
||||
| '/organization/billing'
|
||||
| '/organization/secret-scanning'
|
||||
| '/organization/settings'
|
||||
| '/cert-manager/$projectId'
|
||||
| '/kms/$projectId'
|
||||
| '/secret-manager/$projectId'
|
||||
@@ -4552,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'
|
||||
@@ -4568,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'
|
||||
@@ -4597,6 +4680,7 @@ export interface FileRouteTypes {
|
||||
| '/secret-manager/$projectId/integrations/select-integration-auth'
|
||||
| '/secret-manager/$projectId/secrets/$envSlug'
|
||||
| '/ssh/$projectId/ca/$caId'
|
||||
| '/ssh/$projectId/ssh-host-groups/$sshHostGroupId'
|
||||
| '/cert-manager/$projectId/identities/$identityId'
|
||||
| '/cert-manager/$projectId/members/$membershipId'
|
||||
| '/cert-manager/$projectId/pki-collections/$collectionId'
|
||||
@@ -4728,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'
|
||||
@@ -4759,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'
|
||||
@@ -4789,6 +4875,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId'
|
||||
@@ -5117,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",
|
||||
@@ -5162,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",
|
||||
@@ -5203,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",
|
||||
@@ -5229,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"
|
||||
@@ -5321,6 +5416,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug"
|
||||
@@ -5354,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"
|
||||
@@ -5554,6 +5654,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "ssh/SshCaByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId": {
|
||||
"filePath": "ssh/SshHostGroupDetailsByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId": {
|
||||
"filePath": "project/IdentityDetailsByIDPage/route-cert-manager.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
||||
|
||||
@@ -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"),
|
||||
@@ -312,6 +315,7 @@ const sshRoutes = route("/ssh/$projectId", [
|
||||
route("/certificates", "ssh/SshCertsPage/route.tsx"),
|
||||
route("/cas", "ssh/SshCasPage/route.tsx"),
|
||||
route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"),
|
||||
route("/ssh-host-groups/$sshHostGroupId", "ssh/SshHostGroupDetailsByIDPage/route.tsx"),
|
||||
route("/settings", "ssh/SettingsPage/route.tsx"),
|
||||
route("/access-management", "project/AccessControlPage/route-ssh.tsx"),
|
||||
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-ssh.tsx"),
|
||||
|
||||
+35
-29
@@ -1,6 +1,6 @@
|
||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig, PluginOption } from "vite";
|
||||
import { defineConfig, loadEnv, PluginOption } from "vite";
|
||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
@@ -20,32 +20,38 @@ const virtualRouteFileChangeReloadPlugin: PluginOption = {
|
||||
};
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 3000
|
||||
// proxy: {
|
||||
// "/api": {
|
||||
// target: "http://localhost:8080",
|
||||
// changeOrigin: true,
|
||||
// secure: false,
|
||||
// ws: true
|
||||
// }
|
||||
// }
|
||||
},
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
nodePolyfills({
|
||||
globals: {
|
||||
Buffer: true
|
||||
}
|
||||
}),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
TanStackRouterVite({
|
||||
virtualRouteConfig: "./src/routes.ts"
|
||||
}),
|
||||
react(),
|
||||
virtualRouteFileChangeReloadPlugin
|
||||
]
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
const allowedHosts = env.VITE_ALLOWED_HOSTS?.split(",") ?? [];
|
||||
|
||||
return {
|
||||
server: {
|
||||
allowedHosts,
|
||||
host: true,
|
||||
port: 3000
|
||||
// proxy: {
|
||||
// "/api": {
|
||||
// target: "http://localhost:8080",
|
||||
// changeOrigin: true,
|
||||
// secure: false,
|
||||
// ws: true
|
||||
// }
|
||||
// }
|
||||
},
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
nodePolyfills({
|
||||
globals: {
|
||||
Buffer: true
|
||||
}
|
||||
}),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
TanStackRouterVite({
|
||||
virtualRouteConfig: "./src/routes.ts"
|
||||
}),
|
||||
react(),
|
||||
virtualRouteFileChangeReloadPlugin
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user