mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 15:41:27 +00:00
Merge pull request #2225 from akhilmhdh/feat/org-project-management
Feat/org project management
This commit is contained in:
@@ -50,7 +50,7 @@ export const Pagination = ({
|
||||
>
|
||||
<div className="mr-6 flex items-center space-x-2">
|
||||
<div className="text-xs">
|
||||
{(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count}
|
||||
{(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -20,7 +20,12 @@ export enum OrgPermissionSubjects {
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms"
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@@ -37,6 +42,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||
|
||||
@@ -56,7 +56,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_CERT]: "Get certificate",
|
||||
[EventType.DELETE_CERT]: "Delete certificate",
|
||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||
[EventType.GET_CERT_BODY]: "Get certificate body"
|
||||
[EventType.GET_CERT_BODY]: "Get certificate body",
|
||||
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -70,5 +70,6 @@ export enum EventType {
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
GET_CERT_BODY = "get-cert-body"
|
||||
GET_CERT_BODY = "get-cert-body",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||
}
|
||||
|
||||
@@ -579,6 +579,16 @@ interface GetCertBody {
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgAdminAccessProjectEvent {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||
metadata: {
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
projectId: string;
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -635,7 +645,8 @@ export type Event =
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody;
|
||||
| GetCertBody
|
||||
| OrgAdminAccessProjectEvent;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
||||
@@ -19,6 +19,7 @@ export * from "./keys";
|
||||
export * from "./kms";
|
||||
export * from "./ldapConfig";
|
||||
export * from "./oidcConfig";
|
||||
export * from "./orgAdmin";
|
||||
export * from "./organization";
|
||||
export * from "./projectUserAdditionalPrivilege";
|
||||
export * from "./rateLimit";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useOrgAdminAccessProject } from "./mutation";
|
||||
export { useOrgAdminGetProjects } from "./queries";
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TOrgAdminAccessProjectDTO } from "./types";
|
||||
|
||||
export const useOrgAdminAccessProject = () =>
|
||||
useMutation({
|
||||
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/organization-admin/projects/${projectId}/grant-admin-access`
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { Workspace } from "../types";
|
||||
import { TOrgAdminGetProjectsDTO } from "./types";
|
||||
|
||||
export const orgAdminQueryKeys = {
|
||||
getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const
|
||||
};
|
||||
|
||||
export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => {
|
||||
return useQuery({
|
||||
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
|
||||
"/api/v1/organization-admin/projects",
|
||||
{
|
||||
params: {
|
||||
limit,
|
||||
offset,
|
||||
search
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export type TOrgAdminGetProjectsDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export type TOrgAdminAccessProjectDTO = {
|
||||
projectId: string;
|
||||
};
|
||||
@@ -317,6 +317,7 @@ export const useDeleteWorkspace = () => {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(["org-admin-projects"]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export type Workspace = {
|
||||
pitVersionLimit: number;
|
||||
auditLogsRetentionDays: number;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type WorkspaceEnv = {
|
||||
|
||||
@@ -13,7 +13,7 @@ interface UsePopUpProps {
|
||||
export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
|
||||
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
|
||||
isOpen: boolean;
|
||||
data?: unknown;
|
||||
data?: any;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -476,10 +476,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
{user?.superAdmin && (
|
||||
<Link href="/admin" legacyBehavior>
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Admin Panel
|
||||
Server Admin Panel
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Organization Admin Console
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<button type="button" onClick={logOutUser} className="w-full">
|
||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { OrgAdminPage } from "@app/views/OrgAdminPage";
|
||||
|
||||
export default function SettingsOrg() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<OrgAdminPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsOrg.requireAuth = true;
|
||||
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();
|
||||
|
||||
const adminConsolePermissionSchmea = z
|
||||
.object({
|
||||
"access-all-projects": z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const formSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
@@ -23,7 +29,6 @@ export const formSchema = z.object({
|
||||
.object({
|
||||
workspace: z
|
||||
.object({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
@@ -38,7 +43,8 @@ export const formSchema = z.object({
|
||||
scim: generalPermissionSchema,
|
||||
ldap: generalPermissionSchema,
|
||||
billing: generalPermissionSchema,
|
||||
identity: generalPermissionSchema
|
||||
identity: generalPermissionSchema,
|
||||
"organization-admin-console": adminConsolePermissionSchmea
|
||||
})
|
||||
.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: "access-all-projects", label: "Access all organization projects" }
|
||||
] as const;
|
||||
|
||||
export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.organization-admin-console"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
if (rule?.["access-all-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.organization-admin-console",
|
||||
{ "access-all-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>Organization 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.organization-admin-console.${action}`}
|
||||
key={`permissions.organization-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.organization-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.organization-admin-console.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
-1
@@ -61,7 +61,10 @@ const getPermissionList = (option: string) => {
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
title: string;
|
||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
|
||||
formName: keyof Omit<
|
||||
Exclude<TFormSchema["permissions"], undefined>,
|
||||
"workspace" | "organization-admin-console"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
+12
@@ -12,6 +12,8 @@ import {
|
||||
TFormSchema
|
||||
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||
|
||||
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
|
||||
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>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { OrgAdminProjects } from "./components/OrgAdminProjects";
|
||||
|
||||
enum TabSections {
|
||||
Projects = "projects"
|
||||
}
|
||||
|
||||
export const OrgAdminPage = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Projects);
|
||||
return (
|
||||
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
|
||||
<div className="w-full max-w-6xl px-6">
|
||||
<div className="mb-4">
|
||||
<p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Projects}>Projects</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Projects}>
|
||||
<OrgAdminProjects />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Input,
|
||||
Pagination,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionAdminConsoleAction,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
|
||||
|
||||
export const OrgAdminProjects = withPermission(
|
||||
() => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(search);
|
||||
const [perPage, setPerPage] = useState(25);
|
||||
const router = useRouter();
|
||||
const orgAdminAccessProject = useOrgAdminAccessProject();
|
||||
|
||||
const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({
|
||||
offset: (page - 1) * perPage,
|
||||
limit: perPage,
|
||||
search: debouncedSearch || undefined
|
||||
});
|
||||
|
||||
const projects = data?.projects || [];
|
||||
const projectCount = data?.count || 0;
|
||||
const isEmpty = !isProjectsLoading && projects.length === 0;
|
||||
|
||||
const handleAccessProject = async (projectId: string) => {
|
||||
try {
|
||||
await orgAdminAccessProject.mutateAsync({
|
||||
projectId
|
||||
});
|
||||
await router.push({
|
||||
pathname: "/project/[projectId]/secrets/overview",
|
||||
query: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to access project",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-projects"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<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>
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||
subject: OrgPermissionSubjects.AdminConsole
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgAdminProjects } from "./OrgAdminProjects";
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgAdminPage } from "./OrgAdminPage";
|
||||
@@ -317,6 +317,12 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
case EventType.ORG_ADMIN_ACCESS_PROJECT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CA:
|
||||
case EventType.GET_CA:
|
||||
case EventType.UPDATE_CA:
|
||||
|
||||
Reference in New Issue
Block a user