mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 00:27:30 +00:00
Merge pull request #5016 from Infisical/PAM-79
improvement(pam): cards for flat account view
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
import { Button } from "@app/components/v2";
|
import { faBorderAll, faList } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { IconButton } from "@app/components/v2";
|
||||||
import { PamAccountView } from "@app/hooks/api/pam";
|
import { PamAccountView } from "@app/hooks/api/pam";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -9,30 +12,32 @@ type Props = {
|
|||||||
export const AccountViewToggle = ({ value, onChange }: Props) => {
|
export const AccountViewToggle = ({ value, onChange }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
<div className="flex gap-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||||
<Button
|
<IconButton
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(PamAccountView.Flat);
|
onChange(PamAccountView.Flat);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel="grid"
|
||||||
size="xs"
|
size="xs"
|
||||||
className={`${
|
className={`${
|
||||||
value === PamAccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
|
value === PamAccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
|
||||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||||
>
|
>
|
||||||
Hide Folders
|
<FontAwesomeIcon icon={faBorderAll} />
|
||||||
</Button>
|
</IconButton>
|
||||||
<Button
|
<IconButton
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(PamAccountView.Nested);
|
onChange(PamAccountView.Nested);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel="list"
|
||||||
size="xs"
|
size="xs"
|
||||||
className={`${
|
className={`${
|
||||||
value === PamAccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
|
value === PamAccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
|
||||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||||
>
|
>
|
||||||
Show Folders
|
<FontAwesomeIcon icon={faList} />
|
||||||
</Button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Badge, UnstableButton } from "@app/components/v3";
|
||||||
|
import { PAM_RESOURCE_TYPE_MAP, TPamAccount } from "@app/hooks/api/pam";
|
||||||
|
import { LogInIcon, PackageOpenIcon } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
account: TPamAccount;
|
||||||
|
onAccess: (resource: TPamAccount) => void;
|
||||||
|
accountPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PamAccountCard = ({ account, onAccess, accountPath }: Props) => {
|
||||||
|
const { name, description, resource } = account;
|
||||||
|
|
||||||
|
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[account.resource.resourceType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={account.id}
|
||||||
|
className="flex flex-col overflow-clip rounded-sm border border-mineshaft-600 bg-mineshaft-800 p-4 text-start transition-transform duration-100"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3.5">
|
||||||
|
<img
|
||||||
|
alt={resourceTypeName}
|
||||||
|
src={`/images/integrations/${image}`}
|
||||||
|
className="size-10 object-contain"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<p className="truncate text-lg font-medium text-mineshaft-100">{name}</p>
|
||||||
|
<UnstableButton onClick={() => onAccess(account)} size="xs" variant="outline">
|
||||||
|
<LogInIcon />
|
||||||
|
Connect
|
||||||
|
</UnstableButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className={`${accountPath ? "text-mineshaft-300" : "text-mineshaft-400"} truncate text-xs leading-4`}
|
||||||
|
>
|
||||||
|
{resourceTypeName} - {accountPath || "root"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="neutral" className="mt-3.5">
|
||||||
|
<PackageOpenIcon />
|
||||||
|
{resource.name}
|
||||||
|
</Badge>
|
||||||
|
<p className="mt-2 truncate text-sm text-mineshaft-400">{description || "No description"}</p>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { formatDistance } from "date-fns";
|
import { formatDistance } from "date-fns";
|
||||||
import { FolderIcon, PackageOpenIcon } from "lucide-react";
|
import { PackageOpenIcon } from "lucide-react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -39,8 +39,6 @@ type Props = {
|
|||||||
onUpdate: (resource: TPamAccount) => void;
|
onUpdate: (resource: TPamAccount) => void;
|
||||||
onDelete: (resource: TPamAccount) => void;
|
onDelete: (resource: TPamAccount) => void;
|
||||||
search: string;
|
search: string;
|
||||||
isFlatView: boolean;
|
|
||||||
accountPath?: string;
|
|
||||||
isAccessLoading?: boolean;
|
isAccessLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,8 +48,6 @@ export const PamAccountRow = ({
|
|||||||
onAccess,
|
onAccess,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onDelete,
|
onDelete,
|
||||||
isFlatView,
|
|
||||||
accountPath,
|
|
||||||
isAccessLoading
|
isAccessLoading
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { id, name } = account;
|
const { id, name } = account;
|
||||||
@@ -95,14 +91,6 @@ export const PamAccountRow = ({
|
|||||||
<HighlightText text={account.resource.name} highlight={search} />
|
<HighlightText text={account.resource.name} highlight={search} />
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
{isFlatView && accountPath && (
|
|
||||||
<Badge variant="neutral">
|
|
||||||
<FolderIcon />
|
|
||||||
<span>
|
|
||||||
<HighlightText text={accountPath} highlight={search} />
|
|
||||||
</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{"lastRotatedAt" in account && account.lastRotatedAt && (
|
{"lastRotatedAt" in account && account.lastRotatedAt && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="max-w-sm text-center"
|
className="max-w-sm text-center"
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import { useListPamAccounts, useListPamResources } from "@app/hooks/api/pam/quer
|
|||||||
import { AccountViewToggle } from "./AccountViewToggle";
|
import { AccountViewToggle } from "./AccountViewToggle";
|
||||||
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
||||||
import { PamAccessAccountModal } from "./PamAccessAccountModal";
|
import { PamAccessAccountModal } from "./PamAccessAccountModal";
|
||||||
|
import { PamAccountCard } from "./PamAccountCard";
|
||||||
import { PamAccountRow } from "./PamAccountRow";
|
import { PamAccountRow } from "./PamAccountRow";
|
||||||
import { PamAddAccountModal } from "./PamAddAccountModal";
|
import { PamAddAccountModal } from "./PamAddAccountModal";
|
||||||
import { PamAddFolderModal } from "./PamAddFolderModal";
|
import { PamAddFolderModal } from "./PamAddFolderModal";
|
||||||
@@ -128,7 +129,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
|||||||
setOrderDirection,
|
setOrderDirection,
|
||||||
setOrderBy
|
setOrderBy
|
||||||
} = usePagination<PamAccountOrderBy>(PamAccountOrderBy.Name, {
|
} = usePagination<PamAccountOrderBy>(PamAccountOrderBy.Name, {
|
||||||
initPerPage: getUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, 20),
|
initPerPage: getUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, 18),
|
||||||
initSearch
|
initSearch
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,10 +232,25 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
|||||||
|
|
||||||
const resources = resourcesData?.resources || [];
|
const resources = resourcesData?.resources || [];
|
||||||
|
|
||||||
|
function accessAccount(account: TPamAccount) {
|
||||||
|
// For AWS IAM, directly open console without modal
|
||||||
|
if (account.resource.resourceType === PamResourceType.AwsIam) {
|
||||||
|
let fullAccountPath = account?.name;
|
||||||
|
const folderPath = account.folderId ? folderPaths[account.folderId] : undefined;
|
||||||
|
if (folderPath) {
|
||||||
|
const path = folderPath.replace(/^\/+|\/+$/g, "");
|
||||||
|
fullAccountPath = `${path}/${account?.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
accessAwsIam(account, fullAccountPath);
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("accessAccount", account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col gap-4">
|
||||||
{accountView === PamAccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
<div className="flex gap-2">
|
||||||
<div className="mt-4 flex gap-2">
|
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
|
||||||
{(isAllowed) =>
|
{(isAllowed) =>
|
||||||
isAllowed && (
|
isAllowed && (
|
||||||
@@ -392,32 +408,64 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer className="mt-4">
|
{accountView === PamAccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
||||||
<Table>
|
{accountView === PamAccountView.Flat ? (
|
||||||
<THead>
|
<>
|
||||||
<Tr>
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<Th>
|
{filteredAccounts.map((account) => (
|
||||||
<div className="flex items-center">
|
<PamAccountCard
|
||||||
Accounts
|
key={account.id}
|
||||||
<IconButton
|
account={account}
|
||||||
variant="plain"
|
accountPath={account.folderId ? folderPaths[account.folderId] : undefined}
|
||||||
className={getClassName(PamAccountOrderBy.Name)}
|
onAccess={(e: TPamAccount) => accessAccount(e)}
|
||||||
ariaLabel="sort"
|
/>
|
||||||
onClick={() => handleSort(PamAccountOrderBy.Name)}
|
))}
|
||||||
>
|
</div>
|
||||||
<FontAwesomeIcon icon={getColSortIcon(PamAccountOrderBy.Name)} />
|
{!isLoading && isContentEmpty && (
|
||||||
</IconButton>
|
<EmptyState
|
||||||
</div>
|
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
||||||
</Th>
|
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||||
<Th className="w-5" />
|
className="rounded border border-mineshaft-500"
|
||||||
</Tr>
|
/>
|
||||||
</THead>
|
)}
|
||||||
<TBody>
|
{Boolean(totalCount) && !isLoading && !isContentEmpty && (
|
||||||
{isLoading && <TableSkeleton columns={2} innerKey="pam-accounts" />}
|
<Pagination
|
||||||
{!isLoading && (
|
className="col-span-full justify-start! border-transparent bg-transparent pl-2"
|
||||||
<>
|
count={totalCount}
|
||||||
{accountView !== PamAccountView.Flat &&
|
page={page}
|
||||||
foldersToRender.map((folder) => (
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
perPageList={[9, 18, 48, 99]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Accounts
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PamAccountOrderBy.Name)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PamAccountOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PamAccountOrderBy.Name)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={2} innerKey="pam-accounts" />}
|
||||||
|
{!isLoading && (
|
||||||
|
<>
|
||||||
|
{foldersToRender.map((folder) => (
|
||||||
<PamFolderRow
|
<PamFolderRow
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
folder={folder}
|
folder={folder}
|
||||||
@@ -427,53 +475,39 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
|||||||
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
|
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{filteredAccounts.map((account) => (
|
{filteredAccounts.map((account) => (
|
||||||
<PamAccountRow
|
<PamAccountRow
|
||||||
key={account.id}
|
key={account.id}
|
||||||
account={account}
|
account={account}
|
||||||
search={search}
|
search={search}
|
||||||
isFlatView={accountView === PamAccountView.Flat}
|
isAccessLoading={loadingAccountId === account.id}
|
||||||
accountPath={account.folderId ? folderPaths[account.folderId] : undefined}
|
onAccess={(e: TPamAccount) => accessAccount(e)}
|
||||||
isAccessLoading={loadingAccountId === account.id}
|
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
||||||
onAccess={(e: TPamAccount) => {
|
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
||||||
// For AWS IAM, directly open console without modal
|
/>
|
||||||
if (e.resource.resourceType === PamResourceType.AwsIam) {
|
))}
|
||||||
let fullAccountPath = e?.name;
|
</>
|
||||||
const folderPath = e.folderId ? folderPaths[e.folderId] : undefined;
|
)}
|
||||||
if (folderPath) {
|
</TBody>
|
||||||
const path = folderPath.replace(/^\/+|\/+$/g, "");
|
</Table>
|
||||||
fullAccountPath = `${path}/${e?.name}`;
|
{!isLoading && isContentEmpty && (
|
||||||
}
|
<EmptyState
|
||||||
|
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
||||||
accessAwsIam(e, fullAccountPath);
|
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||||
} else {
|
/>
|
||||||
handlePopUpOpen("accessAccount", e);
|
)}
|
||||||
}
|
{Boolean(totalCount) && !isLoading && !isContentEmpty && (
|
||||||
}}
|
<Pagination
|
||||||
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
count={totalCount}
|
||||||
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
page={page}
|
||||||
/>
|
perPage={perPage}
|
||||||
))}
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
</>
|
onChangePerPage={handlePerPageChange}
|
||||||
)}
|
perPageList={[9, 18, 48, 99]}
|
||||||
</TBody>
|
/>
|
||||||
</Table>
|
)}
|
||||||
{Boolean(totalCount) && !isLoading && (
|
</TableContainer>
|
||||||
<Pagination
|
)}
|
||||||
count={totalCount}
|
|
||||||
page={page}
|
|
||||||
perPage={perPage}
|
|
||||||
onChangePage={(newPage) => setPage(newPage)}
|
|
||||||
onChangePerPage={handlePerPageChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isLoading && isContentEmpty && (
|
|
||||||
<EmptyState
|
|
||||||
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
|
||||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<PamDeleteFolderModal
|
<PamDeleteFolderModal
|
||||||
isOpen={popUp.deleteFolder.isOpen}
|
isOpen={popUp.deleteFolder.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const PamAddAccountModal = ({
|
|||||||
onComplete={(account) => {
|
onComplete={(account) => {
|
||||||
if (onComplete) onComplete(account);
|
if (onComplete) onComplete(account);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
setSelectedResource(null);
|
||||||
}}
|
}}
|
||||||
onBack={() => setSelectedResource(null)}
|
onBack={() => setSelectedResource(null)}
|
||||||
resourceId={selectedResource.id}
|
resourceId={selectedResource.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user