Revise PR based on review

This commit is contained in:
Tuan Dang
2025-05-02 12:16:51 -07:00
parent 83df0850ce
commit 6a5748150a
12 changed files with 130 additions and 22 deletions
@@ -4,7 +4,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema"; import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
import { sanitizedSshHostGroup } from "@app/ee/services/ssh-host-group/ssh-host-group-schema"; import { sanitizedSshHostGroup } from "@app/ee/services/ssh-host-group/ssh-host-group-schema";
import { EHostGroupMembershipFilter } from "@app/ee/services/ssh-host-group/ssh-host-group-types"; import { EHostGroupMembershipFilter } from "@app/ee/services/ssh-host-group/ssh-host-group-types";
import { SSH_HOST_GROUPS } from "@app/lib/api-docs"; import { ApiDocsTags, SSH_HOST_GROUPS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -18,6 +18,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Get SSH Host Group",
params: z.object({ params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId) sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
}), }),
@@ -60,6 +63,8 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Create SSH Host Group", description: "Create SSH Host Group",
body: z.object({ body: z.object({
projectId: z.string().describe(SSH_HOST_GROUPS.CREATE.projectId), projectId: z.string().describe(SSH_HOST_GROUPS.CREATE.projectId),
@@ -107,6 +112,8 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Update SSH Host Group", description: "Update SSH Host Group",
params: z.object({ params: z.object({
sshHostGroupId: z.string().trim().describe(SSH_HOST_GROUPS.UPDATE.sshHostGroupId) sshHostGroupId: z.string().trim().describe(SSH_HOST_GROUPS.UPDATE.sshHostGroupId)
@@ -155,6 +162,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Delete SSH Host Group",
params: z.object({ params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE.sshHostGroupId) sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE.sshHostGroupId)
}), }),
@@ -197,6 +207,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Get SSH Hosts in a Host Group",
params: z.object({ params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId) sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
}), }),
@@ -256,6 +269,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Add an SSH Host to a Host Group",
params: z.object({ params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.sshHostGroupId), sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.sshHostGroupId),
hostId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.hostId) hostId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.hostId)
@@ -301,6 +317,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Remove an SSH Host from a Host Group",
params: z.object({ params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.sshHostGroupId), sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.sshHostGroupId),
hostId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.hostId) hostId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.hostId)
+21 -2
View File
@@ -5,7 +5,7 @@ import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certif
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema"; import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
import { LoginMappingSource } from "@app/ee/services/ssh-host/ssh-host-types"; import { LoginMappingSource } from "@app/ee/services/ssh-host/ssh-host-types";
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators"; import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
import { SSH_HOSTS } from "@app/lib/api-docs"; import { ApiDocsTags, SSH_HOSTS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
@@ -22,6 +22,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
response: { response: {
200: z.array( 200: z.array(
sanitizedSshHost.extend({ sanitizedSshHost.extend({
@@ -54,6 +56,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
params: z.object({ params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId) sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
}), }),
@@ -100,7 +104,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
description: "Add an SSH Host", hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Register SSH Host",
body: z.object({ body: z.object({
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId), projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
hostname: z hostname: z
@@ -176,6 +182,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Update SSH Host", description: "Update SSH Host",
params: z.object({ params: z.object({
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId) sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
@@ -252,6 +260,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Delete SSH Host",
params: z.object({ params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId) sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
}), }),
@@ -299,6 +310,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Issue SSH certificate for user", description: "Issue SSH certificate for user",
params: z.object({ params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId) sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
@@ -371,6 +384,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Issue SSH certificate for host", description: "Issue SSH certificate for host",
params: z.object({ params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId) sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId)
@@ -435,6 +450,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: publicSshCaLimit rateLimit: publicSshCaLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Get public key of the user SSH CA linked to the host", description: "Get public key of the user SSH CA linked to the host",
params: z.object({ params: z.object({
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId) sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
@@ -456,6 +473,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: publicSshCaLimit rateLimit: publicSshCaLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Get public key of the host SSH CA linked to the host", description: "Get public key of the host SSH CA linked to the host",
params: z.object({ params: z.object({
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId) sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
@@ -90,15 +90,19 @@ export const sshHostDALFactory = (db: TDbClient) => {
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } =
hostRows[0]; hostRows[0];
const loginMappingGrouped = groupBy(hostRows, (r) => `${r.loginUser}|${r.source}`); const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
const loginMappings = Object.entries(loginMappingGrouped).map(([key]) => { const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, mappings]) => {
const [loginUser, source] = key.split("|"); // Prefer HOST source over HOST_GROUP
const preferredMapping =
mappings.find((m) => m.source === LoginMappingSource.HOST) ||
mappings.find((m) => m.source === LoginMappingSource.HOST_GROUP);
return { return {
loginUser, loginUser,
allowedPrincipals: { allowedPrincipals: {
usernames: [user.username] usernames: [user.username]
}, },
source: source as LoginMappingSource source: preferredMapping!.source
}; };
}); });
+2
View File
@@ -48,6 +48,8 @@ export enum ApiDocsTags {
SshCertificates = "SSH Certificates", SshCertificates = "SSH Certificates",
SshCertificateAuthorities = "SSH Certificate Authorities", SshCertificateAuthorities = "SSH Certificate Authorities",
SshCertificateTemplates = "SSH Certificate Templates", SshCertificateTemplates = "SSH Certificate Templates",
SshHosts = "SSH Hosts",
SshHostGroups = "SSH Host Groups",
KmsKeys = "KMS Keys", KmsKeys = "KMS Keys",
KmsEncryption = "KMS Encryption", KmsEncryption = "KMS Encryption",
KmsSigning = "KMS Signing" KmsSigning = "KMS Signing"
@@ -43,6 +43,9 @@ export const useUpdateSshHostGroup = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId) queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
}); });
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
});
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId) queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
}); });
@@ -63,6 +66,9 @@ export const useDeleteSshHostGroup = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId) queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
}); });
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
});
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId) queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
}); });
@@ -95,7 +95,8 @@ const Page = () => {
)} )}
onClick={() => onClick={() =>
handlePopUpOpen("deleteSshHostGroup", { handlePopUpOpen("deleteSshHostGroup", {
groupId: data.id groupId: data.id,
name: data.name
}) })
} }
disabled={!isAllowed} disabled={!isAllowed}
@@ -123,7 +124,9 @@ const Page = () => {
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteSshHostGroup.isOpen} isOpen={popUp.deleteSshHostGroup.isOpen}
title="Are you sure want to remove the SSH group from the project?" title={`Are you sure want to remove the SSH group: ${
(popUp?.deleteSshHostGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
@@ -88,9 +88,13 @@ export const SshHostGroupHostsSection = ({ sshHostGroupId }: Props) => {
<AddHostGroupMemberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <AddHostGroupMemberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.removeHostFromSshHostGroup.isOpen} isOpen={popUp.removeHostFromSshHostGroup.isOpen}
title={`Are you sure want to remove the host ${ title={`Are you sure want to remove ${
(popUp?.removeHostFromSshHostGroup?.data as { hostname: string })?.hostname || "" (popUp?.removeHostFromSshHostGroup?.data as { hostname: string; alias?: string })
} from this SSH group?`} ?.alias ||
(popUp?.removeHostFromSshHostGroup?.data as { hostname: string; alias?: string })
?.hostname ||
""
} from this host group?`}
onChange={(isOpen) => handlePopUpToggle("removeHostFromSshHostGroup", isOpen)} onChange={(isOpen) => handlePopUpToggle("removeHostFromSshHostGroup", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
@@ -1,4 +1,4 @@
import { faServer, faUserMinus } from "@fortawesome/free-solid-svg-icons"; import { faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
@@ -68,13 +68,14 @@ export const SshHostGroupHostsTable = ({ sshHostGroupId, handlePopUpOpen }: Prop
onClick={() => onClick={() =>
handlePopUpOpen("removeHostFromSshHostGroup", { handlePopUpOpen("removeHostFromSshHostGroup", {
sshHostId: host.id, sshHostId: host.id,
alias: host.alias,
hostname: host.hostname hostname: host.hostname
}) })
} }
variant="plain" variant="plain"
colorSchema="danger" colorSchema="danger"
> >
<FontAwesomeIcon icon={faUserMinus} /> <FontAwesomeIcon icon={faTrash} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
@@ -77,7 +77,7 @@ export const SshHostGroupsSection = () => {
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteSshHostGroup.isOpen} isOpen={popUp.deleteSshHostGroup.isOpen}
title="Are you sure you want to remove the SSH host group?" title={`Are you sure you want to remove the SSH host group: ${popUp?.deleteSshHostGroup?.data?.name}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
@@ -114,7 +114,33 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />} icon={<FontAwesomeIcon icon={faPencil} />}
> >
Edit SSH host group 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> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -130,13 +156,14 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("deleteSshHostGroup", { handlePopUpOpen("deleteSshHostGroup", {
sshHostGroupId: group.id sshHostGroupId: group.id,
name: group.name
}); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />} icon={<FontAwesomeIcon icon={faTrash} />}
> >
Delete SSH host group Delete Host Group
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -362,7 +362,10 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
const newValue = e.target.value; const newValue = e.target.value;
const loginMappings = getValues("loginMappings"); const loginMappings = getValues("loginMappings");
const isDuplicate = loginMappings.some( const isDuplicate = loginMappings.some(
(mapping, index) => index !== i && mapping.loginUser === newValue (mapping, index) =>
index !== i &&
mapping.loginUser === newValue &&
mapping.source === LoginMappingSource.HOST
); );
if (isDuplicate) { if (isDuplicate) {
@@ -91,7 +91,27 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
{host.loginMappings.length === 0 ? ( {host.loginMappings.length === 0 ? (
<span className="italic text-mineshaft-400">None</span> <span className="italic text-mineshaft-400">None</span>
) : ( ) : (
host.loginMappings.map(({ loginUser, allowedPrincipals, source }) => ( Object.entries(
host.loginMappings.reduce(
(acc, mapping) => {
const { loginUser, source } = mapping;
const existing = acc[loginUser];
if (!existing) {
acc[loginUser] = mapping;
} else if (
existing.source === LoginMappingSource.HOST_GROUP &&
source === LoginMappingSource.HOST
) {
// Prefer HOST over HOST_GROUP
acc[loginUser] = mapping;
}
return acc;
},
{} as Record<string, (typeof host.loginMappings)[number]>
)
).map(([loginUser, { allowedPrincipals, source }]) => (
<div key={`${host.id}-${loginUser}`} className="mb-2"> <div key={`${host.id}-${loginUser}`} className="mb-2">
<div className="text-mineshaft-200"> <div className="text-mineshaft-200">
{loginUser} {loginUser}
@@ -147,7 +167,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />} icon={<FontAwesomeIcon icon={faPencil} />}
> >
Edit SSH host Edit Host
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -169,7 +189,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />} icon={<FontAwesomeIcon icon={faTrash} />}
> >
Delete SSH host Delete Host
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>