mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
cards for flat account view
This commit is contained in:
@@ -49,7 +49,7 @@ export const Pagination = ({
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-end border-t border-mineshaft-600 bg-mineshaft-800 px-4 py-3 text-white",
|
||||
"border-mineshaft-600 bg-mineshaft-800 flex w-full items-center justify-end border-t px-4 py-3 text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -94,7 +94,7 @@ export const Pagination = ({
|
||||
onClick={() => onChangePage(1)}
|
||||
isDisabled={!canGoFirst}
|
||||
>
|
||||
<FontAwesomeIcon className="absolute top-1 left-2.5 text-xs" icon={faChevronLeft} />
|
||||
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronLeft} />
|
||||
<FontAwesomeIcon className="text-xs" icon={faChevronLeft} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
@@ -120,7 +120,7 @@ export const Pagination = ({
|
||||
onClick={() => onChangePage(upperLimit)}
|
||||
isDisabled={!canGoLast}
|
||||
>
|
||||
<FontAwesomeIcon className="absolute top-1 left-2.5 text-xs" icon={faChevronRight} />
|
||||
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronRight} />
|
||||
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -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";
|
||||
|
||||
type Props = {
|
||||
@@ -8,31 +11,33 @@ type Props = {
|
||||
|
||||
export const AccountViewToggle = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<div className="flex gap-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<Button
|
||||
<div className="border-mineshaft-600 bg-mineshaft-800 flex gap-0.5 rounded-md border p-1">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(PamAccountView.Flat);
|
||||
}}
|
||||
ariaLabel="grid"
|
||||
size="xs"
|
||||
className={`${
|
||||
value === PamAccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
} hover:bg-mineshaft-600 min-w-[2.4rem] rounded border-none`}
|
||||
>
|
||||
Hide Folders
|
||||
</Button>
|
||||
<Button
|
||||
<FontAwesomeIcon icon={faBorderAll} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(PamAccountView.Nested);
|
||||
}}
|
||||
ariaLabel="list"
|
||||
size="xs"
|
||||
className={`${
|
||||
value === PamAccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
} hover:bg-mineshaft-600 min-w-[2.4rem] rounded border-none`}
|
||||
>
|
||||
Show Folders
|
||||
</Button>
|
||||
<FontAwesomeIcon icon={faList} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const FolderBreadCrumbs = ({ path = "/" }: Props) => {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 py-1 pr-2 pl-5 text-sm hover:bg-mineshaft-600"
|
||||
className="breadcrumb border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-600 relative z-20 border-solid py-1 pl-5 pr-2 text-sm"
|
||||
onClick={() => onFolderCrumbClick(0)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
@@ -48,7 +48,7 @@ export const FolderBreadCrumbs = ({ path = "/" }: Props) => {
|
||||
key={`path-${index + 1}`}
|
||||
className={`breadcrumb relative z-20 ${
|
||||
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
|
||||
} border-solid border-mineshaft-600 py-1 pr-2 pl-5 text-sm text-mineshaft-200`}
|
||||
} border-mineshaft-600 text-mineshaft-200 border-solid py-1 pl-5 pr-2 text-sm`}
|
||||
onClick={() => onFolderCrumbClick(index + 1)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamAccount } from "@app/hooks/api/pam";
|
||||
import { LogInIcon } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
account: TPamAccount;
|
||||
onAccess: (resource: TPamAccount) => void;
|
||||
accountPath?: string;
|
||||
};
|
||||
|
||||
export const PamAccountCard = ({ account, onAccess, accountPath }: Props) => {
|
||||
const { name, description } = account;
|
||||
|
||||
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[account.resource.resourceType];
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onAccess(account)}
|
||||
type="button"
|
||||
key={account.id}
|
||||
className="border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-700 flex cursor-pointer flex-col overflow-clip rounded-sm border p-4 text-start transition-transform duration-100 hover:scale-[103%]"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="border-mineshaft-500 bg-mineshaft-600 rounded-sm border p-1.5 shadow-inner">
|
||||
<img
|
||||
alt={resourceTypeName}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="size-7 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex justify-between gap-2">
|
||||
<p className="text-mineshaft-100 truncate text-lg font-medium">{name}</p>
|
||||
<LogInIcon className="text-mineshaft-400 size-5" />
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={`${accountPath ? "text-mineshaft-300" : "text-mineshaft-400"} truncate text-xs leading-4`}
|
||||
>
|
||||
{accountPath || "root"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-mineshaft-400 mt-4 truncate text-sm">{description || "No description"}</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { FolderIcon, PackageOpenIcon } from "lucide-react";
|
||||
import { PackageOpenIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -39,8 +39,6 @@ type Props = {
|
||||
onUpdate: (resource: TPamAccount) => void;
|
||||
onDelete: (resource: TPamAccount) => void;
|
||||
search: string;
|
||||
isFlatView: boolean;
|
||||
accountPath?: string;
|
||||
isAccessLoading?: boolean;
|
||||
};
|
||||
|
||||
@@ -50,8 +48,6 @@ export const PamAccountRow = ({
|
||||
onAccess,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
isFlatView,
|
||||
accountPath,
|
||||
isAccessLoading
|
||||
}: Props) => {
|
||||
const { id, name } = account;
|
||||
@@ -95,14 +91,6 @@ export const PamAccountRow = ({
|
||||
<HighlightText text={account.resource.name} highlight={search} />
|
||||
</span>
|
||||
</Badge>
|
||||
{isFlatView && accountPath && (
|
||||
<Badge variant="neutral">
|
||||
<FolderIcon />
|
||||
<span>
|
||||
<HighlightText text={accountPath} highlight={search} />
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{"lastRotatedAt" in account && account.lastRotatedAt && (
|
||||
<Tooltip
|
||||
className="max-w-sm text-center"
|
||||
|
||||
@@ -62,6 +62,7 @@ import { useListPamAccounts, useListPamResources } from "@app/hooks/api/pam/quer
|
||||
import { AccountViewToggle } from "./AccountViewToggle";
|
||||
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
||||
import { PamAccessAccountModal } from "./PamAccessAccountModal";
|
||||
import { PamAccountCard } from "./PamAccountCard";
|
||||
import { PamAccountRow } from "./PamAccountRow";
|
||||
import { PamAddAccountModal } from "./PamAddAccountModal";
|
||||
import { PamAddFolderModal } from "./PamAddFolderModal";
|
||||
@@ -128,7 +129,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<PamAccountOrderBy>(PamAccountOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, 20),
|
||||
initPerPage: getUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, 16),
|
||||
initSearch
|
||||
});
|
||||
|
||||
@@ -231,10 +232,25 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
|
||||
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 (
|
||||
<div>
|
||||
{accountView === PamAccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
||||
<div className="mt-4 flex gap-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-2">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
@@ -244,6 +260,12 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
setPage(1);
|
||||
setFilter({ resourceIds: [] });
|
||||
setAccountView(e);
|
||||
|
||||
// Reset perPage to appropriate default for the view
|
||||
const newPerPage = e === PamAccountView.Flat ? 16 : 10;
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, newPerPage);
|
||||
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
@@ -277,14 +299,14 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 min-w-10 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
"border-mineshaft-600 bg-mineshaft-800 hover:border-primary/60 hover:bg-primary/10 flex h-10 min-w-10 items-center justify-center overflow-hidden border p-0 transition-all",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="max-h-[70vh] thin-scrollbar overflow-y-auto" align="end">
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>Resource</DropdownMenuLabel>
|
||||
{resources.length ? (
|
||||
resources.map((resource) => {
|
||||
@@ -359,7 +381,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
ariaLabel="add-folder-or-import"
|
||||
className="rounded-l-none bg-mineshaft-600 p-3"
|
||||
className="bg-mineshaft-600 rounded-l-none p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</IconButton>
|
||||
@@ -392,32 +414,55 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<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 && (
|
||||
<>
|
||||
{accountView !== PamAccountView.Flat &&
|
||||
foldersToRender.map((folder) => (
|
||||
{accountView === PamAccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
||||
{accountView === PamAccountView.Flat ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{filteredAccounts.map((account) => (
|
||||
<PamAccountCard
|
||||
key={account.id}
|
||||
account={account}
|
||||
accountPath={account.folderId ? folderPaths[account.folderId] : undefined}
|
||||
onAccess={(e: TPamAccount) => accessAccount(e)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Pagination
|
||||
className="justify-start! col-span-full border-transparent bg-transparent pl-2"
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
perPageList={[8, 12, 16, 20, 40]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<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
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
@@ -427,53 +472,39 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
|
||||
/>
|
||||
))}
|
||||
{filteredAccounts.map((account) => (
|
||||
<PamAccountRow
|
||||
key={account.id}
|
||||
account={account}
|
||||
search={search}
|
||||
isFlatView={accountView === PamAccountView.Flat}
|
||||
accountPath={account.folderId ? folderPaths[account.folderId] : undefined}
|
||||
isAccessLoading={loadingAccountId === account.id}
|
||||
onAccess={(e: TPamAccount) => {
|
||||
// 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) {
|
||||
const path = folderPath.replace(/^\/+|\/+$/g, "");
|
||||
fullAccountPath = `${path}/${e?.name}`;
|
||||
}
|
||||
{filteredAccounts.map((account) => (
|
||||
<PamAccountRow
|
||||
key={account.id}
|
||||
account={account}
|
||||
search={search}
|
||||
isAccessLoading={loadingAccountId === account.id}
|
||||
onAccess={(e: TPamAccount) => accessAccount(e)}
|
||||
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
||||
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
|
||||
accessAwsIam(e, fullAccountPath);
|
||||
} else {
|
||||
handlePopUpOpen("accessAccount", e);
|
||||
}
|
||||
}}
|
||||
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
||||
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(totalCount) && !isLoading && (
|
||||
<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>
|
||||
{Boolean(totalCount) && !isLoading && (
|
||||
<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
|
||||
isOpen={popUp.deleteFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||
|
||||
Reference in New Issue
Block a user