merge main

This commit is contained in:
x032205
2025-10-10 20:33:52 -04:00
parent a61536adbb
commit a804672042
1020 changed files with 23364 additions and 13891 deletions
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -55,18 +55,14 @@ const Content = ({ onClose }: ContentProps) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { data, isFetching } = useAdminGetUsers(
{
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
},
{
placeholderData: (prev) => prev
}
);
const { data, isPending } = useAdminGetUsers({
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
});
const users = useMemo(() => data?.pages.flat().filter((user) => !user.superAdmin) ?? [], [data]);
const { users: usersData = [] } = data ?? {};
const users = usersData.filter((user) => !user.superAdmin);
const onSubmit = async ({ user }: FormData) => {
try {
@@ -91,7 +87,7 @@ const Content = ({ onClose }: ContentProps) => {
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="User">
<FilterableSelect
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
isLoading={searchUserFilter !== debouncedSearchTerm || isPending}
className="w-full"
placeholder="Search users..."
options={users}
@@ -11,7 +11,6 @@ import {
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { InfiniteData } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -28,6 +27,7 @@ import {
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
@@ -39,7 +39,12 @@ import {
Tr
} from "@app/components/v2";
import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import {
useAdminBulkDeleteUsers,
useAdminDeleteUser,
@@ -54,15 +59,17 @@ const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions f
const ServerAdminsPanelTable = ({
handlePopUpOpen,
users: usersPages,
users,
isPending,
searchUserFilter,
setSearchUserFilter,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
selectedUsers,
setSelectedUsers
setSelectedUsers,
totalCount,
page,
perPage,
setPage,
handlePerPageChange
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
@@ -75,20 +82,20 @@ const ServerAdminsPanelTable = ({
}
) => void;
isPending: boolean;
users: InfiniteData<User[], unknown> | undefined;
users?: User[];
searchUserFilter: string;
setSearchUserFilter: (filter: string) => void;
selectedUsers: User[];
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
hasNextPage: boolean;
totalCount: number;
page: number;
perPage: number;
setPage: Dispatch<SetStateAction<number>>;
handlePerPageChange: (newPerPage: number) => void;
}) => {
const { subscription } = useSubscription();
const users = usersPages?.pages.flat();
const isEmpty = !isPending && !users?.length;
const isEmpty = !isPending && totalCount === 0;
const selectedUserIds = selectedUsers.map((user) => user.id);
@@ -208,7 +215,7 @@ const ServerAdminsPanelTable = ({
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -bottom-[0.01rem] -right-1"
className="absolute -right-1 -bottom-[0.01rem]"
size="2xs"
icon={faXmark}
/>
@@ -241,16 +248,13 @@ const ServerAdminsPanelTable = ({
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</>
@@ -278,18 +282,24 @@ export const ServerAdminsTable = () => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const {
data: users,
isPending,
isFetchingNextPage,
hasNextPage,
fetchNextPage
} = useAdminGetUsers({
limit: 20,
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
initPerPage: getUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, 20)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, newPerPage);
};
const { data, isPending } = useAdminGetUsers({
limit,
offset,
searchTerm: debouncedSearchTerm,
adminsOnly: true
});
const { users = [], totalCount = 0 } = data ?? {};
const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string };
@@ -351,7 +361,7 @@ export const ServerAdminsTable = () => {
<>
<div
className={twMerge(
"h-0 flex-shrink-0 overflow-hidden transition-all",
"h-0 shrink-0 overflow-hidden transition-all",
selectedUsers.length > 0 && "h-16"
)}
>
@@ -389,9 +399,11 @@ export const ServerAdminsTable = () => {
searchUserFilter={searchUserFilter}
setSearchUserFilter={setSearchUserFilter}
isPending={isPending}
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
page={page}
perPage={perPage}
setPage={setPage}
handlePerPageChange={handlePerPageChange}
totalCount={totalCount}
/>
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}
@@ -433,7 +445,7 @@ export const ServerAdminsTable = () => {
<div className="mt-4 text-sm text-mineshaft-400">
The following users will be deleted:
</div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<div className="mt-2 max-h-80 overflow-y-auto rounded-sm border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc">
{selectedUsers?.map((user) => {
const email = user.email ?? user.username;
@@ -455,7 +467,7 @@ export const ServerAdminsTable = () => {
<div className="inline-block">
<Badge
variant="primary"
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
@@ -115,7 +115,7 @@ export const AuthenticationPageForm = () => {
onSubmit={handleSubmit(onAuthFormSubmit)}
>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Login Methods</div>
<div className="mb-2 text-xl font-medium text-mineshaft-100">Login Methods</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select the login methods you wish to allow for all users of this instance.
</div>
@@ -60,7 +60,7 @@ export const CachingPageForm = () => {
<div className="mb-6 flex flex-wrap items-end justify-between gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-3">
<span className="text-xl font-semibold text-mineshaft-100">Secrets Cache</span>
<span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span>
{isInvalidating && (
<Badge
variant="danger"
@@ -82,7 +82,7 @@ export const EncryptionPageForm = () => {
>
<div className="flex flex-col justify-start">
<div className="flex w-full justify-between">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
<div className="mb-2 text-xl font-medium text-mineshaft-100">
KMS Encryption Strategy
</div>
</div>
@@ -58,7 +58,7 @@ export const GroupContainer = ({
icon={faChevronRight}
/>
<div className="flex-grow select-none text-base">{group.name}</div>
<div className="grow text-base select-none">{group.name}</div>
</div>
{(open || search) && (
@@ -214,13 +214,13 @@ export const EnvironmentPageForm = () => {
<div className="flex w-full flex-row items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Overrides</p>
<p className="text-xl font-medium text-mineshaft-100">Overrides</p>
<a
href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
@@ -107,7 +107,7 @@ export const GeneralPageForm = () => {
onSubmit={handleSubmit(onFormSubmit)}
>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Allow user signups</div>
<div className="mb-2 text-xl font-medium text-mineshaft-100">Allow user signups</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want users to be able to signup freely into your Infisical instance.
</div>
@@ -132,7 +132,7 @@ export const GeneralPageForm = () => {
</div>
{signUpMode === "anyone" && (
<div className="flex flex-col justify-start">
<div className="mb-4 text-xl font-semibold text-mineshaft-100">
<div className="mb-4 text-xl font-medium text-mineshaft-100">
Restrict signup by email domain(s)
</div>
<Controller
@@ -159,7 +159,7 @@ export const GeneralPageForm = () => {
)}
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Default organization</div>
<div className="mb-2 text-xl font-medium text-mineshaft-100">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/Github logins. When
selected, user logins will be automatically scoped to the selected organization.
@@ -197,7 +197,7 @@ export const GeneralPageForm = () => {
</div>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Trust emails</div>
<div className="mb-2 text-xl font-medium text-mineshaft-100">Trust emails</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want Infisical to trust external emails from SAML/LDAP/OIDC identity
providers. If set to false, then Infisical will prompt SAML/LDAP/OIDC provisioned users to
@@ -257,7 +257,7 @@ export const GeneralPageForm = () => {
</div>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Notices</div>
<div className="mb-2 text-xl font-medium text-mineshaft-100">Notices</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-400">
Configure system-wide notification banners and security messages. These settings control
the text displayed to users during authentication and throughout their session
@@ -274,7 +274,7 @@ export const GeneralPageForm = () => {
placeholder="**Auth Consent Message**"
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
className="h-48 thin-scrollbar max-w-lg resize-none! bg-mineshaft-800"
/>
</FormControl>
)}
@@ -293,7 +293,7 @@ export const GeneralPageForm = () => {
placeholder='<div style="background-color: red">TOP SECRET</div>'
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
className="h-48 thin-scrollbar max-w-lg resize-none! bg-mineshaft-800"
/>
</FormControl>
)}
@@ -53,7 +53,7 @@ export const IntegrationsPageForm = () => {
return (
<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-xl font-medium text-mineshaft-100">Integrations</div>
<div className="text-sm text-mineshaft-300">
Configure your instance-wide settings to enable integration with third-party services.
</div>
@@ -75,7 +75,7 @@ export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props
<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</div>
<div className="text-[15px] font-medium">Microsoft Teams</div>
</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
@@ -29,7 +29,7 @@ const getCustomSlackAppCreationUrl = () =>
- 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: {
@@ -108,7 +108,7 @@ export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
<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</div>
<div className="text-[15px] font-medium">Slack</div>
</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -72,18 +72,13 @@ const Content = ({ onClose }: ContentProps) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { data, isFetching } = useAdminGetUsers(
{
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
},
{
placeholderData: (prev) => prev
}
);
const { data, isPending } = useAdminGetUsers({
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
});
const users = useMemo(() => data?.pages.flat() ?? [], [data]);
const { users = [] } = data ?? {};
const onSubmit = async ({ name, invitees }: FormData) => {
try {
@@ -155,7 +150,7 @@ const Content = ({ onClose }: ContentProps) => {
})
.includes(input)
}
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
isLoading={searchUserFilter !== debouncedSearchTerm || isPending}
className="w-full"
placeholder="Search users or invite new ones..."
isMulti
@@ -11,7 +11,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
@@ -20,6 +19,7 @@ import {
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
@@ -29,7 +29,12 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useDebounce, usePopUp } from "@app/hooks";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin";
import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -48,14 +53,34 @@ const IdentityPanelTable = ({
const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities(
{
limit: 20,
searchTerm: debouncedSearchTerm
}
);
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
initPerPage: getUserTablePreference(
"ResourceOverviewIdentitiesTable",
PreferenceKey.PerPage,
10
)
});
const isEmpty = !isPending && !data?.pages?.[0].length;
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("ResourceOverviewIdentitiesTable", PreferenceKey.PerPage, newPerPage);
};
const { data, isPending } = useAdminGetIdentities({
limit,
offset,
searchTerm: debouncedSearchTerm
});
const { identities, totalCount = 0 } = data ?? {};
const isEmpty = !isPending && !totalCount;
useResetPageHelper({
totalCount,
offset,
setPage
});
return (
<>
@@ -80,75 +105,68 @@ const IdentityPanelTable = ({
<TBody>
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
{!isPending &&
data?.pages?.map((identities) =>
identities.map(({ name, id, isInstanceAdmin }) => (
<Tr key={`identity-${id}`} className="w-full">
<Td>
{name}
{isInstanceAdmin && (
<Badge variant="primary" className="ml-2">
Server Admin
</Badge>
)}
</Td>
<Td>
{isInstanceAdmin && (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
{isInstanceAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeServerAdmin", { name, id });
}}
icon={
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -bottom-[0.01rem] -right-1"
size="2xs"
icon={faXmark}
/>
</div>
}
>
Remove Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Td>
</Tr>
))
)}
identities?.map(({ name, id, isInstanceAdmin }) => (
<Tr key={`identity-${id}`} className="w-full">
<Td>
{name}
{isInstanceAdmin && (
<Badge variant="primary" className="ml-2">
Server Admin
</Badge>
)}
</Td>
<Td>
{isInstanceAdmin && (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeServerAdmin", { name, id });
}}
icon={
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -right-1 -bottom-[0.01rem]"
size="2xs"
icon={faXmark}
/>
</div>
}
>
Remove Server Admin
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Td>
</Tr>
))}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faWrench} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
{!isPending && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</>
@@ -185,7 +203,7 @@ export const MachineIdentitiesTable = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-mineshaft-100">Machine Identities</p>
<p className="text-xl font-medium text-mineshaft-100">Machine Identities</p>
<p className="text-sm text-bunker-300">Manage machine identities across your instance.</p>
</div>
</div>
@@ -48,6 +48,11 @@ import {
} from "@app/components/v2";
import { useUser } from "@app/context";
import { OrgMembershipRole } from "@app/helpers/roles";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useAdminDeleteOrganization,
@@ -291,7 +296,7 @@ const ViewMembersModalContent = ({
</div>
</Td>
<Td className="max-w-0">
<Badge className="flex w-fit max-w-full items-center gap-x-1 whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
<Badge className="flex w-fit max-w-full items-center gap-x-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-200">
<p className="truncate capitalize">{member.role.replace("-", " ")}</p>
{Boolean(member.roleId) && (
<Tooltip content="This member has a custom role assigned.">
@@ -443,13 +448,31 @@ const OrganizationsPanelTable = ({
const { user } = useUser();
const navigate = useNavigate();
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
useAdminGetOrganizations({
limit: 20,
searchTerm: debouncedSearchTerm
});
const isEmpty = !isPending && !data?.pages?.[0].length;
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, newPerPage);
};
const { data, isPending } = useAdminGetOrganizations({
limit,
offset,
searchTerm: debouncedSearchTerm
});
const { organizations, totalCount = 0 } = data ?? {};
const isEmpty = !isPending && !totalCount;
useResetPageHelper({
totalCount,
offset,
setPage
});
const { mutateAsync: accessOrganization } = useServerAdminAccessOrg();
@@ -499,125 +522,120 @@ const OrganizationsPanelTable = ({
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="organizations" />}
{!isPending &&
data?.pages?.map((orgs) =>
orgs.map((org) => {
const isMember = org.members.find((member) => member.user.id === user.id);
organizations?.map((org) => {
const isMember = org.members.find((member) => member.user.id === user.id);
return (
<Tr key={`org-${org.id}`} className="w-full">
<Td className="w-1/2 max-w-0">
<div className="flex items-center gap-x-1.5">
{org.name ? (
<p className="truncate">{org.name}</p>
) : (
<span className="text-mineshaft-400">Not Set</span>
)}
</div>
</Td>
<Td className="w-1/3">
<button
type="button"
onClick={() => handlePopUpOpen("viewMembers", { organization: org })}
className="flex items-center hover:underline"
>
<Tooltip className="text-center" content="View Members">
<FontAwesomeIcon
icon={faEye}
className="mr-1.5 text-mineshaft-300"
size="sm"
/>
return (
<Tr key={`org-${org.id}`} className="w-full">
<Td className="w-1/2 max-w-0">
<div className="flex items-center gap-x-1.5">
{org.name ? (
<p className="truncate">{org.name}</p>
) : (
<span className="text-mineshaft-400">Not Set</span>
)}
</div>
</Td>
<Td className="w-1/3">
<button
type="button"
onClick={() => handlePopUpOpen("viewMembers", { organization: org })}
className="flex items-center hover:underline"
>
<Tooltip className="text-center" content="View Members">
<FontAwesomeIcon
icon={faEye}
className="mr-1.5 text-mineshaft-300"
size="sm"
/>
</Tooltip>
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
{!org.members.some(
(member) =>
member.role === OrgMembershipRole.Admin &&
member.status === OrgMembershipStatus.Accepted
) && (
<Tooltip content="No admins have accepted their invitations.">
<div className="ml-1.5">
<FontAwesomeIcon className="text-yellow" icon={faWarning} />
</div>
</Tooltip>
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
{!org.members.some(
(member) =>
member.role === OrgMembershipRole.Admin &&
member.status === OrgMembershipStatus.Accepted
) && (
<Tooltip content="No admins have accepted their invitations.">
<div className="ml-1.5">
<FontAwesomeIcon className="text-yellow" icon={faWarning} />
</div>
</Tooltip>
)}
</button>
</Td>
<Td className="w-1/3">
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
</Td>
<Td>
<div className="flex justify-end gap-x-1">
{isMember && (
<Tooltip
className="text-center"
content="You are a member of this organization"
)}
</button>
</Td>
<Td className="w-1/3">
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
</Td>
<Td>
<div className="flex justify-end gap-x-1">
{isMember && (
<Tooltip
className="text-center"
content="You are a member of this organization"
>
<div>
<FontAwesomeIcon
className="text-mineshaft-400"
icon={faUserCheck}
size="sm"
/>
</div>
</Tooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<div>
<FontAwesomeIcon
className="text-mineshaft-400"
icon={faUserCheck}
size="sm"
/>
</div>
</Tooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
{!isMember && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleAccessOrg(org.id);
}}
icon={<FontAwesomeIcon icon={faUserPlus} />}
>
Join Organization
</DropdownMenuItem>
)}
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
{!isMember && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteOrganization", {
orgId: org.id,
orgName: org.name
});
handleAccessOrg(org.id);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
icon={<FontAwesomeIcon icon={faUserPlus} />}
>
Delete Organization
Join Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})
)}
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteOrganization", {
orgId: org.id,
orgName: org.name
});
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No organizations found" icon={faBuilding} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of list"}
</Button>
{!isPending && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
<ViewMembersModal
@@ -696,7 +714,7 @@ export const OrganizationsTable = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-mineshaft-100">Organizations</p>
<p className="text-xl font-medium text-mineshaft-100">Organizations</p>
<p className="text-sm text-bunker-300">
Manage, join and view organizations across your instance.
</p>
@@ -13,7 +13,6 @@ import {
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { InfiniteData } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -31,6 +30,7 @@ import {
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
@@ -42,7 +42,12 @@ import {
Tr
} from "@app/components/v2";
import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import {
useAdminBulkDeleteUsers,
useAdminDeleteUser,
@@ -58,17 +63,19 @@ const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions f
const UserPanelTable = ({
handlePopUpOpen,
users: usersPages,
users,
isPending,
adminsOnly,
searchUserFilter,
setSearchUserFilter,
setAdminsOnly,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
selectedUsers,
setSelectedUsers
setSelectedUsers,
totalCount,
page,
perPage,
setPage,
handlePerPageChange
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
@@ -81,22 +88,22 @@ const UserPanelTable = ({
}
) => void;
isPending: boolean;
users: InfiniteData<User[], unknown> | undefined;
users: User[] | undefined;
adminsOnly: boolean;
setAdminsOnly: (adminsOnly: boolean) => void;
searchUserFilter: string;
setSearchUserFilter: (filter: string) => void;
selectedUsers: User[];
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
hasNextPage: boolean;
totalCount: number;
page: number;
perPage: number;
setPage: Dispatch<SetStateAction<number>>;
handlePerPageChange: (newPerPage: number) => void;
}) => {
const { subscription } = useSubscription();
const users = usersPages?.pages.flat();
const isEmpty = !isPending && !users?.length;
const isEmpty = !isPending && totalCount === 0;
const isTableFiltered = Boolean(adminsOnly);
const selectedUserIds = selectedUsers.map((user) => user.id);
@@ -268,7 +275,7 @@ const UserPanelTable = ({
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -bottom-[0.01rem] -right-1"
className="absolute -right-1 -bottom-[0.01rem]"
size="2xs"
icon={faXmark}
/>
@@ -302,16 +309,13 @@ const UserPanelTable = ({
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</>
@@ -341,18 +345,24 @@ export const UserIdentitiesTable = () => {
const [adminsOnly, setAdminsOnly] = useState(false);
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const {
data: users,
isPending,
isFetchingNextPage,
hasNextPage,
fetchNextPage
} = useAdminGetUsers({
limit: 20,
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
initPerPage: getUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, 20)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, newPerPage);
};
const { data, isPending } = useAdminGetUsers({
limit,
offset,
searchTerm: debouncedSearchTerm,
adminsOnly
});
const { users, totalCount = 0 } = data ?? {};
const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string };
@@ -433,7 +443,7 @@ export const UserIdentitiesTable = () => {
<>
<div
className={twMerge(
"h-0 flex-shrink-0 overflow-hidden transition-all",
"h-0 shrink-0 overflow-hidden transition-all",
selectedUsers.length > 0 && "h-16"
)}
>
@@ -465,7 +475,7 @@ export const UserIdentitiesTable = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-mineshaft-100">User Identities</p>
<p className="text-xl font-medium text-mineshaft-100">User Identities</p>
<p className="text-sm text-bunker-300">Manage user identities across your instance.</p>
</div>
</div>
@@ -479,9 +489,11 @@ export const UserIdentitiesTable = () => {
isPending={isPending}
adminsOnly={adminsOnly}
setAdminsOnly={setAdminsOnly}
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
page={page}
perPage={perPage}
setPage={setPage}
handlePerPageChange={handlePerPageChange}
totalCount={totalCount}
/>
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}
@@ -530,7 +542,7 @@ export const UserIdentitiesTable = () => {
<div className="mt-4 text-sm text-mineshaft-400">
The following users will be deleted:
</div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<div className="mt-2 max-h-80 overflow-y-auto rounded-sm border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc">
{selectedUsers?.map((user) => {
const email = user.email ?? user.username;
@@ -552,7 +564,7 @@ export const UserIdentitiesTable = () => {
<div className="inline-block">
<Badge
variant="primary"
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
@@ -74,7 +74,7 @@ export const SignUpPage = () => {
if (config?.initialized) return <ContentLoader text="Redirecting to admin page..." />;
return (
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-linear-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
<Helmet>
<title>{t("common.head-title", { title: t("signup.title") })}</title>
<link rel="icon" href="/infisical.ico" />