mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 03:25:48 +00:00
feat: brought back workspace permission and made requested changes
This commit is contained in:
@@ -9,6 +9,10 @@ export enum OrgPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
|
GrantAccessProjects = "grant-access-projects"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionSubjects {
|
export enum OrgPermissionSubjects {
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Role = "role",
|
Role = "role",
|
||||||
@@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms"
|
Kms = "kms",
|
||||||
|
AdminConsole = "admin-console"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -39,7 +44,8 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
@@ -107,6 +113,8 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
|
can(OrgPermissionAdminConsoleAction.GrantAccessProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||||
await server.register(registerOrgAdminRouter, { prefix: "/org-admin" });
|
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||||
await server.register(registerUserRouter, { prefix: "/user" });
|
await server.register(registerUserRouter, { prefix: "/user" });
|
||||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/projects/:projectId/access",
|
url: "/projects/:projectId/grant-admin-access",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { membership } = await server.services.orgAdmin.accessProject({
|
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
@@ -42,15 +45,17 @@ export const orgAdminServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TListOrgProjectsDTO) => {
|
}: TListOrgProjectsDTO) => {
|
||||||
const { membership } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const isAdmin = membership.role === OrgMembershipRole.Admin;
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" });
|
OrgPermissionAdminConsoleAction.GrantAccessProjects,
|
||||||
|
OrgPermissionSubjects.AdminConsole
|
||||||
|
);
|
||||||
const projects = await projectDAL.find(
|
const projects = await projectDAL.find(
|
||||||
{
|
{
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
@@ -65,16 +70,24 @@ export const orgAdminServiceFactory = ({
|
|||||||
return { projects, count };
|
return { projects, count };
|
||||||
};
|
};
|
||||||
|
|
||||||
const accessProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TAccessProjectDTO) => {
|
const grantProjectAdminAccess = async ({
|
||||||
const { membership } = await permissionService.getOrgPermission(
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
}: TAccessProjectDTO) => {
|
||||||
|
const { permission, membership } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const isAdmin = membership.role === OrgMembershipRole.Admin;
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" });
|
OrgPermissionAdminConsoleAction.GrantAccessProjects,
|
||||||
|
OrgPermissionSubjects.AdminConsole
|
||||||
|
);
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
@@ -174,5 +187,5 @@ export const orgAdminServiceFactory = ({
|
|||||||
return { isExistingMember: false, membership: updatedMembership };
|
return { isExistingMember: false, membership: updatedMembership };
|
||||||
};
|
};
|
||||||
|
|
||||||
return { listOrgProjects, accessProject };
|
return { listOrgProjects, grantProjectAdminAccess };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ export enum OrgPermissionSubjects {
|
|||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms"
|
Kms = "kms",
|
||||||
|
AdminConsole = "admin-console"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
|
GrantAccessProjects = "grant-access-projects"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -37,6 +42,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { TOrgAdminAccessProjectDTO } from "./types";
|
|||||||
export const useOrgAdminAccessProject = () =>
|
export const useOrgAdminAccessProject = () =>
|
||||||
useMutation({
|
useMutation({
|
||||||
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
|
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
|
||||||
const { data } = await apiRequest.post(`/api/v1/org-admin/projects/${projectId}/access`);
|
const { data } = await apiRequest.post(
|
||||||
|
`/api/v1/organization-admin/projects/${projectId}/grant-admin-access`
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdmin
|
|||||||
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
|
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
|
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
|
||||||
"/api/v1/org-admin/projects",
|
"/api/v1/organization-admin/projects",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import {
|
|||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useOrgPermission,
|
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -132,8 +131,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const { workspaces, currentWorkspace } = useWorkspace();
|
const { workspaces, currentWorkspace } = useWorkspace();
|
||||||
const { orgs, currentOrg } = useOrganization();
|
const { orgs, currentOrg } = useOrganization();
|
||||||
const { membership } = useOrgPermission();
|
|
||||||
const isOrgAdmin = membership?.role === "admin";
|
|
||||||
|
|
||||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||||
@@ -483,6 +480,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
|
||||||
|
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||||
|
Admin Panel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
<button type="button" onClick={logOutUser} className="w-full">
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
@@ -751,18 +753,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{isOrgAdmin && (
|
|
||||||
<Link href={`/org/${currentOrg?.id}/admin`} passHref>
|
|
||||||
<a>
|
|
||||||
<MenuItem
|
|
||||||
isSelected={router.asPath === `/org/${currentOrg?.id}/admin`}
|
|
||||||
icon="system-outline-109-slider-toggle-settings"
|
|
||||||
>
|
|
||||||
Admin Panel
|
|
||||||
</MenuItem>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<Link href={`/org/${currentOrg?.id}/settings`} passHref>
|
<Link href={`/org/${currentOrg?.id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
-133
@@ -1,133 +0,0 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
import { faMoneyBill } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isNonEditable?: boolean;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
NoAccess = "no-access",
|
|
||||||
ReadOnly = "read-only",
|
|
||||||
FullAccess = "full-acess",
|
|
||||||
Custom = "custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
const PERMISSIONS = [
|
|
||||||
{ action: "read", label: "View projects" },
|
|
||||||
{ action: "create", label: "Create new projects" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => {
|
|
||||||
const rule = useWatch({
|
|
||||||
control,
|
|
||||||
name: "permissions.workspace"
|
|
||||||
});
|
|
||||||
const [isCustom, setIsCustom] = useToggle();
|
|
||||||
|
|
||||||
const selectedPermissionCategory = useMemo(() => {
|
|
||||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
|
||||||
const totalActions = PERMISSIONS.length;
|
|
||||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
|
||||||
|
|
||||||
if (isCustom) return Permission.Custom;
|
|
||||||
if (score === 0) return Permission.NoAccess;
|
|
||||||
if (score === totalActions) return Permission.FullAccess;
|
|
||||||
if (score === 1 && rule?.read) return Permission.ReadOnly;
|
|
||||||
|
|
||||||
return Permission.Custom;
|
|
||||||
}, [rule, isCustom]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
|
||||||
else setIsCustom.off();
|
|
||||||
}, [selectedPermissionCategory]);
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (val === Permission.Custom) setIsCustom.on();
|
|
||||||
else setIsCustom.off();
|
|
||||||
|
|
||||||
switch (val) {
|
|
||||||
case Permission.NoAccess:
|
|
||||||
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
case Permission.FullAccess:
|
|
||||||
setValue("permissions.workspace", { read: true, create: true }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
case Permission.ReadOnly:
|
|
||||||
setValue("permissions.workspace", { read: true, create: false }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
|
||||||
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div>
|
|
||||||
<FontAwesomeIcon icon={faMoneyBill} className="text-4xl" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-grow flex-col">
|
|
||||||
<div className="mb-1 text-lg font-medium">Project</div>
|
|
||||||
<div className="text-xs font-light">
|
|
||||||
View and create new projects in this organization
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
defaultValue={Permission.NoAccess}
|
|
||||||
isDisabled={isNonEditable}
|
|
||||||
value={selectedPermissionCategory}
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<motion.div
|
|
||||||
initial={false}
|
|
||||||
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
|
||||||
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
|
|
||||||
>
|
|
||||||
{isCustom &&
|
|
||||||
PERMISSIONS.map(({ action, label }) => (
|
|
||||||
<Controller
|
|
||||||
name={`permissions.workspace.${action}`}
|
|
||||||
key={`permissions.workspace.${action}`}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
id={`permissions.workspace.${action}`}
|
|
||||||
isDisabled={isNonEditable}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -12,6 +12,12 @@ const generalPermissionSchema = z
|
|||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
const adminConsolePermissionSchmea = z
|
||||||
|
.object({
|
||||||
|
"grant-access-projects": z.boolean().optional()
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
@@ -23,7 +29,6 @@ export const formSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
workspace: z
|
workspace: z
|
||||||
.object({
|
.object({
|
||||||
read: z.boolean().optional(),
|
|
||||||
create: z.boolean().optional()
|
create: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -38,7 +43,8 @@ export const formSchema = z.object({
|
|||||||
scim: generalPermissionSchema,
|
scim: generalPermissionSchema,
|
||||||
ldap: generalPermissionSchema,
|
ldap: generalPermissionSchema,
|
||||||
billing: generalPermissionSchema,
|
billing: generalPermissionSchema,
|
||||||
identity: generalPermissionSchema
|
identity: generalPermissionSchema,
|
||||||
|
"admin-console": adminConsolePermissionSchmea
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isEditable: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSION_ACTIONS = [
|
||||||
|
{ action: "grant-access-projects", label: "Grant access projects" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => {
|
||||||
|
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: "permissions.admin-console"
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
if (rule?.["grant-access-projects"]) {
|
||||||
|
return Permission.Custom;
|
||||||
|
}
|
||||||
|
return Permission.NoAccess;
|
||||||
|
}, [rule, isCustom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
}, [selectedPermissionCategory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||||
|
if (isRowCustom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (!val) return;
|
||||||
|
if (val === Permission.Custom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
setIsCustom.on();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCustom.off();
|
||||||
|
|
||||||
|
if (val === Permission.NoAccess) {
|
||||||
|
setValue(
|
||||||
|
"permissions.admin-console",
|
||||||
|
{ "grant-access-projects": false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
onClick={() => setIsRowExpanded.toggle()}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||||
|
</Td>
|
||||||
|
<Td>Admin Console</Td>
|
||||||
|
<Td>
|
||||||
|
<Select
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
isDisabled={!isEditable}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{isRowExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td
|
||||||
|
colSpan={3}
|
||||||
|
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={`permissions.admin-console.${action}`}
|
||||||
|
key={`permissions.admin-console.${action}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
if (!isEditable) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update default role"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
id={`permissions.admin-console.${action}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isEditable: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSION_ACTIONS = [{ action: "create", label: "Create projects" }] as const;
|
||||||
|
|
||||||
|
export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => {
|
||||||
|
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: "permissions.workspace"
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
if (rule?.create) {
|
||||||
|
return Permission.Custom;
|
||||||
|
}
|
||||||
|
return Permission.NoAccess;
|
||||||
|
}, [rule, isCustom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
}, [selectedPermissionCategory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||||
|
if (isRowCustom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (!val) return;
|
||||||
|
if (val === Permission.Custom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
setIsCustom.on();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCustom.off();
|
||||||
|
|
||||||
|
if (val === Permission.NoAccess) {
|
||||||
|
setValue("permissions.workspace", { create: false }, { shouldDirty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
onClick={() => setIsRowExpanded.toggle()}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||||
|
</Td>
|
||||||
|
<Td>Project</Td>
|
||||||
|
<Td>
|
||||||
|
<Select
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
isDisabled={!isEditable}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{isRowExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td
|
||||||
|
colSpan={3}
|
||||||
|
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={`permissions.workspace.${action}`}
|
||||||
|
key={`permissions.workspace.${action}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
if (!isEditable) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update default role"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
id={`permissions.admin-console.${action}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
-1
@@ -61,7 +61,10 @@ const getPermissionList = (option: string) => {
|
|||||||
type Props = {
|
type Props = {
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
|
formName: keyof Omit<
|
||||||
|
Exclude<TFormSchema["permissions"], undefined>,
|
||||||
|
"workspace" | "admin-console"
|
||||||
|
>;
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
control: Control<TFormSchema>;
|
control: Control<TFormSchema>;
|
||||||
};
|
};
|
||||||
|
|||||||
+12
@@ -12,6 +12,8 @@ import {
|
|||||||
TFormSchema
|
TFormSchema
|
||||||
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||||
|
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||||
import { RolePermissionRow } from "./RolePermissionRow";
|
import { RolePermissionRow } from "./RolePermissionRow";
|
||||||
|
|
||||||
const SIMPLE_PERMISSION_OPTIONS = [
|
const SIMPLE_PERMISSION_OPTIONS = [
|
||||||
@@ -153,6 +155,16 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<OrgRoleWorkspaceRow
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
isEditable={isCustomRole}
|
||||||
|
/>
|
||||||
|
<OrgPermissionAdminConsoleRow
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
isEditable={isCustomRole}
|
||||||
|
/>
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
+133
-121
@@ -23,133 +23,145 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr} from "@app/components/v2";
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionAdminConsoleAction,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
import { useDebounce } from "@app/hooks";
|
import { useDebounce } from "@app/hooks";
|
||||||
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
|
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
|
||||||
|
|
||||||
export const OrgAdminProjects = () => {
|
export const OrgAdminProjects = withPermission(
|
||||||
const [page, setPage] = useState(1);
|
() => {
|
||||||
const [search, setSearch] = useState("");
|
const [page, setPage] = useState(1);
|
||||||
const debouncedSearch = useDebounce(search);
|
const [search, setSearch] = useState("");
|
||||||
const [perPage, setPerPage] = useState(25);
|
const debouncedSearch = useDebounce(search);
|
||||||
const router = useRouter();
|
const [perPage, setPerPage] = useState(25);
|
||||||
const orgAdminAccessProject = useOrgAdminAccessProject();
|
const router = useRouter();
|
||||||
|
const orgAdminAccessProject = useOrgAdminAccessProject();
|
||||||
|
|
||||||
const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({
|
const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({
|
||||||
offset: (page - 1) * perPage,
|
offset: (page - 1) * perPage,
|
||||||
limit: perPage,
|
limit: perPage,
|
||||||
search: debouncedSearch || undefined
|
search: debouncedSearch || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const projects = data?.projects || [];
|
const projects = data?.projects || [];
|
||||||
const projectCount = data?.count || 0;
|
const projectCount = data?.count || 0;
|
||||||
const isEmpty = !isProjectsLoading && projects.length === 0;
|
const isEmpty = !isProjectsLoading && projects.length === 0;
|
||||||
|
|
||||||
const handleAccessProject = async (projectId: string) => {
|
const handleAccessProject = async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
await orgAdminAccessProject.mutateAsync({
|
await orgAdminAccessProject.mutateAsync({
|
||||||
projectId
|
|
||||||
});
|
|
||||||
await router.push({
|
|
||||||
pathname: "/project/[projectId]/secrets/overview",
|
|
||||||
query: {
|
|
||||||
projectId
|
projectId
|
||||||
}
|
});
|
||||||
});
|
await router.push({
|
||||||
} catch {
|
pathname: "/project/[projectId]/secrets/overview",
|
||||||
createNotification({
|
query: {
|
||||||
text: "Failed to access project",
|
projectId
|
||||||
type: "error"
|
}
|
||||||
});
|
});
|
||||||
}
|
} catch {
|
||||||
};
|
createNotification({
|
||||||
|
text: "Failed to access project",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="panel-projects"
|
key="panel-projects"
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Projects</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Projects</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search by project name"
|
||||||
|
/>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Slug</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
|
||||||
|
{!isProjectsLoading &&
|
||||||
|
projects?.map(({ name, slug, createdAt, id }) => (
|
||||||
|
<Tr key={`project-${id}`} className="group w-full">
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{slug}</Td>
|
||||||
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")}</Td>
|
||||||
|
<Td>
|
||||||
|
<div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="text-bunker-300 hover:text-primary-400 data-[state=open]:text-primary-400"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
handleAccessProject(id);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faSignIn} />}
|
||||||
|
disabled={
|
||||||
|
orgAdminAccessProject.variables?.projectId === id &&
|
||||||
|
orgAdminAccessProject.isLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Access{" "}
|
||||||
|
{orgAdminAccessProject.variables?.projectId === id &&
|
||||||
|
orgAdminAccessProject.isLoading && <Spinner size="xs" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isProjectsLoading && (
|
||||||
|
<Pagination
|
||||||
|
count={projectCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isEmpty && <EmptyState title="No projects found" />}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</motion.div>
|
||||||
<Input
|
);
|
||||||
value={search}
|
},
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
{
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
action: OrgPermissionAdminConsoleAction.GrantAccessProjects,
|
||||||
placeholder="Search by project name"
|
subject: OrgPermissionSubjects.AdminConsole
|
||||||
/>
|
}
|
||||||
<TableContainer className="mt-4">
|
);
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>Slug</Th>
|
|
||||||
<Th>Created At</Th>
|
|
||||||
<Th className="w-5" />
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
|
|
||||||
{!isProjectsLoading &&
|
|
||||||
projects?.map(({ name, slug, createdAt, id }) => (
|
|
||||||
<Tr key={`project-${id}`} className="group w-full">
|
|
||||||
<Td>{name}</Td>
|
|
||||||
<Td>{slug}</Td>
|
|
||||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")}</Td>
|
|
||||||
<Td>
|
|
||||||
<div>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="text-bunker-300 hover:text-primary-400 data-[state=open]:text-primary-400"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
handleAccessProject(id);
|
|
||||||
}}
|
|
||||||
icon={<FontAwesomeIcon icon={faSignIn} />}
|
|
||||||
disabled={
|
|
||||||
orgAdminAccessProject.variables?.projectId === id &&
|
|
||||||
orgAdminAccessProject.isLoading
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Access{" "}
|
|
||||||
{orgAdminAccessProject.variables?.projectId === id &&
|
|
||||||
orgAdminAccessProject.isLoading && <Spinner size="xs" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!isProjectsLoading && (
|
|
||||||
<Pagination
|
|
||||||
count={projectCount}
|
|
||||||
page={page}
|
|
||||||
perPage={perPage}
|
|
||||||
onChangePage={(newPage) => setPage(newPage)}
|
|
||||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isEmpty && <EmptyState title="No projects found" />}
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user