mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(pam): PAM Platform V1
This commit is contained in:
@@ -80,6 +80,10 @@ const PROJECT_TYPE_MENU_ITEMS = [
|
||||
{
|
||||
label: "Secret Scanning",
|
||||
value: ProjectType.SecretScanning
|
||||
},
|
||||
{
|
||||
label: "PAM",
|
||||
value: ProjectType.PAM
|
||||
}
|
||||
];
|
||||
|
||||
@@ -193,12 +197,12 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<div className="mt-2 grid grid-cols-5 gap-4">
|
||||
<div className="mt-2 grid grid-cols-3 gap-3">
|
||||
{PROJECT_TYPE_MENU_ITEMS.map((el) => (
|
||||
<div
|
||||
key={el.value}
|
||||
className={twMerge(
|
||||
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
|
||||
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 px-2 py-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
|
||||
field.value === el.value && "border-primary-400 bg-mineshaft-600 opacity-100"
|
||||
)}
|
||||
onClick={() => field.onChange(el.value)}
|
||||
|
||||
@@ -8,9 +8,24 @@ export const HighlightText = ({
|
||||
highlightClassName?: string;
|
||||
}) => {
|
||||
if (!text) return null;
|
||||
|
||||
const renderTextWithNewlines = (input: string, baseKeyPrefix: string = ""): React.ReactNode[] => {
|
||||
if (!input) return [];
|
||||
const lines = input.split("\n");
|
||||
return lines.flatMap((line, index) => {
|
||||
const nodes: React.ReactNode[] = [line];
|
||||
if (index < lines.length - 1) {
|
||||
nodes.push(<br key={`${baseKeyPrefix}-br-${line}`} />);
|
||||
}
|
||||
return nodes;
|
||||
});
|
||||
};
|
||||
|
||||
const searchTerm = highlight.toLowerCase().trim();
|
||||
|
||||
if (!searchTerm) return <span>{text}</span>;
|
||||
if (!searchTerm) {
|
||||
return <span>{renderTextWithNewlines(text, "full-text")}</span>;
|
||||
}
|
||||
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
@@ -20,12 +35,17 @@ export const HighlightText = ({
|
||||
|
||||
text.replace(regex, (match: string, offset: number) => {
|
||||
if (offset > lastIndex) {
|
||||
parts.push(<span key={`pre-${lastIndex}`}>{text.substring(lastIndex, offset)}</span>);
|
||||
const preMatchText = text.substring(lastIndex, offset);
|
||||
parts.push(
|
||||
<span key={`pre-${lastIndex}`}>
|
||||
{renderTextWithNewlines(preMatchText, `pre-${lastIndex}`)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
parts.push(
|
||||
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
|
||||
{match}
|
||||
{renderTextWithNewlines(match, `match-${offset}`)}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -35,7 +55,12 @@ export const HighlightText = ({
|
||||
});
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(<span key={`post-${lastIndex}`}>{text.substring(lastIndex)}</span>);
|
||||
const postMatchText = text.substring(lastIndex);
|
||||
parts.push(
|
||||
<span key={`post-${lastIndex}`}>
|
||||
{renderTextWithNewlines(postMatchText, `post-${lastIndex}`)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return parts;
|
||||
|
||||
@@ -350,6 +350,24 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings"
|
||||
)
|
||||
},
|
||||
Pam: {
|
||||
AccountsPage: setRoute(
|
||||
"/projects/pam/$projectId/accounts",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts"
|
||||
),
|
||||
ResourcesPage: setRoute(
|
||||
"/projects/pam/$projectId/resources",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources"
|
||||
),
|
||||
SessionsPage: setRoute(
|
||||
"/projects/pam/$projectId/sessions",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/"
|
||||
),
|
||||
PamSessionByIDPage: setRoute(
|
||||
"/projects/pam/$projectId/sessions/$sessionId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
|
||||
)
|
||||
},
|
||||
Public: {
|
||||
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
|
||||
ViewSecretRequestByIDPage: setRoute(
|
||||
|
||||
@@ -187,6 +187,19 @@ export enum ProjectPermissionCommitsActions {
|
||||
PerformRollback = "perform-rollback"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionPamAccountActions {
|
||||
Access = "access",
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionPamSessionActions {
|
||||
Read = "read"
|
||||
// Terminate = "terminate"
|
||||
}
|
||||
|
||||
export type IdentityManagementSubjectFields = {
|
||||
identityId: string;
|
||||
};
|
||||
@@ -208,7 +221,8 @@ export type ConditionalProjectPermissionSubject =
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| ProjectPermissionSub.SecretEvents
|
||||
| ProjectPermissionSub.AppConnections;
|
||||
| ProjectPermissionSub.AppConnections
|
||||
| ProjectPermissionSub.PamAccounts;
|
||||
|
||||
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
|
||||
[PermissionConditionOperators.$EQ]: "equal to",
|
||||
@@ -289,7 +303,11 @@ export enum ProjectPermissionSub {
|
||||
SecretScanningFindings = "secret-scanning-findings",
|
||||
SecretScanningConfigs = "secret-scanning-configs",
|
||||
SecretEvents = "secret-events",
|
||||
AppConnections = "app-connections"
|
||||
AppConnections = "app-connections",
|
||||
PamFolders = "pam-folders",
|
||||
PamResources = "pam-resources",
|
||||
PamAccounts = "pam-accounts",
|
||||
PamSessions = "pam-sessions"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -350,6 +368,12 @@ export type PkiTemplateSubjectFields = {
|
||||
// (dangtony98): consider adding [commonName] as a subject field in the future
|
||||
};
|
||||
|
||||
export type PamAccountSubjectFields = {
|
||||
resourceName: string;
|
||||
accountName: string;
|
||||
accountPath: string;
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionSecretActions,
|
||||
@@ -475,6 +499,16 @@ export type ProjectPermissionSet =
|
||||
| ProjectPermissionSub.AppConnections
|
||||
| (ForcedSubject<ProjectPermissionSub.AppConnections> & AppConnectionSubjectFields)
|
||||
)
|
||||
];
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PamFolders]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PamResources]
|
||||
| [
|
||||
ProjectPermissionPamAccountActions,
|
||||
(
|
||||
| ProjectPermissionSub.PamAccounts
|
||||
| (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
||||
@@ -82,6 +82,8 @@ export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]
|
||||
return "/projects/cert-management/$projectId/subscribers" as const;
|
||||
case ProjectType.SecretScanning:
|
||||
return `/projects/${type}/$projectId/data-sources` as const;
|
||||
case ProjectType.PAM:
|
||||
return `/projects/${type}/$projectId/accounts` as const;
|
||||
default:
|
||||
return `/projects/${type}/$projectId/overview` as const;
|
||||
}
|
||||
@@ -93,7 +95,8 @@ export const getProjectTitle = (type: ProjectType) => {
|
||||
[ProjectType.KMS]: "Key Management",
|
||||
[ProjectType.CertificateManager]: "Cert Management",
|
||||
[ProjectType.SSH]: "SSH",
|
||||
[ProjectType.SecretScanning]: "Secret Scanning"
|
||||
[ProjectType.SecretScanning]: "Secret Scanning",
|
||||
[ProjectType.PAM]: "PAM"
|
||||
};
|
||||
return titleConvert[type];
|
||||
};
|
||||
@@ -104,7 +107,8 @@ export const getProjectLottieIcon = (type: ProjectType) => {
|
||||
[ProjectType.KMS]: "unlock",
|
||||
[ProjectType.CertificateManager]: "note",
|
||||
[ProjectType.SSH]: "terminal",
|
||||
[ProjectType.SecretScanning]: "secret-scan"
|
||||
[ProjectType.SecretScanning]: "secret-scan",
|
||||
[ProjectType.PAM]: "groups"
|
||||
};
|
||||
return titleConvert[type];
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ProjectType } from "../projects/types";
|
||||
import { EventType, UserAgentType } from "./enums";
|
||||
|
||||
export const secretEvents: EventType[] = [
|
||||
@@ -246,7 +247,26 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
|
||||
[EventType.CREATE_ORG_ROLE]: "Create Org Role",
|
||||
[EventType.UPDATE_ORG_ROLE]: "Update Org Role",
|
||||
[EventType.DELETE_ORG_ROLE]: "Delete Org Role"
|
||||
[EventType.DELETE_ORG_ROLE]: "Delete Org Role",
|
||||
|
||||
[EventType.PAM_SESSION_START]: "PAM Session Start",
|
||||
[EventType.PAM_SESSION_LOGS_UPDATE]: "PAM Session Logs Update",
|
||||
[EventType.PAM_SESSION_END]: "PAM Session End",
|
||||
[EventType.PAM_SESSION_GET]: "PAM Session Get",
|
||||
[EventType.PAM_SESSION_LIST]: "PAM Session List",
|
||||
[EventType.PAM_FOLDER_CREATE]: "PAM Folder Create",
|
||||
[EventType.PAM_FOLDER_UPDATE]: "PAM Folder Update",
|
||||
[EventType.PAM_FOLDER_DELETE]: "PAM Folder Delete",
|
||||
[EventType.PAM_ACCOUNT_LIST]: "PAM Account List",
|
||||
[EventType.PAM_ACCOUNT_ACCESS]: "PAM Account Access",
|
||||
[EventType.PAM_ACCOUNT_CREATE]: "PAM Account Create",
|
||||
[EventType.PAM_ACCOUNT_UPDATE]: "PAM Account Update",
|
||||
[EventType.PAM_ACCOUNT_DELETE]: "PAM Account Delete",
|
||||
[EventType.PAM_RESOURCE_LIST]: "PAM Resource List",
|
||||
[EventType.PAM_RESOURCE_GET]: "PAM Resource Get",
|
||||
[EventType.PAM_RESOURCE_CREATE]: "PAM Resource Create",
|
||||
[EventType.PAM_RESOURCE_UPDATE]: "PAM Resource Update",
|
||||
[EventType.PAM_RESOURCE_DELETE]: "PAM Resource Delete"
|
||||
};
|
||||
|
||||
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
||||
@@ -258,3 +278,35 @@ export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
||||
[UserAgentType.PYTHON_SDK]: "InfisicalPythonSDK",
|
||||
[UserAgentType.OTHER]: "Other"
|
||||
};
|
||||
|
||||
const sharedProjectEvents = [
|
||||
EventType.ADD_PROJECT_MEMBER,
|
||||
EventType.REMOVE_PROJECT_MEMBER,
|
||||
EventType.CREATE_PROJECT_ROLE,
|
||||
EventType.UPDATE_PROJECT_ROLE,
|
||||
EventType.DELETE_PROJECT_ROLE
|
||||
];
|
||||
|
||||
export const projectToEventsMap: Partial<Record<ProjectType, EventType[]>> = {
|
||||
[ProjectType.PAM]: [
|
||||
...sharedProjectEvents,
|
||||
EventType.PAM_SESSION_START,
|
||||
EventType.PAM_SESSION_LOGS_UPDATE,
|
||||
EventType.PAM_SESSION_END,
|
||||
EventType.PAM_SESSION_GET,
|
||||
EventType.PAM_SESSION_LIST,
|
||||
EventType.PAM_FOLDER_CREATE,
|
||||
EventType.PAM_FOLDER_UPDATE,
|
||||
EventType.PAM_FOLDER_DELETE,
|
||||
EventType.PAM_ACCOUNT_LIST,
|
||||
EventType.PAM_ACCOUNT_ACCESS,
|
||||
EventType.PAM_ACCOUNT_CREATE,
|
||||
EventType.PAM_ACCOUNT_UPDATE,
|
||||
EventType.PAM_ACCOUNT_DELETE,
|
||||
EventType.PAM_RESOURCE_LIST,
|
||||
EventType.PAM_RESOURCE_GET,
|
||||
EventType.PAM_RESOURCE_CREATE,
|
||||
EventType.PAM_RESOURCE_UPDATE,
|
||||
EventType.PAM_RESOURCE_DELETE
|
||||
]
|
||||
};
|
||||
|
||||
@@ -240,5 +240,24 @@ export enum EventType {
|
||||
|
||||
CREATE_ORG_ROLE = "create-org-role",
|
||||
UPDATE_ORG_ROLE = "update-org-role",
|
||||
DELETE_ORG_ROLE = "delete-org-role"
|
||||
DELETE_ORG_ROLE = "delete-org-role",
|
||||
|
||||
PAM_SESSION_START = "pam-session-start",
|
||||
PAM_SESSION_LOGS_UPDATE = "pam-session-logs-update",
|
||||
PAM_SESSION_END = "pam-session-end",
|
||||
PAM_SESSION_GET = "pam-session-get",
|
||||
PAM_SESSION_LIST = "pam-session-list",
|
||||
PAM_FOLDER_CREATE = "pam-folder-create",
|
||||
PAM_FOLDER_UPDATE = "pam-folder-update",
|
||||
PAM_FOLDER_DELETE = "pam-folder-delete",
|
||||
PAM_ACCOUNT_LIST = "pam-account-list",
|
||||
PAM_ACCOUNT_ACCESS = "pam-account-access",
|
||||
PAM_ACCOUNT_CREATE = "pam-account-create",
|
||||
PAM_ACCOUNT_UPDATE = "pam-account-update",
|
||||
PAM_ACCOUNT_DELETE = "pam-account-delete",
|
||||
PAM_RESOURCE_LIST = "pam-resource-list",
|
||||
PAM_RESOURCE_GET = "pam-resource-get",
|
||||
PAM_RESOURCE_CREATE = "pam-resource-create",
|
||||
PAM_RESOURCE_UPDATE = "pam-resource-update",
|
||||
PAM_RESOURCE_DELETE = "pam-resource-delete"
|
||||
}
|
||||
|
||||
10
frontend/src/hooks/api/pam/enums.ts
Normal file
10
frontend/src/hooks/api/pam/enums.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum PamResourceType {
|
||||
Postgres = "postgres"
|
||||
}
|
||||
|
||||
export enum PamSessionStatus {
|
||||
Starting = "starting",
|
||||
Active = "active",
|
||||
Ended = "ended",
|
||||
Terminated = "terminated"
|
||||
}
|
||||
5
frontend/src/hooks/api/pam/index.ts
Normal file
5
frontend/src/hooks/api/pam/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./enums";
|
||||
export * from "./maps";
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
8
frontend/src/hooks/api/pam/maps.ts
Normal file
8
frontend/src/hooks/api/pam/maps.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { PamResourceType } from "./enums";
|
||||
|
||||
export const PAM_RESOURCE_TYPE_MAP: Record<
|
||||
PamResourceType,
|
||||
{ name: string; image: string; size?: number }
|
||||
> = {
|
||||
[PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" }
|
||||
};
|
||||
174
frontend/src/hooks/api/pam/mutations.tsx
Normal file
174
frontend/src/hooks/api/pam/mutations.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { pamKeys } from "./queries";
|
||||
import {
|
||||
TCreatePamAccountDTO,
|
||||
TCreatePamFolderDTO,
|
||||
TCreatePamResourceDTO,
|
||||
TDeletePamAccountDTO,
|
||||
TDeletePamFolderDTO,
|
||||
TDeletePamResourceDTO,
|
||||
TPamAccount,
|
||||
TPamFolder,
|
||||
TPamResource,
|
||||
TUpdatePamAccountDTO,
|
||||
TUpdatePamFolderDTO,
|
||||
TUpdatePamResourceDTO
|
||||
} from "./types";
|
||||
|
||||
// Resources
|
||||
export const useCreatePamResource = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ resourceType, ...params }: TCreatePamResourceDTO) => {
|
||||
const { data } = await apiRequest.post<{ resource: TPamResource }>(
|
||||
`/api/v1/pam/resources/${resourceType}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.resource;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePamResource = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ resourceId, resourceType, ...params }: TUpdatePamResourceDTO) => {
|
||||
const { data } = await apiRequest.patch<{ resource: TPamResource }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.resource;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeletePamResource = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ resourceId, resourceType }: TDeletePamResourceDTO) => {
|
||||
const { data } = await apiRequest.delete<{ resource: TPamResource }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}`
|
||||
);
|
||||
|
||||
return data.resource;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Accounts
|
||||
export const useCreatePamAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ resourceId, resourceType, ...params }: TCreatePamAccountDTO) => {
|
||||
const { data } = await apiRequest.post<{ account: TPamAccount }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.account;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePamAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
resourceId,
|
||||
resourceType,
|
||||
accountId,
|
||||
...params
|
||||
}: TUpdatePamAccountDTO) => {
|
||||
const { data } = await apiRequest.patch<{ account: TPamAccount }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.account;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeletePamAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ resourceId, resourceType, accountId }: TDeletePamAccountDTO) => {
|
||||
const { data } = await apiRequest.delete<{ account: TPamAccount }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`
|
||||
);
|
||||
|
||||
return data.account;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Folders
|
||||
export const useCreatePamFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (params: TCreatePamFolderDTO) => {
|
||||
const { data } = await apiRequest.post<{ folder: TPamFolder }>("/api/v1/pam/folders", params);
|
||||
|
||||
return data.folder;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePamFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ folderId, ...params }: TUpdatePamFolderDTO) => {
|
||||
const { data } = await apiRequest.patch<{ folder: TPamFolder }>(
|
||||
`/api/v1/pam/folders/${folderId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.folder;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeletePamFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ folderId }: TDeletePamFolderDTO) => {
|
||||
const { data } = await apiRequest.delete<{ folder: TPamFolder }>(
|
||||
`/api/v1/pam/folders/${folderId}`
|
||||
);
|
||||
|
||||
return data.folder;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
138
frontend/src/hooks/api/pam/queries.tsx
Normal file
138
frontend/src/hooks/api/pam/queries.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TPamResourceOption } from "./types/resource-options";
|
||||
import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types";
|
||||
|
||||
export const pamKeys = {
|
||||
all: ["pam"] as const,
|
||||
resource: () => [...pamKeys.all, "resource"] as const,
|
||||
account: () => [...pamKeys.all, "account"] as const,
|
||||
session: () => [...pamKeys.all, "session"] as const,
|
||||
listResourceOptions: () => [...pamKeys.resource(), "options"] as const,
|
||||
listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId],
|
||||
listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId],
|
||||
getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId],
|
||||
listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId]
|
||||
};
|
||||
|
||||
// Resources
|
||||
export const useListPamResourceOptions = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TPamResourceOption[],
|
||||
unknown,
|
||||
TPamResourceOption[],
|
||||
ReturnType<typeof pamKeys.listResourceOptions>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pamKeys.listResourceOptions(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ resourceOptions: TPamResourceOption[] }>(
|
||||
"/api/v1/pam/resources/options"
|
||||
);
|
||||
|
||||
return data.resourceOptions;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useListPamResources = (
|
||||
projectId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TPamResource[],
|
||||
unknown,
|
||||
TPamResource[],
|
||||
ReturnType<typeof pamKeys.listResources>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pamKeys.listResources(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ resources: TPamResource[] }>(
|
||||
"/api/v1/pam/resources",
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data.resources;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
// Accounts
|
||||
export const useListPamAccounts = (
|
||||
projectId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
{ accounts: TPamAccount[]; folders: TPamFolder[] },
|
||||
unknown,
|
||||
{ accounts: TPamAccount[]; folders: TPamFolder[] },
|
||||
ReturnType<typeof pamKeys.listAccounts>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pamKeys.listAccounts(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ accounts: TPamAccount[]; folders: TPamFolder[] }>(
|
||||
"/api/v1/pam/accounts",
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
// Sessions
|
||||
export const useGetPamSessionById = (
|
||||
sessionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<TPamSession, unknown, TPamSession, ReturnType<typeof pamKeys.getSession>>,
|
||||
"queryKey" | "queryFn" | "enabled"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pamKeys.getSession(sessionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ session: TPamSession }>(
|
||||
`/api/v1/pam/sessions/${sessionId}`
|
||||
);
|
||||
|
||||
return data.session;
|
||||
},
|
||||
enabled: !!sessionId,
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useListPamSessions = (
|
||||
projectId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<TPamSession[], unknown, TPamSession[], ReturnType<typeof pamKeys.listSessions>>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pamKeys.listSessions(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ sessions: TPamSession[] }>("/api/v1/pam/sessions", {
|
||||
params: { projectId }
|
||||
});
|
||||
|
||||
return data.sessions;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
17
frontend/src/hooks/api/pam/types/base-account.ts
Normal file
17
frontend/src/hooks/api/pam/types/base-account.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PamResourceType } from "../enums";
|
||||
|
||||
export interface TBasePamAccount {
|
||||
id: string;
|
||||
projectId: string;
|
||||
folderId?: string | null;
|
||||
resourceId: string;
|
||||
resource: {
|
||||
id: string;
|
||||
name: string;
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
name: string;
|
||||
description?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
8
frontend/src/hooks/api/pam/types/base-resource.ts
Normal file
8
frontend/src/hooks/api/pam/types/base-resource.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface TBasePamResource {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
gatewayId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
97
frontend/src/hooks/api/pam/types/index.ts
Normal file
97
frontend/src/hooks/api/pam/types/index.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { PamResourceType, PamSessionStatus } from "../enums";
|
||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||
|
||||
export * from "./postgres-resource";
|
||||
|
||||
export type TPamResource = TPostgresResource;
|
||||
|
||||
export type TPamAccount = TPostgresAccount;
|
||||
|
||||
export type TPamFolder = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
parentId?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TPamSession = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
accountId?: string | null;
|
||||
resourceType: PamResourceType;
|
||||
resourceName: string;
|
||||
accountName: string;
|
||||
userId?: string | null;
|
||||
actorName: string;
|
||||
actorEmail: string;
|
||||
actorIp: string;
|
||||
actorUserAgent: string;
|
||||
status: PamSessionStatus;
|
||||
expiresAt?: string | null;
|
||||
startedAt?: string | null;
|
||||
endedAt?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
commandLogs: {
|
||||
input: string;
|
||||
output: string;
|
||||
timestamp: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
// Resource DTOs
|
||||
export type TCreatePamResourceDTO = Pick<
|
||||
TPamResource,
|
||||
"name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId"
|
||||
>;
|
||||
|
||||
export type TUpdatePamResourceDTO = Partial<
|
||||
Pick<TPamResource, "name" | "connectionDetails" | "gatewayId">
|
||||
> & {
|
||||
resourceId: string;
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
|
||||
export type TDeletePamResourceDTO = {
|
||||
resourceId: string;
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
|
||||
// Account DTOs
|
||||
export type TCreatePamAccountDTO = Pick<
|
||||
TPamAccount,
|
||||
"name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId"
|
||||
> & {
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
|
||||
export type TUpdatePamAccountDTO = Partial<
|
||||
Pick<TPamAccount, "name" | "description" | "credentials">
|
||||
> & {
|
||||
accountId: string;
|
||||
resourceId: string;
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
|
||||
export type TDeletePamAccountDTO = {
|
||||
accountId: string;
|
||||
resourceId: string;
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
|
||||
// Folder DTOs
|
||||
export type TCreatePamFolderDTO = Pick<
|
||||
TPamFolder,
|
||||
"name" | "description" | "parentId" | "projectId"
|
||||
>;
|
||||
|
||||
export type TUpdatePamFolderDTO = Partial<Pick<TPamFolder, "name" | "description">> & {
|
||||
folderId: string;
|
||||
};
|
||||
|
||||
export type TDeletePamFolderDTO = {
|
||||
folderId: string;
|
||||
};
|
||||
14
frontend/src/hooks/api/pam/types/postgres-resource.ts
Normal file
14
frontend/src/hooks/api/pam/types/postgres-resource.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PamResourceType } from "../enums";
|
||||
import { TBaseSqlConnectionDetails, TBaseSqlCredentials } from "./shared/sql-resource";
|
||||
import { TBasePamAccount } from "./base-account";
|
||||
import { TBasePamResource } from "./base-resource";
|
||||
|
||||
// Resources
|
||||
export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & {
|
||||
connectionDetails: TBaseSqlConnectionDetails;
|
||||
};
|
||||
|
||||
// Accounts
|
||||
export type TPostgresAccount = TBasePamAccount & {
|
||||
credentials: TBaseSqlCredentials;
|
||||
};
|
||||
11
frontend/src/hooks/api/pam/types/resource-options.ts
Normal file
11
frontend/src/hooks/api/pam/types/resource-options.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PamResourceType } from "../enums";
|
||||
|
||||
export type TPamResourceOptionBase = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TPostgresResourceOption = TPamResourceOptionBase & {
|
||||
resource: PamResourceType.Postgres;
|
||||
};
|
||||
|
||||
export type TPamResourceOption = TPostgresResourceOption;
|
||||
12
frontend/src/hooks/api/pam/types/shared/sql-resource.ts
Normal file
12
frontend/src/hooks/api/pam/types/shared/sql-resource.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type TBaseSqlConnectionDetails = {
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
sslEnabled: boolean;
|
||||
sslRejectUnauthorized: boolean;
|
||||
};
|
||||
|
||||
export type TBaseSqlCredentials = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
@@ -13,7 +13,8 @@ export enum ProjectType {
|
||||
CertificateManager = "cert-manager",
|
||||
KMS = "kms",
|
||||
SSH = "ssh",
|
||||
SecretScanning = "secret-scanning"
|
||||
SecretScanning = "secret-scanning",
|
||||
PAM = "pam"
|
||||
}
|
||||
|
||||
export enum ProjectUserMembershipTemporaryMode {
|
||||
|
||||
@@ -58,4 +58,5 @@ export type SubscriptionPlan = {
|
||||
cardDeclined?: boolean;
|
||||
cardDeclinedReason?: string;
|
||||
machineIdentityAuthTemplates: boolean;
|
||||
pam: boolean;
|
||||
};
|
||||
|
||||
195
frontend/src/layouts/PamLayout/PamLayout.tsx
Normal file
195
frontend/src/layouts/PamLayout/PamLayout.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
faBook,
|
||||
faBoxOpen,
|
||||
faCog,
|
||||
faDisplay,
|
||||
faHome,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { useProject, useProjectPermission, useSubscription } from "@app/context";
|
||||
|
||||
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
|
||||
import { useEffect } from "react";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
|
||||
export const PamLayout = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.pam) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="groups" />
|
||||
PAM
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Resources">
|
||||
<Link
|
||||
to="/projects/pam/$projectId/accounts"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</div>
|
||||
Accounts
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/resources"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBoxOpen} />
|
||||
</div>
|
||||
Resources
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/sessions"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faDisplay} />
|
||||
</div>
|
||||
Sessions
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link
|
||||
to="/projects/pam/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Management
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Home
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use PAM if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
frontend/src/layouts/PamLayout/index.tsx
Normal file
1
frontend/src/layouts/PamLayout/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { PamLayout } from "./PamLayout";
|
||||
@@ -164,7 +164,10 @@ export const PkiSyncRow = ({
|
||||
</div>
|
||||
</Td>
|
||||
{subscriberId ? (
|
||||
<PkiSyncTableCell primaryText={pkiSync.subscriber?.name || subscriberId} secondaryText="PKI Subscriber" />
|
||||
<PkiSyncTableCell
|
||||
primaryText={pkiSync.subscriber?.name || subscriberId}
|
||||
secondaryText="PKI Subscriber"
|
||||
/>
|
||||
) : (
|
||||
<Td>
|
||||
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
|
||||
|
||||
@@ -24,12 +24,13 @@ import { useOrganization } from "@app/context";
|
||||
import { useGetUserProjects } from "@app/hooks/api";
|
||||
import {
|
||||
eventToNameMap,
|
||||
projectToEventsMap,
|
||||
secretEvents,
|
||||
userAgentTypeToNameMap
|
||||
} from "@app/hooks/api/auditLogs/constants";
|
||||
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||
import { Project } from "@app/hooks/api/projects/types";
|
||||
import { Project, ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { LogFilterItem } from "./LogFilterItem";
|
||||
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
|
||||
@@ -94,10 +95,20 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||
const selectedProject = project ?? watch("project");
|
||||
|
||||
const currentSelectedEventTypes = selectedEventTypes ?? [];
|
||||
const hasSecretEventFilter = currentSelectedEventTypes.some((eventType) =>
|
||||
secretEvents.includes(eventType)
|
||||
);
|
||||
const showSecretsSection =
|
||||
selectedEventTypes?.some(
|
||||
(eventType) => secretEvents.includes(eventType) && eventType !== EventType.GET_SECRETS
|
||||
) || selectedEventTypes?.length === 0;
|
||||
project?.type !== ProjectType.PAM &&
|
||||
(hasSecretEventFilter || currentSelectedEventTypes.length === 0);
|
||||
|
||||
const filteredEventTypes = useMemo(() => {
|
||||
const projectEvents = project?.type ? projectToEventsMap[project.type] : undefined;
|
||||
if (!projectEvents) return eventTypes;
|
||||
|
||||
return eventTypes.filter((v) => projectEvents.includes(v.value as EventType));
|
||||
}, [project]);
|
||||
|
||||
const availableEnvironments = useMemo(() => {
|
||||
if (!selectedProject) return [];
|
||||
@@ -166,7 +177,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? eventTypes.find(
|
||||
? filteredEventTypes.find(
|
||||
(eventType) => eventType.value === selectedEventTypes[0]
|
||||
)?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
@@ -181,8 +192,8 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
|
||||
>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{eventTypes && eventTypes.length > 0 ? (
|
||||
eventTypes.map((eventType) => {
|
||||
{filteredEventTypes.length > 0 ? (
|
||||
filteredEventTypes.map((eventType) => {
|
||||
const isSelected = selectedEventTypes?.includes(
|
||||
eventType.value as EventType
|
||||
);
|
||||
@@ -190,7 +201,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
eventTypes.length > 1 && event.preventDefault()
|
||||
filteredEventTypes.length > 1 && event.preventDefault()
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
|
||||
@@ -63,6 +63,10 @@ const PROJECT_TYPE_MENU_ITEMS = [
|
||||
{
|
||||
label: "Secret Scanning",
|
||||
value: ProjectType.SecretScanning
|
||||
},
|
||||
{
|
||||
label: "PAM",
|
||||
value: ProjectType.PAM
|
||||
}
|
||||
];
|
||||
|
||||
@@ -131,12 +135,12 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<div className="mt-2 grid grid-cols-5 gap-4">
|
||||
<div className="mt-2 grid grid-cols-3 gap-3">
|
||||
{PROJECT_TYPE_MENU_ITEMS.map((el) => (
|
||||
<div
|
||||
key={el.value}
|
||||
className={twMerge(
|
||||
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
|
||||
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 px-2 py-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
|
||||
field.value === el.value && "border-primary-400 bg-mineshaft-600 opacity-100"
|
||||
)}
|
||||
onClick={() => field.onChange(el.value)}
|
||||
|
||||
34
frontend/src/pages/pam/PamAccountsPage/PamAccountsPage.tsx
Normal file
34
frontend/src/pages/pam/PamAccountsPage/PamAccountsPage.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { PamAccountsSection } from "./components/PamAccountsSection";
|
||||
|
||||
export const PamAccountsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "PAM" })}</title>
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionPamAccountActions.Read}
|
||||
a={ProjectPermissionSub.PamAccounts}
|
||||
>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Accounts" description="View, access, and manage accounts." />
|
||||
<PamAccountsSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Button } from "@app/components/v2";
|
||||
|
||||
export enum AccountView {
|
||||
Flat = "flat",
|
||||
Nested = "nested"
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: AccountView;
|
||||
onChange: (value: AccountView) => void;
|
||||
};
|
||||
|
||||
export const AccountViewToggle = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<div className="flex gap-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(AccountView.Flat);
|
||||
}}
|
||||
size="xs"
|
||||
className={`${
|
||||
value === AccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
Hide Folders
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(AccountView.Nested);
|
||||
}}
|
||||
size="xs"
|
||||
className={`${
|
||||
value === AccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
Show Folders
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormLabel, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
import { TPamAccount } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
account?: TPamAccount;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
|
||||
const [duration, setDuration] = useState("4h");
|
||||
|
||||
const isDurationValid = useMemo(() => ms(duration || "1s") > 0, [duration]);
|
||||
|
||||
const command = useMemo(
|
||||
() =>
|
||||
account
|
||||
? `infisical pam access ${account.id}${duration ? ` --duration ${duration}` : ""}`
|
||||
: "",
|
||||
[account, duration]
|
||||
);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const copyCommand = () => {
|
||||
navigator.clipboard.writeText(command);
|
||||
|
||||
createNotification({
|
||||
text: "Command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Access Account"
|
||||
subTitle={`Access ${account.name} using a CLI command.`}
|
||||
>
|
||||
<FormLabel
|
||||
label="Duration"
|
||||
tooltipText="The maximum duration of your session. Ex: 1h, 3w, 30d"
|
||||
/>
|
||||
<Input
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
placeholder="permanent"
|
||||
isError={!isDurationValid}
|
||||
/>
|
||||
<FormLabel label="CLI Command" className="mt-4" />
|
||||
<Input value={command} isDisabled />
|
||||
<a
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span>Install the Infisical CLI</span>
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
|
||||
</a>
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
isDisabled={!isDurationValid}
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
colorSchema="secondary"
|
||||
onClick={copyCommand}
|
||||
>
|
||||
Copy Command
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
export const genericAccountFieldsSchema = z.object({
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }),
|
||||
description: z.string().max(512).nullable().optional()
|
||||
});
|
||||
|
||||
export const GenericAccountFields = () => {
|
||||
const {
|
||||
formState: { errors },
|
||||
control
|
||||
} = useFormContext<{ name: string; description: string }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
helperText="Name must be slug-friendly"
|
||||
errorText={errors.name?.message}
|
||||
isError={Boolean(errors.name?.message)}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus placeholder="my-account" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
errorText={errors.name?.message}
|
||||
isError={Boolean(errors.name?.message)}
|
||||
label="Description"
|
||||
>
|
||||
<TextArea {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
PamResourceType,
|
||||
TPamAccount,
|
||||
useCreatePamAccount,
|
||||
useUpdatePamAccount
|
||||
} from "@app/hooks/api/pam";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { PamAccountHeader } from "../PamAccountHeader";
|
||||
import { PostgresAccountForm } from "./PostgresAccountForm";
|
||||
|
||||
type FormProps = {
|
||||
onComplete: (account: TPamAccount) => void;
|
||||
};
|
||||
|
||||
type CreateFormProps = FormProps & {
|
||||
projectId: string;
|
||||
resourceId: string;
|
||||
resourceType: PamResourceType;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
type UpdateFormProps = FormProps & {
|
||||
account: TPamAccount;
|
||||
};
|
||||
|
||||
const CreateForm = ({
|
||||
onComplete,
|
||||
projectId,
|
||||
resourceId,
|
||||
resourceType,
|
||||
folderId
|
||||
}: CreateFormProps) => {
|
||||
const createPamAccount = useCreatePamAccount();
|
||||
|
||||
console.log({ folderId });
|
||||
|
||||
const onSubmit = async (
|
||||
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
|
||||
) => {
|
||||
try {
|
||||
const account = await createPamAccount.mutateAsync({
|
||||
...formData,
|
||||
folderId,
|
||||
resourceId,
|
||||
resourceType,
|
||||
projectId
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully created account",
|
||||
type: "success"
|
||||
});
|
||||
onComplete(account);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: "Failed to create account",
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
switch (resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
return <PostgresAccountForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
|
||||
const updatePamAccount = useUpdatePamAccount();
|
||||
|
||||
const onSubmit = async (
|
||||
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
|
||||
) => {
|
||||
try {
|
||||
const updatedAccount = await updatePamAccount.mutateAsync({
|
||||
accountId: account.id,
|
||||
resourceId: account.resourceId,
|
||||
resourceType: account.resource.resourceType,
|
||||
...formData
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully updated account",
|
||||
type: "success"
|
||||
});
|
||||
onComplete(updatedAccount);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: "Failed to update account",
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
switch (account.resource.resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
return <PostgresAccountForm account={account} onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onBack?: () => void;
|
||||
projectId: string;
|
||||
} & FormProps &
|
||||
(
|
||||
| {
|
||||
account: TPamAccount;
|
||||
resourceId?: undefined;
|
||||
resourceName?: undefined;
|
||||
resourceType?: undefined;
|
||||
folderId?: undefined;
|
||||
}
|
||||
| {
|
||||
account?: undefined;
|
||||
resourceId: string;
|
||||
resourceName: string;
|
||||
resourceType: PamResourceType;
|
||||
folderId?: string;
|
||||
}
|
||||
);
|
||||
|
||||
export const PamAccountForm = ({ onBack, projectId, ...props }: Props) => {
|
||||
const { account, resourceName, resourceType } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PamAccountHeader
|
||||
resourceName={account ? account.resource.name : resourceName}
|
||||
resourceType={account ? account.resource.resourceType : resourceType}
|
||||
onBack={onBack}
|
||||
/>
|
||||
{account ? (
|
||||
<UpdateForm {...props} account={account} />
|
||||
) : (
|
||||
<CreateForm {...props} projectId={projectId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { TPostgresAccount } from "@app/hooks/api/pam";
|
||||
|
||||
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
|
||||
type Props = {
|
||||
account?: TPostgresAccount;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericAccountFieldsSchema.extend({
|
||||
credentials: BaseSqlAccountSchema
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(account);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: account
|
||||
? {
|
||||
...account,
|
||||
credentials: {
|
||||
...account.credentials,
|
||||
password: "******"
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<GenericAccountFields />
|
||||
<SqlAccountFields isUpdate={isUpdate} />
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Account" : "Create Account"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
|
||||
export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
name="credentials.username"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Username"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.password"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Password"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
onFocus={(e) => {
|
||||
if (isUpdate && field.value === "******") {
|
||||
field.onChange("");
|
||||
}
|
||||
e.target.type = "text";
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (isUpdate && field.value === "") {
|
||||
field.onChange("******");
|
||||
}
|
||||
e.target.type = "password";
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const BaseSqlAccountSchema = z.object({
|
||||
username: z.string().trim().min(1, "Username required"),
|
||||
password: z.string().trim().min(1, "Password required")
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
resourceName: string;
|
||||
resourceType: PamResourceType;
|
||||
onBack?: () => void;
|
||||
};
|
||||
|
||||
export const PamAccountHeader = ({ resourceName, resourceType, onBack }: Props) => {
|
||||
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
|
||||
<img
|
||||
alt={`${details.name} logo`}
|
||||
src={`/images/integrations/${details.image}`}
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">{resourceName}</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">{details.name} resource</p>
|
||||
</div>
|
||||
{onBack && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-auto mt-1 text-xs text-mineshaft-400 underline underline-offset-2 hover:text-mineshaft-300"
|
||||
onClick={onBack}
|
||||
>
|
||||
Select another resource
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
faBoxOpen,
|
||||
faCheck,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faRightToBracket,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamAccount } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
account: TPamAccount;
|
||||
onAccess: (resource: TPamAccount) => void;
|
||||
onUpdate: (resource: TPamAccount) => void;
|
||||
onDelete: (resource: TPamAccount) => void;
|
||||
search: string;
|
||||
};
|
||||
|
||||
export const PamAccountRow = ({ account, search, onAccess, onUpdate, onDelete }: Props) => {
|
||||
const { id, name } = account;
|
||||
|
||||
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[account.resource.resourceType];
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
const handleCopyId = useCallback(
|
||||
(idToCopy: string) => {
|
||||
setIsIdCopied.on();
|
||||
navigator.clipboard.writeText(idToCopy);
|
||||
|
||||
createNotification({
|
||||
text: "Account ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
},
|
||||
[isIdCopied]
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr className={twMerge("group h-10")} key={`account-${id}`}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<img alt={resourceTypeName} src={`/images/integrations/${image}`} className="size-5" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span>
|
||||
<HighlightText text={name} highlight={search} />
|
||||
</span>
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-bunker-300/20 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBoxOpen} />
|
||||
<span>
|
||||
<HighlightText text={account.resource.name} highlight={search} />
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPamAccountActions.Access}
|
||||
a={ProjectPermissionSub.PamAccounts}
|
||||
>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faRightToBracket} />}
|
||||
onClick={() => onAccess(account)}
|
||||
size="xs"
|
||||
>
|
||||
Access
|
||||
</Button>
|
||||
</ProjectPermissionCan>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyId(id);
|
||||
}}
|
||||
>
|
||||
Copy Account ID
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPamAccountActions.Edit}
|
||||
a={ProjectPermissionSub.PamAccounts}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => onUpdate(account)}
|
||||
>
|
||||
Edit Account
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPamAccountActions.Delete}
|
||||
a={ProjectPermissionSub.PamAccounts}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={() => onDelete(account)}
|
||||
>
|
||||
Delete Account
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ContentLoader } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useListPamAccounts } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamAccountsTable } from "./PamAccountsTable";
|
||||
|
||||
export const PamAccountsSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data, isPending } = useListPamAccounts(currentProject.id, {
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
if (isPending) return <ContentLoader />;
|
||||
|
||||
return (
|
||||
<PamAccountsTable
|
||||
projectId={currentProject.id}
|
||||
accounts={data?.accounts || []}
|
||||
folders={data?.folders || []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,501 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheckCircle,
|
||||
faFilter,
|
||||
faFolderPlus,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionPamAccountActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamAccount, TPamFolder } from "@app/hooks/api/pam";
|
||||
|
||||
import { AccountView, AccountViewToggle } from "./AccountViewToggle";
|
||||
import { PamAccessAccountModal } from "./PamAccessAccountModal";
|
||||
import { PamAccountRow } from "./PamAccountRow";
|
||||
import { PamAddAccountModal } from "./PamAddAccountModal";
|
||||
import { PamAddFolderModal } from "./PamAddFolderModal";
|
||||
import { PamDeleteAccountModal } from "./PamDeleteAccountModal";
|
||||
import { PamDeleteFolderModal } from "./PamDeleteFolderModal";
|
||||
import { PamFolderRow } from "./PamFolderRow";
|
||||
import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
|
||||
import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
|
||||
import { useSubscription } from "@app/context";
|
||||
|
||||
enum OrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
type Filters = {
|
||||
resource: string[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
accounts: TPamAccount[];
|
||||
folders: TPamFolder[];
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.AccountsPage.path });
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"misc",
|
||||
"addFolder",
|
||||
"updateFolder",
|
||||
"deleteFolder",
|
||||
"addAccount",
|
||||
"accessAccount",
|
||||
"updateAccount",
|
||||
"deleteAccount"
|
||||
] as const);
|
||||
|
||||
const {
|
||||
search: initSearch,
|
||||
accountPath,
|
||||
accountView: initAccountView
|
||||
} = useSearch({
|
||||
from: ROUTE_PATHS.Pam.AccountsPage.id
|
||||
});
|
||||
|
||||
const [accountView, setAccountView] = useState<AccountView>(initAccountView ?? AccountView.Flat);
|
||||
|
||||
const [filters, setFilters] = useState<Filters>({
|
||||
resource: []
|
||||
});
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<OrderBy>(OrderBy.Name, { initPerPage: 20, initSearch });
|
||||
|
||||
const { foldersByParentId, pathMap } = useMemo(() => {
|
||||
const foldersById: Record<string, TPamFolder> = {};
|
||||
const tempFoldersByParentId: Record<string, TPamFolder[]> = { null: [] };
|
||||
const tempPathMap: Record<string, string> = { "/": "null" };
|
||||
|
||||
folders.forEach((folder) => {
|
||||
foldersById[folder.id] = folder;
|
||||
if (!tempFoldersByParentId[folder.parentId || "null"]) {
|
||||
tempFoldersByParentId[folder.parentId || "null"] = [];
|
||||
}
|
||||
tempFoldersByParentId[folder.parentId || "null"].push(folder);
|
||||
});
|
||||
|
||||
const buildPaths = (parentId: string | null, currentPath: string) => {
|
||||
(tempFoldersByParentId[parentId || "null"] || []).forEach((folder) => {
|
||||
const newPath = `${currentPath}${folder.name}/`;
|
||||
tempPathMap[newPath] = folder.id;
|
||||
buildPaths(folder.id, newPath);
|
||||
});
|
||||
};
|
||||
|
||||
buildPaths(null, "/");
|
||||
|
||||
return { foldersByParentId: tempFoldersByParentId, pathMap: tempPathMap };
|
||||
}, [folders]);
|
||||
|
||||
const effectiveFolderIdForFiltering = useMemo(() => {
|
||||
if (accountView === AccountView.Flat) {
|
||||
return null;
|
||||
}
|
||||
const folderId = pathMap[accountPath];
|
||||
return folderId === "null" ? null : folderId || null;
|
||||
}, [accountView, accountPath, pathMap]);
|
||||
|
||||
const foldersToRender = useMemo(() => {
|
||||
if (accountView === AccountView.Flat) {
|
||||
return [];
|
||||
}
|
||||
return (foldersByParentId[effectiveFolderIdForFiltering || "null"] || []).filter((folder) =>
|
||||
folder.name.toLowerCase().includes(search.trim().toLowerCase())
|
||||
);
|
||||
}, [accountView, effectiveFolderIdForFiltering, foldersByParentId, search]);
|
||||
|
||||
const accountsToProcess = useMemo(() => {
|
||||
if (accountView === AccountView.Flat) {
|
||||
return accounts;
|
||||
}
|
||||
return accounts.filter(
|
||||
(acc) => (acc.folderId || "null") === (effectiveFolderIdForFiltering || "null")
|
||||
);
|
||||
}, [accountView, accounts, effectiveFolderIdForFiltering]);
|
||||
|
||||
const filteredAccounts = useMemo(
|
||||
() =>
|
||||
accountsToProcess
|
||||
.filter((account) => {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
resource: { name: resourceName, id: resourceId }
|
||||
} = account;
|
||||
|
||||
if (filters.resource.length && !filters.resource.includes(resourceId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) ||
|
||||
resourceName.toLowerCase().includes(searchValue) ||
|
||||
(description || "").toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [accOne, accTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case OrderBy.Name:
|
||||
default:
|
||||
return accOne.name.toLowerCase().localeCompare(accTwo.name.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[accountsToProcess, orderDirection, search, orderBy, filters]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredAccounts.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const currentPageData = useMemo(
|
||||
() => filteredAccounts.slice(offset, perPage * page),
|
||||
[filteredAccounts, offset, perPage, page]
|
||||
);
|
||||
|
||||
const handleSort = (column: OrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: OrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const isTableFiltered = Boolean(filters.resource.length);
|
||||
|
||||
const handleFolderClick = (folder: TPamFolder) => {
|
||||
if (accountView === AccountView.Flat) {
|
||||
return;
|
||||
}
|
||||
const newPath = `${accountPath}${folder.name}/`;
|
||||
navigate({ search: (prev) => ({ ...prev, accountPath: newPath }) });
|
||||
};
|
||||
|
||||
const isContentEmpty = !filteredAccounts.length && !foldersToRender.length;
|
||||
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
|
||||
|
||||
const uniqueResources = useMemo(() => {
|
||||
const resourceMap = new Map<string, TPamAccount["resource"]>();
|
||||
accounts.forEach((account) => {
|
||||
resourceMap.set(account.resource.id, account.resource);
|
||||
});
|
||||
return Array.from(resourceMap.values());
|
||||
}, [accounts]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
<AccountViewToggle
|
||||
value={accountView}
|
||||
onChange={(e) => {
|
||||
setAccountView(e);
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
accountView: e === AccountView.Flat ? undefined : e,
|
||||
accountPath: e === AccountView.Flat ? "/" : prev.accountPath
|
||||
})
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
const newSearch = e.target.value;
|
||||
setSearch(newSearch);
|
||||
navigate({
|
||||
search: (prev) => ({ ...prev, search: newSearch || undefined }),
|
||||
replace: true
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search accounts..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter accounts"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 min-w-10 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>Resource</DropdownMenuLabel>
|
||||
{uniqueResources.length ? (
|
||||
uniqueResources.map((resource) => {
|
||||
const { name, image } = PAM_RESOURCE_TYPE_MAP[resource.resourceType];
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const newResources = filters.resource.includes(resource.id)
|
||||
? filters.resource.filter((a) => a !== resource.id)
|
||||
: [...filters.resource, resource.id];
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
resource: newResources
|
||||
}));
|
||||
}}
|
||||
key={resource.id}
|
||||
icon={
|
||||
filters.resource.includes(resource.id) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${name} resource`}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span>{resource.name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem isDisabled>No Account Resources</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPamAccountActions.Create}
|
||||
a={ProjectPermissionSub.PamAccounts}
|
||||
>
|
||||
{(isAllowedToCreateAccounts) => (
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addAccount")}
|
||||
isDisabled={!isAllowedToCreateAccounts || !subscription.pam}
|
||||
className={`h-10 transition-colors ${accountView === AccountView.Flat ? "" : "rounded-r-none"}`}
|
||||
>
|
||||
Add Account
|
||||
</Button>
|
||||
|
||||
{accountView !== AccountView.Flat && (
|
||||
<DropdownMenu
|
||||
open={popUp.misc.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
ariaLabel="add-folder-or-import"
|
||||
className="rounded-l-none bg-mineshaft-600 p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" sideOffset={5}>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.PamFolders}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} className="pr-2" />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addFolder");
|
||||
handlePopUpClose("misc");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Accounts
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(OrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{accountView !== AccountView.Flat &&
|
||||
foldersToRender.map((folder) => (
|
||||
<PamFolderRow
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
search={search}
|
||||
onClick={() => handleFolderClick(folder)}
|
||||
onUpdate={(e) => handlePopUpOpen("updateFolder", e)}
|
||||
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
|
||||
/>
|
||||
))}
|
||||
{currentPageData.map((account) => (
|
||||
<PamAccountRow
|
||||
key={account.id}
|
||||
account={account}
|
||||
search={search}
|
||||
onAccess={(e) => {
|
||||
handlePopUpOpen("accessAccount", e);
|
||||
}}
|
||||
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
||||
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredAccounts.length) && (
|
||||
<Pagination
|
||||
count={filteredAccounts.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{isContentEmpty && (
|
||||
<EmptyState
|
||||
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<PamDeleteFolderModal
|
||||
isOpen={popUp.deleteFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||
folder={popUp.deleteFolder.data}
|
||||
/>
|
||||
<PamUpdateFolderModal
|
||||
isOpen={popUp.updateFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("updateFolder", isOpen)}
|
||||
folder={popUp.updateFolder.data}
|
||||
/>
|
||||
<PamAddFolderModal
|
||||
isOpen={popUp.addFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||
projectId={projectId}
|
||||
currentFolderId={effectiveFolderIdForFiltering}
|
||||
/>
|
||||
<PamAccessAccountModal
|
||||
isOpen={popUp.accessAccount.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("accessAccount", isOpen)}
|
||||
account={popUp.accessAccount.data}
|
||||
/>
|
||||
<PamDeleteAccountModal
|
||||
isOpen={popUp.deleteAccount.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}
|
||||
account={popUp.deleteAccount.data}
|
||||
/>
|
||||
<PamUpdateAccountModal
|
||||
isOpen={popUp.updateAccount.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("updateAccount", isOpen)}
|
||||
account={popUp.updateAccount.data}
|
||||
/>
|
||||
<PamAddAccountModal
|
||||
isOpen={popUp.addAccount.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addAccount", isOpen)}
|
||||
projectId={projectId}
|
||||
currentFolderId={effectiveFolderIdForFiltering}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { PamResourceType, TPamAccount } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamAccountForm } from "./PamAccountForm/PamAccountForm";
|
||||
import { ResourceSelect } from "./ResourceSelect";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
projectId: string;
|
||||
onComplete?: (account: TPamAccount) => void;
|
||||
currentFolderId: string | null;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onComplete: (account: TPamAccount) => void;
|
||||
projectId: string;
|
||||
currentFolderId: string | null;
|
||||
};
|
||||
|
||||
const Content = ({ onComplete, projectId, currentFolderId }: ContentProps) => {
|
||||
const [selectedResource, setSelectedResource] = useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
resourceType: PamResourceType;
|
||||
} | null>(null);
|
||||
|
||||
if (selectedResource) {
|
||||
return (
|
||||
<PamAccountForm
|
||||
onComplete={onComplete}
|
||||
onBack={() => setSelectedResource(null)}
|
||||
resourceId={selectedResource.id}
|
||||
resourceName={selectedResource.name}
|
||||
resourceType={selectedResource.resourceType}
|
||||
projectId={projectId}
|
||||
folderId={currentFolderId ?? undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ResourceSelect projectId={projectId} onSubmit={(e) => setSelectedResource(e.resource)} />;
|
||||
};
|
||||
|
||||
export const PamAddAccountModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
projectId,
|
||||
onComplete,
|
||||
currentFolderId
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Add Account"
|
||||
subTitle="Select a resource to add an account under."
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<Content
|
||||
projectId={projectId}
|
||||
onComplete={(account) => {
|
||||
if (onComplete) onComplete(account);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
currentFolderId={currentFolderId}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { TPamFolder, useCreatePamFolder } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamFolderForm } from "./PamFolderForm";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
projectId: string;
|
||||
currentFolderId: string | null;
|
||||
};
|
||||
|
||||
export const PamAddFolderModal = ({ isOpen, onOpenChange, projectId, currentFolderId }: Props) => {
|
||||
const createPamFolder = useCreatePamFolder();
|
||||
|
||||
console.log({ currentFolderId });
|
||||
|
||||
const onSubmit = async (formData: Pick<TPamFolder, "name" | "description">) => {
|
||||
try {
|
||||
await createPamFolder.mutateAsync({
|
||||
...formData,
|
||||
parentId: currentFolderId,
|
||||
projectId
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully created folder",
|
||||
type: "success"
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: "Failed to create folder",
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent className="max-w-2xl" title="Create Folder">
|
||||
<PamFolderForm onSubmit={onSubmit} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { TPamAccount, useDeletePamAccount } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
account?: TPamAccount;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
|
||||
const deletePamAccount = useDeletePamAccount();
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const {
|
||||
id: accountId,
|
||||
name,
|
||||
resourceId,
|
||||
resource: { resourceType }
|
||||
} = account;
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deletePamAccount.mutateAsync({
|
||||
accountId,
|
||||
resourceId,
|
||||
resourceType
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted account",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete account",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDelete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { TPamFolder, useDeletePamFolder } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
folder?: TPamFolder;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const PamDeleteFolderModal = ({ isOpen, onOpenChange, folder }: Props) => {
|
||||
const deletePamFolder = useDeletePamFolder();
|
||||
|
||||
if (!folder) return null;
|
||||
|
||||
const { id: folderId, name } = folder;
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deletePamFolder.mutateAsync({
|
||||
folderId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted folder",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete folder",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDelete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, TextArea } from "@app/components/v2";
|
||||
import { TPamFolder } from "@app/hooks/api/pam";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
type Props = {
|
||||
folder?: TPamFolder;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }),
|
||||
description: z.string().max(512).optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PamFolderForm = ({ folder, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(folder);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: folder
|
||||
? {
|
||||
name: folder.name,
|
||||
description: folder.description || ""
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty, errors }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
helperText="Name must be slug-friendly"
|
||||
errorText={errors.name?.message}
|
||||
isError={Boolean(errors.name?.message)}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus placeholder="my-folder" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
errorText={errors.name?.message}
|
||||
isError={Boolean(errors.name?.message)}
|
||||
label="Description"
|
||||
isOptional
|
||||
>
|
||||
<TextArea {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Folder" : "Create Folder"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { faEdit, faEllipsisV, faFolder, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { TPamFolder } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
folder: TPamFolder;
|
||||
onUpdate: (folder: TPamFolder) => void;
|
||||
onDelete: (folder: TPamFolder) => void;
|
||||
onClick: () => void;
|
||||
search: string;
|
||||
};
|
||||
|
||||
export const PamFolderRow = ({ folder, onClick, onDelete, onUpdate, search }: Props) => {
|
||||
return (
|
||||
<Tr
|
||||
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex w-5 justify-center">
|
||||
<FontAwesomeIcon icon={faFolder} className="size-4 text-yellow-700" />
|
||||
</div>
|
||||
<span>
|
||||
<HighlightText text={folder.name} highlight={search} />
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex h-[22px] justify-end">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="hidden w-6 group-hover:flex data-[state=open]:!flex"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.PamFolders}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdate(folder);
|
||||
}}
|
||||
>
|
||||
Edit Folder
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.PamFolders}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(folder);
|
||||
}}
|
||||
>
|
||||
Delete Folder
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { components, OptionProps } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
|
||||
|
||||
export const PamResourceOption = ({
|
||||
isSelected,
|
||||
children,
|
||||
...props
|
||||
}: OptionProps<{ id: string; name: string; resourceType: PamResourceType }>) => {
|
||||
const isCreateOption = props.data.id === "_create";
|
||||
|
||||
const { name, image } = PAM_RESOURCE_TYPE_MAP[props.data.resourceType];
|
||||
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
{isCreateOption ? (
|
||||
<div className="flex items-center gap-x-1 text-mineshaft-400">
|
||||
<FontAwesomeIcon icon={faPlus} size="sm" />
|
||||
<span className="mr-auto">Create New Resource</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="truncate">{children}</p>
|
||||
<div className="ml-2 mr-auto">
|
||||
<Badge className="flex h-5 items-center gap-1 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<img
|
||||
alt={`${name} logo`}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="size-3"
|
||||
/>
|
||||
{name}
|
||||
</Badge>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { TPamAccount } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamAccountForm } from "./PamAccountForm/PamAccountForm";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
account?: TPamAccount;
|
||||
};
|
||||
|
||||
export const PamUpdateAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent className="max-w-2xl" title="Edit Account" subTitle="Update account details.">
|
||||
<PamAccountForm
|
||||
onComplete={() => onOpenChange(false)}
|
||||
account={account}
|
||||
projectId={account.projectId}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { TPamFolder, useUpdatePamFolder } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamFolderForm } from "./PamFolderForm";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
folder?: TPamFolder;
|
||||
};
|
||||
|
||||
export const PamUpdateFolderModal = ({ isOpen, onOpenChange, folder }: Props) => {
|
||||
const updatePamFolder = useUpdatePamFolder();
|
||||
|
||||
if (!folder) return null;
|
||||
|
||||
const onSubmit = async (formData: Pick<TPamFolder, "name" | "description">) => {
|
||||
try {
|
||||
await updatePamFolder.mutateAsync({
|
||||
...formData,
|
||||
folderId: folder.id
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully updated folder",
|
||||
type: "success"
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: "Failed to updated folder",
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent className="max-w-2xl" title="Edit Account" subTitle="Update account details.">
|
||||
<PamFolderForm onSubmit={onSubmit} folder={folder} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FilterableSelect, FormControl, ModalClose, Spinner } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { PamResourceType, useListPamResources } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamAddResourceModal } from "../../PamResourcesPage/components/PamAddResourceModal";
|
||||
import { PamResourceOption } from "./PamResourceOption";
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: FormData) => void;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
resource: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
resourceType: z.nativeEnum(PamResourceType)
|
||||
})
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ResourceSelect = ({ onSubmit, projectId }: Props) => {
|
||||
const { permission } = useProjectPermission();
|
||||
const { isPending, data: resources } = useListPamResources(projectId);
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addResource"] as const);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const { handleSubmit, control, setValue } = form;
|
||||
|
||||
const canCreateResource = permission.can(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.PamResources
|
||||
);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
<Spinner size="lg" className="text-mineshaft-500" />
|
||||
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Resource">
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
if ((newValue as SingleValue<{ id: string }>)?.id === "_create") {
|
||||
handlePopUpOpen("addResource");
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
}}
|
||||
isLoading={isPending}
|
||||
options={[
|
||||
...(canCreateResource
|
||||
? [
|
||||
{
|
||||
id: "_create",
|
||||
name: "Create Resource",
|
||||
// This is just to make typescript happy. Does not actually do anything
|
||||
resourceType: PamResourceType.Postgres
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(resources ?? [])
|
||||
]}
|
||||
placeholder="Select resource..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
components={{ Option: PamResourceOption }}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="resource"
|
||||
/>
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button className="mr-4" size="sm" type="submit" colorSchema="secondary">
|
||||
Continue
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<PamAddResourceModal
|
||||
isOpen={popUp.addResource.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addResource", isOpen)}
|
||||
projectId={projectId}
|
||||
onComplete={(resource) => setValue("resource", resource)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
49
frontend/src/pages/pam/PamAccountsPage/route.tsx
Normal file
49
frontend/src/pages/pam/PamAccountsPage/route.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AccountView } from "./components/AccountViewToggle";
|
||||
import { PamAccountsPage } from "./PamAccountsPage";
|
||||
|
||||
const PamAccountsPageQueryParamsSchema = z.object({
|
||||
search: z.string().optional(),
|
||||
accountView: z.nativeEnum(AccountView).optional(),
|
||||
accountPath: z.string().catch("/")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts"
|
||||
)({
|
||||
validateSearch: zodValidator(PamAccountsPageQueryParamsSchema),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ accountPath: "/" })]
|
||||
},
|
||||
beforeLoad: ({ context, params, search }) => {
|
||||
const accountPathSegments = search.accountPath.split("/").filter(Boolean);
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Accounts",
|
||||
link: linkOptions({
|
||||
to: "/projects/pam/$projectId/accounts",
|
||||
params: () => params as never,
|
||||
search: (prev) => ({ ...prev, accountPath: "/" })
|
||||
})
|
||||
},
|
||||
...accountPathSegments.map((segment, index) => {
|
||||
const newPath = `/${accountPathSegments.slice(0, index + 1).join("/")}/`;
|
||||
return {
|
||||
label: segment,
|
||||
link: linkOptions({
|
||||
to: "/projects/pam/$projectId/accounts",
|
||||
params: () => params as never,
|
||||
search: (prev) => ({ ...prev, accountPath: newPath })
|
||||
})
|
||||
};
|
||||
})
|
||||
]
|
||||
};
|
||||
},
|
||||
component: PamAccountsPage
|
||||
});
|
||||
37
frontend/src/pages/pam/PamResourcesPage/PamResourcesPage.tsx
Normal file
37
frontend/src/pages/pam/PamResourcesPage/PamResourcesPage.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { PamResourcesSection } from "./components/PamResourcesSection";
|
||||
|
||||
export const PamResourcesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "PAM" })}</title>
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionPamAccountActions.Read}
|
||||
a={ProjectPermissionSub.PamResources}
|
||||
>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Resources"
|
||||
description="Manage resources such as servers, databases, and more."
|
||||
/>
|
||||
<PamResourcesSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { PamResourceType, TPamResource } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamResourceForm } from "./PamResourceForm";
|
||||
import { ResourceTypeSelect } from "./ResourceTypeSelect";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
projectId: string;
|
||||
onComplete?: (resource: TPamResource) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onComplete: (resource: TPamResource) => void;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
const Content = ({ onComplete, projectId }: ContentProps) => {
|
||||
const [selectedResourceType, setSelectedResourceType] = useState<PamResourceType | null>(null);
|
||||
|
||||
if (selectedResourceType) {
|
||||
return (
|
||||
<PamResourceForm
|
||||
onComplete={onComplete}
|
||||
onBack={() => setSelectedResourceType(null)}
|
||||
resourceType={selectedResourceType}
|
||||
projectId={projectId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ResourceTypeSelect onSelect={setSelectedResourceType} />;
|
||||
};
|
||||
|
||||
export const PamAddResourceModal = ({ isOpen, onOpenChange, projectId, onComplete }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent className="max-w-2xl" title="Add Resource" subTitle="Select a resource to add.">
|
||||
<Content
|
||||
projectId={projectId}
|
||||
onComplete={(resource) => {
|
||||
if (onComplete) onComplete(resource);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamResource, useDeletePamResource } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
resource?: TPamResource;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const PamDeleteResourceModal = ({ isOpen, onOpenChange, resource }: Props) => {
|
||||
const deletePamResource = useDeletePamResource();
|
||||
|
||||
if (!resource) return null;
|
||||
|
||||
const { id: resourceId, name, resourceType } = resource;
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deletePamResource.mutateAsync({
|
||||
resourceId,
|
||||
resourceType
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully removed ${PAM_RESOURCE_TYPE_MAP[resourceType].name} resource`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to remove ${PAM_RESOURCE_TYPE_MAP[resourceType].name} resource`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDelete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
|
||||
import { FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
export const genericResourceFieldsSchema = z.object({
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }),
|
||||
gatewayId: z.string().min(1)
|
||||
});
|
||||
|
||||
export const GenericResourceFields = () => {
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
control
|
||||
} = useFormContext<{ name: string; gatewayId: string }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
helperText="Name must be slug-friendly"
|
||||
errorText={errors.name?.message}
|
||||
isError={Boolean(errors.name?.message)}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus placeholder="my-resource" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="gatewayId"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message} label="Gateway">
|
||||
<Select
|
||||
value={value || (null as unknown as string)}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isGatewaysLoading}
|
||||
placeholder="Select a Gateway..."
|
||||
position="popper"
|
||||
>
|
||||
{(gateways || []).map((el) => (
|
||||
<SelectItem value={el.id} key={el.id}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
PAM_RESOURCE_TYPE_MAP,
|
||||
PamResourceType,
|
||||
TPamResource,
|
||||
useCreatePamResource,
|
||||
useUpdatePamResource
|
||||
} from "@app/hooks/api/pam";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { PamResourceHeader } from "../PamResourceHeader";
|
||||
import { PostgresResourceForm } from "./PostgresResourceForm";
|
||||
|
||||
type FormProps = {
|
||||
onComplete: (resource: TPamResource) => void;
|
||||
} & ({ resource: TPamResource } | { resourceType: PamResourceType });
|
||||
|
||||
type CreateFormProps = FormProps & {
|
||||
resourceType: PamResourceType;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
type UpdateFormProps = FormProps & {
|
||||
resource: TPamResource;
|
||||
};
|
||||
|
||||
const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) => {
|
||||
const createPamResource = useCreatePamResource();
|
||||
const { name: resourceName } = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
const onSubmit = async (
|
||||
formData: DiscriminativePick<
|
||||
TPamResource,
|
||||
"name" | "resourceType" | "connectionDetails" | "gatewayId"
|
||||
>
|
||||
) => {
|
||||
try {
|
||||
const resource = await createPamResource.mutateAsync({
|
||||
...formData,
|
||||
projectId
|
||||
});
|
||||
createNotification({
|
||||
text: `Successfully created ${resourceName} resource`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(resource);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: `Failed to create ${resourceName} resource`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
switch (resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
return <PostgresResourceForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => {
|
||||
const updatePamResource = useUpdatePamResource();
|
||||
const { name: resourceName } = PAM_RESOURCE_TYPE_MAP[resource.resourceType];
|
||||
|
||||
const onSubmit = async (
|
||||
formData: DiscriminativePick<TPamResource, "name" | "resourceType" | "connectionDetails">
|
||||
) => {
|
||||
try {
|
||||
const updatedResource = await updatePamResource.mutateAsync({
|
||||
resourceId: resource.id,
|
||||
...formData
|
||||
});
|
||||
createNotification({
|
||||
text: `Successfully updated ${resourceName} resource`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(updatedResource);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: `Failed to update ${resourceName} resource`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
switch (resource.resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${resource.resourceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
type Props = { onBack?: () => void; projectId: string } & Pick<FormProps, "onComplete"> &
|
||||
(
|
||||
| { resourceType: PamResourceType; resource?: undefined }
|
||||
| { resourceType?: undefined; resource: TPamResource }
|
||||
);
|
||||
export const PamResourceForm = ({ onBack, projectId, ...props }: Props) => {
|
||||
const { resource, resourceType } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PamResourceHeader resourceType={resourceType || resource.resourceType} onBack={onBack} />
|
||||
{resource ? (
|
||||
<UpdateForm {...props} resource={resource} />
|
||||
) : (
|
||||
<CreateForm {...props} resourceType={resourceType} projectId={projectId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam";
|
||||
|
||||
import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
|
||||
import { SqlResourceFields } from "./shared/SqlResourceFields";
|
||||
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
|
||||
|
||||
type Props = {
|
||||
resource?: TPostgresResource;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericResourceFieldsSchema.extend({
|
||||
resourceType: z.literal(PamResourceType.Postgres),
|
||||
connectionDetails: BaseSqlResourceSchema
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PostgresResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(resource);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: resource ?? {
|
||||
resourceType: PamResourceType.Postgres,
|
||||
connectionDetails: {
|
||||
host: "",
|
||||
port: 5432,
|
||||
database: "default",
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
setSelectedTabIndex(0);
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<GenericResourceFields />
|
||||
<SqlResourceFields
|
||||
selectedTabIndex={selectedTabIndex}
|
||||
setSelectedTabIndex={setSelectedTabIndex}
|
||||
/>
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Details" : "Create Resource"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./GenericResourceFields";
|
||||
export * from "./PamResourceForm";
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { FormControl, Input, Switch, TextArea, Tooltip } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
selectedTabIndex: number;
|
||||
setSelectedTabIndex: Dispatch<SetStateAction<number>>;
|
||||
};
|
||||
|
||||
export const SqlResourceFields = ({ setSelectedTabIndex, selectedTabIndex }: Props) => {
|
||||
const { control, watch } = useFormContext();
|
||||
|
||||
const sslEnabled = watch("connectionDetails.sslEnabled");
|
||||
return (
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected ? "border-b-2 border-mineshaft-300 text-mineshaft-200" : "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Configuration
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected ? "border-b-2 border-mineshaft-300 text-mineshaft-200" : "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
SSL ({sslEnabled ? "Enabled" : "Disabled"})
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
|
||||
<Tab.Panel>
|
||||
<div className="mt-[0.675rem] flex items-start gap-2">
|
||||
<Controller
|
||||
name="connectionDetails.host"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Host"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="connectionDetails.database"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Database Name"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="connectionDetails.port"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-28"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Port"
|
||||
>
|
||||
<Input type="number" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Controller
|
||||
name="connectionDetails.sslEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
Enable SSL
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="connectionDetails.sslCertificate"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
label="SSL Certificate"
|
||||
isOptional
|
||||
>
|
||||
<TextArea className="h-[3.5rem] !resize-none" {...field} isDisabled={!sslEnabled} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="connectionDetails.sslRejectUnauthorized"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-reject-unauthorized"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={sslEnabled ? value : false}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!sslEnabled}
|
||||
>
|
||||
<p className="w-[9.5rem]">
|
||||
Reject Unauthorized
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<p>
|
||||
If enabled, Infisical will only connect to the server if it has a valid,
|
||||
trusted SSL certificate.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const BaseSqlResourceSchema = z.object({
|
||||
host: z.string().trim().min(1, "Host required"),
|
||||
port: z.coerce.number().default(5432),
|
||||
database: z.string().trim().min(1, "Database required").default("default"),
|
||||
sslEnabled: z.boolean().default(true),
|
||||
sslRejectUnauthorized: z.boolean().default(true),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
resourceType: PamResourceType;
|
||||
onBack?: () => void;
|
||||
};
|
||||
|
||||
export const PamResourceHeader = ({ resourceType, onBack }: Props) => {
|
||||
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
|
||||
<img
|
||||
alt={`${details.name} logo`}
|
||||
src={`/images/integrations/${details.image}`}
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">{details.name}</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">External resource</p>
|
||||
</div>
|
||||
{onBack && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-auto mt-1 text-xs text-mineshaft-400 underline underline-offset-2 hover:text-mineshaft-300"
|
||||
onClick={onBack}
|
||||
>
|
||||
Select another resource type
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamResource } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
resource: TPamResource;
|
||||
onUpdate: (resource: TPamResource) => void;
|
||||
onDelete: (resource: TPamResource) => void;
|
||||
search: string;
|
||||
};
|
||||
|
||||
export const PamResourceRow = ({ resource, onUpdate, onDelete, search }: Props) => {
|
||||
const { name, resourceType } = resource;
|
||||
|
||||
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
return (
|
||||
<Tr className={twMerge("group h-10")}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<img alt={resourceTypeName} src={`/images/integrations/${image}`} className="size-5" />
|
||||
</div>
|
||||
<span>
|
||||
<HighlightText text={name} highlight={search} />
|
||||
</span>
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<span>
|
||||
<HighlightText text={resourceTypeName} highlight={search} />
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.PamResources}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => onUpdate(resource)}
|
||||
>
|
||||
Edit Resource
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.PamResources}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={() => onDelete(resource)}
|
||||
>
|
||||
Delete Resource
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ContentLoader } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useListPamResources } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamResourcesTable } from "./PamResourcesTable";
|
||||
|
||||
export const PamResourcesSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data: resources = [], isPending } = useListPamResources(currentProject.id, {
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
if (isPending) return <ContentLoader />;
|
||||
|
||||
return <PamResourcesTable resources={resources} projectId={currentProject.id} />;
|
||||
};
|
||||
@@ -0,0 +1,326 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheckCircle,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { PAM_RESOURCE_TYPE_MAP, PamResourceType, TPamResource } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamAddResourceModal } from "./PamAddResourceModal";
|
||||
import { PamDeleteResourceModal } from "./PamDeleteResourceModal";
|
||||
import { PamResourceRow } from "./PamResourceRow";
|
||||
import { PamUpdateResourceModal } from "./PamUpdateResourceModal";
|
||||
|
||||
enum OrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
type Filters = {
|
||||
resourceType: PamResourceType[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
resources: TPamResource[];
|
||||
};
|
||||
|
||||
export const PamResourcesTable = ({ projectId, resources }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.ResourcesPage.path });
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"updateResource",
|
||||
"addResource",
|
||||
"deleteResource"
|
||||
] as const);
|
||||
|
||||
const { search: initSearch } = useSearch({
|
||||
from: ROUTE_PATHS.Pam.ResourcesPage.id
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<Filters>({
|
||||
resourceType: []
|
||||
});
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<OrderBy>(OrderBy.Name, { initPerPage: 20, initSearch });
|
||||
|
||||
const filteredResources = useMemo(
|
||||
() =>
|
||||
resources
|
||||
.filter((resource) => {
|
||||
const { name, resourceType } = resource;
|
||||
|
||||
if (filters.resourceType.length && !filters.resourceType.includes(resourceType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
const { name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) ||
|
||||
resourceTypeName.toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [one, two] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case OrderBy.Name:
|
||||
default:
|
||||
return one.name.toLowerCase().localeCompare(two.name.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[resources, orderDirection, search, orderBy, filters]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredResources.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const currentPageData = useMemo(
|
||||
() => filteredResources.slice(offset, perPage * page),
|
||||
[filteredResources, offset, perPage, page]
|
||||
);
|
||||
|
||||
const handleSort = (column: OrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: OrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const isTableFiltered = Boolean(filters.resourceType.length);
|
||||
const isContentEmpty = !filteredResources.length;
|
||||
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
const newSearch = e.target.value;
|
||||
setSearch(newSearch);
|
||||
navigate({
|
||||
search: (prev) => ({ ...prev, search: newSearch || undefined }),
|
||||
replace: true
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search resources..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter resources"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>Resource Type</DropdownMenuLabel>
|
||||
{resources.length ? (
|
||||
[...new Set(resources.map(({ resourceType }) => resourceType))].map((type) => {
|
||||
const { name, image } = PAM_RESOURCE_TYPE_MAP[type];
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
resourceType: prev.resourceType.includes(type)
|
||||
? prev.resourceType.filter((a) => a !== type)
|
||||
: [...prev.resourceType, type]
|
||||
}));
|
||||
}}
|
||||
key={type}
|
||||
icon={
|
||||
filters.resourceType.includes(type) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${name} resource type`}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem isDisabled>No Resources</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.AttachGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isGatewayAllowed) => (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.PamResources}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip
|
||||
isDisabled={isGatewayAllowed}
|
||||
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||
>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addResource")}
|
||||
isDisabled={
|
||||
!isAllowed || !isGatewayAllowed || !subscription.gateway || !subscription.pam
|
||||
}
|
||||
>
|
||||
Add Resource
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Resource
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(OrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{currentPageData.map((resource) => (
|
||||
<PamResourceRow
|
||||
key={resource.id}
|
||||
resource={resource}
|
||||
onUpdate={(e) => handlePopUpOpen("updateResource", e)}
|
||||
onDelete={(e) => handlePopUpOpen("deleteResource", e)}
|
||||
search={search.trim().toLowerCase()}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredResources.length) && (
|
||||
<Pagination
|
||||
count={filteredResources.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{isContentEmpty && (
|
||||
<EmptyState
|
||||
title={isSearchEmpty ? "No resources match search" : "No resources"}
|
||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<PamDeleteResourceModal
|
||||
isOpen={popUp.deleteResource.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteResource", isOpen)}
|
||||
resource={popUp.deleteResource.data}
|
||||
/>
|
||||
<PamUpdateResourceModal
|
||||
isOpen={popUp.updateResource.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("updateResource", isOpen)}
|
||||
resource={popUp.updateResource.data}
|
||||
/>
|
||||
<PamAddResourceModal
|
||||
isOpen={popUp.addResource.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addResource", isOpen)}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamResource } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamResourceForm } from "./PamResourceForm";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
resource?: TPamResource;
|
||||
};
|
||||
|
||||
export const PamUpdateResourceModal = ({ isOpen, onOpenChange, resource }: Props) => {
|
||||
if (!resource) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Edit Resource"
|
||||
subTitle={`Update details for this ${
|
||||
PAM_RESOURCE_TYPE_MAP[resource.resourceType].name
|
||||
} resource.`}
|
||||
>
|
||||
<PamResourceForm
|
||||
onComplete={() => onOpenChange(false)}
|
||||
resource={resource}
|
||||
projectId={resource.projectId}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useMemo } from "react";
|
||||
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
PAM_RESOURCE_TYPE_MAP,
|
||||
PamResourceType,
|
||||
useListPamResourceOptions
|
||||
} from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
onSelect: (resource: PamResourceType) => void;
|
||||
};
|
||||
|
||||
export const ResourceTypeSelect = ({ onSelect }: Props) => {
|
||||
const { isPending, data: resourceOptions } = useListPamResourceOptions();
|
||||
|
||||
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: 16
|
||||
});
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
resourceOptions?.filter(
|
||||
({ name, resource }) =>
|
||||
name.toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||
resource.toLowerCase().includes(search.trim().toLowerCase())
|
||||
) ?? [],
|
||||
[resourceOptions, search]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredOptions.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
<Spinner size="lg" className="text-mineshaft-500" />
|
||||
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search options..."
|
||||
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
|
||||
/>
|
||||
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
|
||||
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
|
||||
const { image, name, size = 50 } = PAM_RESOURCE_TYPE_MAP[option.resource];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect(option.resource)}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
width: `${size}px`
|
||||
}}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredOptions?.length && (
|
||||
<EmptyState
|
||||
className="col-span-full mt-40"
|
||||
title="No resources match search"
|
||||
icon={faSearch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(filteredOptions.length) && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more resources.</p>
|
||||
<p>
|
||||
{"If you don't see the resource you're looking for,"}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
|
||||
<span className="text-xs">Don't see the resource you're looking for?</span>
|
||||
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
count={filteredOptions.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
perPageList={[16]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
26
frontend/src/pages/pam/PamResourcesPage/route.tsx
Normal file
26
frontend/src/pages/pam/PamResourcesPage/route.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamResourcesPage } from "./PamResourcesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources"
|
||||
)({
|
||||
validateSearch: zodValidator(
|
||||
z.object({
|
||||
search: z.string().optional()
|
||||
})
|
||||
),
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Resources"
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
component: PamResourcesPage
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useGetPamSessionById } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamSessionDetailsSection } from "./components/PamSessionDetailsSection";
|
||||
import { PamSessionLogsSection } from "./components/PamSessionLogsSection";
|
||||
|
||||
const Page = () => {
|
||||
const sessionId = useParams({
|
||||
from: ROUTE_PATHS.Pam.PamSessionByIDPage.id,
|
||||
select: (el) => el.sessionId
|
||||
});
|
||||
const { data: session } = useGetPamSessionById(sessionId);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{session && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title={`${session.accountName} Session`}
|
||||
description={`View details for this ${session.accountName} session.`}
|
||||
/>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<PamSessionDetailsSection session={session} />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<PamSessionLogsSection session={session} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PamSessionByIDPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>PAM Session</title>
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPamSessionActions.Read}
|
||||
a={ProjectPermissionSub.PamSessions}
|
||||
passThrough={false}
|
||||
renderGuardBanner
|
||||
>
|
||||
<Page />
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { faBoxOpen, faCheck, faCopy, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamSessionStatusBadge } from "../../PamSessionsPage/components/PamSessionStatusBadge";
|
||||
|
||||
type Props = {
|
||||
session: TPamSession;
|
||||
};
|
||||
|
||||
const DetailItem = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div className="mb-4">
|
||||
<p className="font-semibold">{label}</p>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PamSessionDetailsSection = ({
|
||||
session: {
|
||||
id,
|
||||
accountName,
|
||||
resourceType,
|
||||
resourceName,
|
||||
status,
|
||||
actorName,
|
||||
actorEmail,
|
||||
createdAt,
|
||||
endedAt,
|
||||
actorIp,
|
||||
actorUserAgent,
|
||||
startedAt,
|
||||
expiresAt
|
||||
}
|
||||
}: Props) => {
|
||||
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
return (
|
||||
<div className="max-w-[350px] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Session Details</h3>
|
||||
</div>
|
||||
<div className="pt-4 text-sm text-mineshaft-300">
|
||||
<DetailItem label="Session ID">
|
||||
<div className="group flex align-top">
|
||||
<p className="truncate">{id}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextId}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="Account">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${details.name} logo`}
|
||||
src={`/images/integrations/${details.image}`}
|
||||
className="size-4"
|
||||
/>
|
||||
<p>{accountName}</p>
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-yellow/20 text-yellow">
|
||||
<FontAwesomeIcon icon={faBoxOpen} />
|
||||
{resourceName}
|
||||
</Badge>
|
||||
</div>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="Actor">
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faUser} className="-translate-y-px" />
|
||||
<p>
|
||||
<strong>{actorName}</strong> ({actorEmail})
|
||||
</p>
|
||||
</div>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="Status">
|
||||
<PamSessionStatusBadge status={status} />
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="IP Address">
|
||||
<p>{actorIp}</p>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="User Agent">
|
||||
<p className="truncate">{actorUserAgent}</p>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="Created At">
|
||||
<p>{new Date(createdAt).toLocaleString()}</p>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="Expires At">
|
||||
<p>{expiresAt ? new Date(expiresAt).toLocaleString() : "Never"}</p>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="Started At">
|
||||
<p>{startedAt ? new Date(startedAt).toLocaleString() : "Not Started"}</p>
|
||||
</DetailItem>
|
||||
|
||||
<DetailItem label="Ended At">
|
||||
<p>{endedAt ? new Date(endedAt).toLocaleString() : "Ongoing"}</p>
|
||||
</DetailItem>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { faTerminal } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { TPamSession } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
session: TPamSession;
|
||||
};
|
||||
|
||||
export const PamSessionLogsSection = ({ session }: Props) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Session Logs</h3>
|
||||
</div>
|
||||
<div className="flex grow flex-col gap-4 text-xs">
|
||||
{session.commandLogs.length > 0 ? (
|
||||
session.commandLogs.map((log) => (
|
||||
<div key={log.timestamp} className="flex flex-col">
|
||||
<div className="flex items-center gap-1.5 text-bunker-400">
|
||||
<FontAwesomeIcon icon={faTerminal} className="size-3" />
|
||||
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="whitespace-pre-wrap font-mono">{log.input}</div>
|
||||
<div className="whitespace-pre-wrap font-mono text-bunker-300">{log.output}</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex w-full grow items-center justify-center text-bunker-300">
|
||||
No session logs
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
26
frontend/src/pages/pam/PamSessionsByIDPage/route.tsx
Normal file
26
frontend/src/pages/pam/PamSessionsByIDPage/route.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { PamSessionByIDPage } from "./PamSessionByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
|
||||
)({
|
||||
component: PamSessionByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Sessions",
|
||||
link: linkOptions({
|
||||
to: "/projects/pam/$projectId/sessions",
|
||||
params
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Details"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
37
frontend/src/pages/pam/PamSessionsPage/PamSessionsPage.tsx
Normal file
37
frontend/src/pages/pam/PamSessionsPage/PamSessionsPage.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { PamSessionSection } from "./components/PamSessionSection";
|
||||
|
||||
export const PamSessionPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "PAM" })}</title>
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionPamSessionActions.Read}
|
||||
a={ProjectPermissionSub.PamSessions}
|
||||
>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Sessions"
|
||||
description="Filter and search through account sessions."
|
||||
/>
|
||||
<PamSessionSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faBoxOpen,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faTerminal
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamSessionStatusBadge } from "./PamSessionStatusBadge";
|
||||
|
||||
type Props = {
|
||||
session: TPamSession;
|
||||
search: string;
|
||||
filteredCommandLogs: TPamSession["commandLogs"];
|
||||
};
|
||||
|
||||
export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) => {
|
||||
const router = useRouter();
|
||||
const [showAllLogs, setShowAllLogs] = useState(false);
|
||||
|
||||
const {
|
||||
id,
|
||||
accountName,
|
||||
resourceType,
|
||||
resourceName,
|
||||
projectId,
|
||||
status,
|
||||
actorName,
|
||||
actorEmail,
|
||||
createdAt,
|
||||
endedAt
|
||||
} = session;
|
||||
|
||||
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
const LOGS_TO_SHOW = 5;
|
||||
const logsToShow = showAllLogs ? filteredCommandLogs : filteredCommandLogs.slice(0, LOGS_TO_SHOW);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className={twMerge("group h-10 cursor-pointer hover:bg-bunker-400/20")}
|
||||
onClick={() => router.history.push(`/projects/pam/${projectId}/sessions/${id}`)}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<img
|
||||
alt={resourceTypeName}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span>
|
||||
<HighlightText text={accountName} highlight={search} />
|
||||
</span>
|
||||
<div className="flex items-center gap-1 text-xs text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBoxOpen} className="size-3" />
|
||||
<span>
|
||||
<HighlightText text={resourceName} highlight={search} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col">
|
||||
<span>
|
||||
<HighlightText text={actorName} highlight={search} />
|
||||
</span>
|
||||
<span className="text-xs text-bunker-300">
|
||||
<HighlightText text={actorEmail} highlight={search} />
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col">
|
||||
<span>{new Date(createdAt).toLocaleTimeString()}</span>
|
||||
<span className="text-xs text-bunker-300">
|
||||
{new Date(createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{endedAt ? (
|
||||
<div className="flex flex-col">
|
||||
<span>{new Date(endedAt).toLocaleTimeString()}</span>
|
||||
<span className="text-xs text-bunker-300">
|
||||
{new Date(endedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-bunker-400">Ongoing</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<PamSessionStatusBadge status={status} />
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.PamResources}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() =>
|
||||
router.history.push(`/projects/pam/${projectId}/sessions/${id}`)
|
||||
}
|
||||
>
|
||||
View Session
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
{filteredCommandLogs.length > 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5} className="py-3 text-xs">
|
||||
{logsToShow.map((log) => (
|
||||
<div key={`${id}-log-${log.timestamp}`} className="mb-4 flex flex-col last:mb-0">
|
||||
<div className="flex items-center gap-1.5 text-bunker-400">
|
||||
<FontAwesomeIcon icon={faTerminal} className="size-3" />
|
||||
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="font-mono">
|
||||
<HighlightText text={log.input} highlight={search} />
|
||||
</div>
|
||||
<div className="font-mono text-bunker-300">
|
||||
<HighlightText text={log.output} highlight={search} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredCommandLogs.length > LOGS_TO_SHOW && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={showAllLogs ? faChevronUp : faChevronDown} />}
|
||||
onClick={() => setShowAllLogs(!showAllLogs)}
|
||||
className="p-0 text-mineshaft-300 hover:text-primary"
|
||||
>
|
||||
{showAllLogs
|
||||
? "Show less"
|
||||
: `Show ${filteredCommandLogs.length - LOGS_TO_SHOW} more log${filteredCommandLogs.length - LOGS_TO_SHOW === 1 ? "" : "s"}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ContentLoader } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useListPamSessions } from "@app/hooks/api/pam";
|
||||
|
||||
import { PamSessionsTable } from "./PamSessionsTable";
|
||||
|
||||
export const PamSessionSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data: sessions = [], isPending } = useListPamSessions(currentProject.id, {
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
if (isPending) return <ContentLoader />;
|
||||
|
||||
return <PamSessionsTable sessions={sessions} />;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
faBan,
|
||||
faCircle,
|
||||
faGavel,
|
||||
faHourglass,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { PamSessionStatus } from "@app/hooks/api/pam";
|
||||
|
||||
interface StatusConfig {
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
icon: IconDefinition;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
const PAM_SESSION_STATUS_CONFIG: Record<PamSessionStatus, StatusConfig> = {
|
||||
[PamSessionStatus.Active]: {
|
||||
bgColor: "bg-green/20",
|
||||
textColor: "text-green",
|
||||
icon: faCircle,
|
||||
iconClassName: "size-2 animate-pulse"
|
||||
},
|
||||
[PamSessionStatus.Terminated]: {
|
||||
bgColor: "bg-red/20",
|
||||
textColor: "text-red",
|
||||
icon: faGavel
|
||||
},
|
||||
[PamSessionStatus.Starting]: {
|
||||
bgColor: "bg-yellow/20",
|
||||
textColor: "text-yellow",
|
||||
icon: faHourglass,
|
||||
iconClassName: "animate-spin"
|
||||
},
|
||||
[PamSessionStatus.Ended]: {
|
||||
bgColor: "bg-bunker-300/20",
|
||||
textColor: "text-bunker-300",
|
||||
icon: faBan
|
||||
}
|
||||
};
|
||||
|
||||
export const PamSessionStatusBadge = ({ status }: { status: PamSessionStatus }) => {
|
||||
const config = PAM_SESSION_STATUS_CONFIG[status];
|
||||
|
||||
const displayName = status[0].toUpperCase() + status.slice(1);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap",
|
||||
config.bgColor,
|
||||
config.textColor
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={config.icon} className={config.iconClassName} />
|
||||
{displayName}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,411 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faCircleXmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheckCircle,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import {
|
||||
PAM_RESOURCE_TYPE_MAP,
|
||||
PamResourceType,
|
||||
PamSessionStatus,
|
||||
TPamSession
|
||||
} from "@app/hooks/api/pam";
|
||||
|
||||
import { PamSessionRow } from "./PamSessionRow";
|
||||
|
||||
enum OrderBy {
|
||||
Account = "account",
|
||||
Actor = "actor",
|
||||
CreatedAt = "createdAt",
|
||||
EndedAt = "endedAt"
|
||||
}
|
||||
|
||||
type Filters = {
|
||||
resourceType: PamResourceType[];
|
||||
status: PamSessionStatus[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
sessions: TPamSession[];
|
||||
};
|
||||
|
||||
export const PamSessionsTable = ({ sessions }: Props) => {
|
||||
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.SessionsPage.path });
|
||||
|
||||
const { search: initSearch } = useSearch({
|
||||
from: ROUTE_PATHS.Pam.SessionsPage.id
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<Filters>({
|
||||
resourceType: [],
|
||||
status: []
|
||||
});
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<OrderBy>(OrderBy.CreatedAt, {
|
||||
initPerPage: 20,
|
||||
initSearch
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setOrderDirection(OrderByDirection.DESC);
|
||||
}, []);
|
||||
|
||||
const sessionsWithMatches = useMemo(() => {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
if (!searchValue) {
|
||||
return sessions.map((session) => ({
|
||||
session,
|
||||
isMatch: true,
|
||||
filteredLogs: []
|
||||
}));
|
||||
}
|
||||
|
||||
return sessions.map((session) => {
|
||||
const {
|
||||
resourceType,
|
||||
accountName,
|
||||
actorEmail,
|
||||
actorIp,
|
||||
actorName,
|
||||
actorUserAgent,
|
||||
id,
|
||||
resourceName,
|
||||
userId,
|
||||
commandLogs
|
||||
} = session;
|
||||
|
||||
const { name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
|
||||
const isMetaMatch =
|
||||
resourceTypeName.toLowerCase().includes(searchValue) ||
|
||||
accountName.toLowerCase().includes(searchValue) ||
|
||||
actorEmail.toLowerCase().includes(searchValue) ||
|
||||
actorIp.toLowerCase().includes(searchValue) ||
|
||||
actorName.toLowerCase().includes(searchValue) ||
|
||||
actorUserAgent.toLowerCase().includes(searchValue) ||
|
||||
id.toLowerCase().includes(searchValue) ||
|
||||
(userId ?? "").toLowerCase().includes(searchValue) ||
|
||||
resourceName.toLowerCase().includes(searchValue);
|
||||
|
||||
const filteredLogs =
|
||||
searchValue.length >= 2
|
||||
? commandLogs.filter(
|
||||
(log) =>
|
||||
log.input.toLowerCase().includes(searchValue) ||
|
||||
log.output.toLowerCase().includes(searchValue)
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
session,
|
||||
isMatch: isMetaMatch || filteredLogs.length > 0,
|
||||
filteredLogs
|
||||
};
|
||||
});
|
||||
}, [sessions, search]);
|
||||
|
||||
const filteredSessions = useMemo(
|
||||
() =>
|
||||
sessionsWithMatches
|
||||
.filter((item) => {
|
||||
if (!item.isMatch) return false;
|
||||
|
||||
const { resourceType, status } = item.session;
|
||||
if (
|
||||
(filters.resourceType.length && !filters.resourceType.includes(resourceType)) ||
|
||||
(filters.status.length && !filters.status.includes(status))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [one, two] =
|
||||
orderDirection === OrderByDirection.ASC
|
||||
? [a.session, b.session]
|
||||
: [b.session, a.session];
|
||||
|
||||
switch (orderBy) {
|
||||
case OrderBy.Account:
|
||||
return one.accountName.toLowerCase().localeCompare(two.accountName.toLowerCase());
|
||||
case OrderBy.Actor:
|
||||
return one.actorName.toLowerCase().localeCompare(two.actorName.toLowerCase());
|
||||
case OrderBy.EndedAt: {
|
||||
const dateOne = one.endedAt || one.createdAt;
|
||||
const dateTwo = two.endedAt || two.createdAt;
|
||||
return new Date(dateOne).getTime() - new Date(dateTwo).getTime();
|
||||
}
|
||||
case OrderBy.CreatedAt:
|
||||
default: {
|
||||
const dateOne = one.createdAt;
|
||||
const dateTwo = two.createdAt;
|
||||
return new Date(dateOne).getTime() - new Date(dateTwo).getTime();
|
||||
}
|
||||
}
|
||||
}),
|
||||
[sessionsWithMatches, orderDirection, orderBy, filters]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredSessions.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const currentPageData = useMemo(
|
||||
() => filteredSessions.slice(offset, perPage * page),
|
||||
[filteredSessions, offset, perPage, page]
|
||||
);
|
||||
|
||||
const handleSort = (column: OrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: OrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const isTableFiltered = Boolean(filters.resourceType.length || filters.status.length);
|
||||
const isContentEmpty = !filteredSessions.length;
|
||||
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
const newSearch = e.target.value;
|
||||
setSearch(newSearch);
|
||||
navigate({
|
||||
search: (prev) => ({ ...prev, search: newSearch || undefined }),
|
||||
replace: true
|
||||
});
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search sessions and logs..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter sessions"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>Session Status</DropdownMenuLabel>
|
||||
{sessions.length ? (
|
||||
[...new Set(sessions.map(({ status }) => status))].map((status) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
status: prev.status.includes(status)
|
||||
? prev.status.filter((a) => a !== status)
|
||||
: [...prev.status, status]
|
||||
}));
|
||||
}}
|
||||
key={status}
|
||||
icon={
|
||||
filters.status.includes(status) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
{status[0].toUpperCase() + status.slice(1)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem isDisabled>No Sessions</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuLabel>Resource Type</DropdownMenuLabel>
|
||||
{sessions.length ? (
|
||||
[...new Set(sessions.map(({ resourceType }) => resourceType))].map((type) => {
|
||||
const { name, image } = PAM_RESOURCE_TYPE_MAP[type];
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
resourceType: prev.resourceType.includes(type)
|
||||
? prev.resourceType.filter((a) => a !== type)
|
||||
: [...prev.resourceType, type]
|
||||
}));
|
||||
}}
|
||||
key={type}
|
||||
icon={
|
||||
filters.resourceType.includes(type) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${name} resource type`}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem isDisabled>No Sessions</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Account
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(OrderBy.Account)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrderBy.Account)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Account)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Actor
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(OrderBy.Actor)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrderBy.Actor)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Actor)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Created At
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(OrderBy.CreatedAt)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrderBy.CreatedAt)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(OrderBy.CreatedAt)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Ended At
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(OrderBy.EndedAt)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrderBy.EndedAt)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(OrderBy.EndedAt)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{currentPageData.map(({ session, filteredLogs }) => (
|
||||
<PamSessionRow
|
||||
key={session.id}
|
||||
session={session}
|
||||
search={search.trim().toLowerCase()}
|
||||
filteredCommandLogs={filteredLogs}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredSessions.length) && (
|
||||
<Pagination
|
||||
count={filteredSessions.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{isContentEmpty && (
|
||||
<EmptyState
|
||||
title={isSearchEmpty ? "No sessions match search" : "No sessions"}
|
||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
26
frontend/src/pages/pam/PamSessionsPage/route.tsx
Normal file
26
frontend/src/pages/pam/PamSessionsPage/route.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamSessionPage } from "./PamSessionsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/"
|
||||
)({
|
||||
validateSearch: zodValidator(
|
||||
z.object({
|
||||
search: z.string().optional()
|
||||
})
|
||||
),
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Sessions"
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
component: PamSessionPage
|
||||
});
|
||||
28
frontend/src/pages/pam/SettingsPage/SettingsPage.tsx
Normal file
28
frontend/src/pages/pam/SettingsPage/SettingsPage.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Settings" description="Configure your PAM project." />
|
||||
<Tabs defaultValue="tab-project-general">
|
||||
<TabList>
|
||||
<Tab value="tab-project-general">General</Tab>
|
||||
</TabList>
|
||||
<TabPanel value="tab-project-general">
|
||||
<ProjectGeneralTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
19
frontend/src/pages/pam/SettingsPage/route.tsx
Normal file
19
frontend/src/pages/pam/SettingsPage/route.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings"
|
||||
)({
|
||||
component: SettingsPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
37
frontend/src/pages/pam/layout.tsx
Normal file
37
frontend/src/pages/pam/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { BreadcrumbTypes } from "@app/components/v2";
|
||||
import { projectKeys } from "@app/hooks/api";
|
||||
import { fetchProjectById } from "@app/hooks/api/projects/queries";
|
||||
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
||||
import { PamLayout } from "@app/layouts/PamLayout";
|
||||
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
)({
|
||||
component: PamLayout,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
const project = await context.queryClient.ensureQueryData({
|
||||
queryKey: projectKeys.getProjectById(params.projectId),
|
||||
queryFn: () => fetchProjectById(params.projectId)
|
||||
});
|
||||
|
||||
await context.queryClient.ensureQueryData({
|
||||
queryKey: roleQueryKeys.getUserProjectPermissions({
|
||||
projectId: params.projectId
|
||||
}),
|
||||
queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId })
|
||||
});
|
||||
|
||||
return {
|
||||
project,
|
||||
breadcrumbs: [
|
||||
{
|
||||
type: BreadcrumbTypes.Component,
|
||||
component: ProjectSelect
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
32
frontend/src/pages/project/AccessControlPage/route-pam.tsx
Normal file
32
frontend/src/pages/project/AccessControlPage/route-pam.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { AccessControlPage } from "./AccessControlPage";
|
||||
|
||||
const AccessControlPageQuerySchema = z.object({
|
||||
selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member),
|
||||
requesterEmail: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management"
|
||||
)({
|
||||
component: AccessControlPage,
|
||||
validateSearch: zodValidator(AccessControlPageQuerySchema),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ requesterEmail: "" })]
|
||||
},
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
19
frontend/src/pages/project/AuditLogsPage/route-pam.tsx
Normal file
19
frontend/src/pages/project/AuditLogsPage/route-pam.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { AuditLogsPage } from "./AuditLogsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs"
|
||||
)({
|
||||
component: AuditLogsPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Audit Logs"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId"
|
||||
)({
|
||||
component: GroupDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-management/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
search: {
|
||||
selectedTab: ProjectAccessControlTabs.Groups
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Group"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId"
|
||||
)({
|
||||
component: IdentityDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-management/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
search: {
|
||||
selectedTab: ProjectAccessControlTabs.Identities
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Machine Identity"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId"
|
||||
)({
|
||||
component: MemberDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-scanning/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
search: {
|
||||
selectedTab: ProjectAccessControlTabs.Member
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "User"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const PamAccountPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
return (
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.PamAccounts}
|
||||
position={position}
|
||||
selectOptions={[
|
||||
{ value: "resourceName", label: "Resource Name" },
|
||||
{ value: "accountName", label: "Account Name" },
|
||||
{ value: "accountPath", label: "Account Path" }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPamAccountActions,
|
||||
ProjectPermissionPamSessionActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiSyncActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
@@ -221,6 +223,18 @@ const SecretEventsPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretEventActions.SubscribeImportMutations]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const PamAccountPolicyActionSchema = z.object({
|
||||
[ProjectPermissionPamAccountActions.Access]: z.boolean().optional(),
|
||||
[ProjectPermissionPamAccountActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionPamAccountActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionPamAccountActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionPamAccountActions.Delete]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const PamSessionPolicyActionSchema = z.object({
|
||||
[ProjectPermissionPamSessionActions.Read]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretRollbackPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
@@ -406,7 +420,16 @@ export const projectRoleFormSchema = z.object({
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
.default([]),
|
||||
[ProjectPermissionSub.PamFolders]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PamResources]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PamAccounts]: PamAccountPolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.PamSessions]: PamSessionPolicyActionSchema.array().default([])
|
||||
})
|
||||
.partial()
|
||||
.optional()
|
||||
@@ -427,7 +450,8 @@ type TConditionalFields =
|
||||
| ProjectPermissionSub.SecretSyncs
|
||||
| ProjectPermissionSub.PkiSyncs
|
||||
| ProjectPermissionSub.SecretEvents
|
||||
| ProjectPermissionSub.AppConnections;
|
||||
| ProjectPermissionSub.AppConnections
|
||||
| ProjectPermissionSub.PamAccounts;
|
||||
|
||||
export const isConditionalSubjects = (
|
||||
subject: ProjectPermissionSub
|
||||
@@ -444,7 +468,8 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.SecretSyncs ||
|
||||
subject === ProjectPermissionSub.PkiSyncs ||
|
||||
subject === ProjectPermissionSub.SecretEvents ||
|
||||
subject === ProjectPermissionSub.AppConnections;
|
||||
subject === ProjectPermissionSub.AppConnections ||
|
||||
subject === ProjectPermissionSub.PamAccounts;
|
||||
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||
@@ -553,7 +578,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.SecretSyncs,
|
||||
ProjectPermissionSub.PkiSyncs,
|
||||
ProjectPermissionSub.SecretEvents,
|
||||
ProjectPermissionSub.AppConnections
|
||||
ProjectPermissionSub.AppConnections,
|
||||
ProjectPermissionSub.PamFolders,
|
||||
ProjectPermissionSub.PamResources
|
||||
].includes(subject)
|
||||
) {
|
||||
// from above statement we are sure it won't be undefined
|
||||
@@ -1112,6 +1139,40 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
inverted
|
||||
});
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PamAccounts) {
|
||||
if (!formVal[subject]) formVal[subject] = [];
|
||||
|
||||
formVal[subject].push({
|
||||
[ProjectPermissionPamAccountActions.Access]: action.includes(
|
||||
ProjectPermissionPamAccountActions.Access
|
||||
),
|
||||
[ProjectPermissionPamAccountActions.Create]: action.includes(
|
||||
ProjectPermissionPamAccountActions.Create
|
||||
),
|
||||
[ProjectPermissionPamAccountActions.Delete]: action.includes(
|
||||
ProjectPermissionPamAccountActions.Delete
|
||||
),
|
||||
[ProjectPermissionPamAccountActions.Edit]: action.includes(
|
||||
ProjectPermissionPamAccountActions.Edit
|
||||
),
|
||||
[ProjectPermissionPamAccountActions.Read]: action.includes(
|
||||
ProjectPermissionPamAccountActions.Read
|
||||
),
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PamSessions) {
|
||||
const canRead = action.includes(ProjectPermissionPamSessionActions.Read);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// Map actions to the keys defined in ApprovalPolicyActionSchema
|
||||
if (canRead) formVal[subject]![0][ProjectPermissionPamAccountActions.Read] = true;
|
||||
}
|
||||
});
|
||||
|
||||
return formVal;
|
||||
@@ -1731,6 +1792,38 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
value: ProjectPermissionAppConnectionActions.Connect
|
||||
}
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PamFolders]: {
|
||||
title: "Folders",
|
||||
actions: [
|
||||
{ label: "Read", value: ProjectPermissionActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionActions.Delete }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PamResources]: {
|
||||
title: "Resources",
|
||||
actions: [
|
||||
{ label: "Read", value: ProjectPermissionActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionActions.Delete }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PamAccounts]: {
|
||||
title: "Accounts",
|
||||
actions: [
|
||||
{ label: "Access", value: ProjectPermissionPamAccountActions.Access },
|
||||
{ label: "Read", value: ProjectPermissionPamAccountActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionPamAccountActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionPamAccountActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionPamAccountActions.Delete }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PamSessions]: {
|
||||
title: "Sessions",
|
||||
actions: [{ label: "Read", value: ProjectPermissionPamSessionActions.Read }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1793,6 +1886,13 @@ const SecretScanningSubject = (enabled = false) => ({
|
||||
[ProjectPermissionSub.SecretScanningConfigs]: enabled
|
||||
});
|
||||
|
||||
const PamPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.PamFolders]: enabled,
|
||||
[ProjectPermissionSub.PamResources]: enabled,
|
||||
[ProjectPermissionSub.PamAccounts]: enabled,
|
||||
[ProjectPermissionSub.PamSessions]: enabled
|
||||
});
|
||||
|
||||
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
|
||||
export const ProjectTypePermissionSubjects: Record<
|
||||
ProjectType,
|
||||
@@ -1805,6 +1905,7 @@ export const ProjectTypePermissionSubjects: Record<
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects(),
|
||||
...SecretScanningSubject(),
|
||||
...PamPermissionSubjects(),
|
||||
[ProjectPermissionSub.AppConnections]: true
|
||||
},
|
||||
[ProjectType.KMS]: {
|
||||
@@ -1814,6 +1915,7 @@ export const ProjectTypePermissionSubjects: Record<
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects(),
|
||||
...SecretScanningSubject(),
|
||||
...PamPermissionSubjects(),
|
||||
[ProjectPermissionSub.AppConnections]: false
|
||||
},
|
||||
[ProjectType.CertificateManager]: {
|
||||
@@ -1823,6 +1925,7 @@ export const ProjectTypePermissionSubjects: Record<
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects(),
|
||||
...SecretScanningSubject(),
|
||||
...PamPermissionSubjects(),
|
||||
[ProjectPermissionSub.AppConnections]: true
|
||||
},
|
||||
[ProjectType.SSH]: {
|
||||
@@ -1832,6 +1935,7 @@ export const ProjectTypePermissionSubjects: Record<
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...SecretScanningSubject(),
|
||||
...PamPermissionSubjects(),
|
||||
[ProjectPermissionSub.AppConnections]: false
|
||||
},
|
||||
[ProjectType.SecretScanning]: {
|
||||
@@ -1841,7 +1945,18 @@ export const ProjectTypePermissionSubjects: Record<
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...PamPermissionSubjects(),
|
||||
[ProjectPermissionSub.AppConnections]: true
|
||||
},
|
||||
[ProjectType.PAM]: {
|
||||
...SharedPermissionSubjects,
|
||||
...SecretScanningSubject(),
|
||||
...SshPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...PamPermissionSubjects(true),
|
||||
[ProjectPermissionSub.AppConnections]: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2245,5 +2360,65 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
}
|
||||
])
|
||||
],
|
||||
[ProjectType.PAM]: [
|
||||
{
|
||||
id: "pam-viewer",
|
||||
name: "PAM Viewing Policies",
|
||||
description: "Grants read access to PAM accounts and resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.PamFolders,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PamResources,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PamAccounts,
|
||||
actions: [ProjectPermissionPamAccountActions.Read]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "pam-accessor",
|
||||
name: "PAM Accessing Policies",
|
||||
description: "Grants the right to access all PAM accounts",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.PamAccounts,
|
||||
actions: [
|
||||
ProjectPermissionPamAccountActions.Access,
|
||||
ProjectPermissionPamAccountActions.Read
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "pam-editor",
|
||||
name: "PAM Editing Policies",
|
||||
description: "Grants read and edit access to PAM accounts and resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.PamFolders,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PamResources,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PamAccounts,
|
||||
actions: [
|
||||
ProjectPermissionPamAccountActions.Read,
|
||||
ProjectPermissionPamAccountActions.Edit,
|
||||
ProjectPermissionPamAccountActions.Create,
|
||||
ProjectPermissionPamAccountActions.Delete
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
]
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionCond
|
||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||
import { PamAccountPermissionConditions } from "./PamAccountPermissionConditions";
|
||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
|
||||
import { PkiSyncPermissionConditions } from "./PkiSyncPermissionConditions";
|
||||
@@ -87,6 +88,10 @@ export const renderConditionalComponents = (
|
||||
return <AppConnectionPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PamAccounts) {
|
||||
return <PamAccountPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug"
|
||||
)({
|
||||
component: RoleDetailsBySlugPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-management/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
search: {
|
||||
selectedTab: ProjectAccessControlTabs.Roles
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Roles"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -72,7 +72,10 @@ import {
|
||||
useMoveSecrets,
|
||||
useUpdateSecretBatch
|
||||
} from "@app/hooks/api";
|
||||
import { dashboardKeys, fetchDashboardProjectSecretsByKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchDashboardProjectSecretsByKeys
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
@@ -67,6 +67,7 @@ import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from
|
||||
import { Route as sshLayoutImport } from './pages/ssh/layout'
|
||||
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
|
||||
import { Route as secretManagerLayoutImport } from './pages/secret-manager/layout'
|
||||
import { Route as pamLayoutImport } from './pages/pam/layout'
|
||||
import { Route as kmsLayoutImport } from './pages/kms/layout'
|
||||
import { Route as certManagerLayoutImport } from './pages/cert-manager/layout'
|
||||
import { Route as secretManagerIntegrationsRouteVercelOauthRedirectImport } from './pages/secret-manager/integrations/route-vercel-oauth-redirect'
|
||||
@@ -87,6 +88,8 @@ import { Route as projectAccessControlPageRouteSecretScanningImport } from './pa
|
||||
import { Route as projectAuditLogsPageRouteSecretManagerImport } from './pages/project/AuditLogsPage/route-secret-manager'
|
||||
import { Route as projectAppConnectionsPageRouteSecretManagerImport } from './pages/project/AppConnectionsPage/route-secret-manager'
|
||||
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
|
||||
import { Route as projectAuditLogsPageRoutePamImport } from './pages/project/AuditLogsPage/route-pam'
|
||||
import { Route as projectAccessControlPageRoutePamImport } from './pages/project/AccessControlPage/route-pam'
|
||||
import { Route as projectAuditLogsPageRouteKmsImport } from './pages/project/AuditLogsPage/route-kms'
|
||||
import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms'
|
||||
import { Route as projectAuditLogsPageRouteCertManagerImport } from './pages/project/AuditLogsPage/route-cert-manager'
|
||||
@@ -103,6 +106,9 @@ import { Route as secretManagerSecretRotationPageRouteImport } from './pages/sec
|
||||
import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route'
|
||||
import { Route as secretManagerSecretApprovalsPageRouteImport } from './pages/secret-manager/SecretApprovalsPage/route'
|
||||
import { Route as secretManagerIPAllowlistPageRouteImport } from './pages/secret-manager/IPAllowlistPage/route'
|
||||
import { Route as pamSettingsPageRouteImport } from './pages/pam/SettingsPage/route'
|
||||
import { Route as pamPamResourcesPageRouteImport } from './pages/pam/PamResourcesPage/route'
|
||||
import { Route as pamPamAccountsPageRouteImport } from './pages/pam/PamAccountsPage/route'
|
||||
import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/route'
|
||||
import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route'
|
||||
import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route'
|
||||
@@ -123,6 +129,10 @@ import { Route as projectRoleDetailsBySlugPageRouteSecretManagerImport } from '.
|
||||
import { Route as projectMemberDetailsByIDPageRouteSecretManagerImport } from './pages/project/MemberDetailsByIDPage/route-secret-manager'
|
||||
import { Route as projectIdentityDetailsByIDPageRouteSecretManagerImport } from './pages/project/IdentityDetailsByIDPage/route-secret-manager'
|
||||
import { Route as projectGroupDetailsByIDPageRouteSecretManagerImport } from './pages/project/GroupDetailsByIDPage/route-secret-manager'
|
||||
import { Route as projectRoleDetailsBySlugPageRoutePamImport } from './pages/project/RoleDetailsBySlugPage/route-pam'
|
||||
import { Route as projectMemberDetailsByIDPageRoutePamImport } from './pages/project/MemberDetailsByIDPage/route-pam'
|
||||
import { Route as projectIdentityDetailsByIDPageRoutePamImport } from './pages/project/IdentityDetailsByIDPage/route-pam'
|
||||
import { Route as projectGroupDetailsByIDPageRoutePamImport } from './pages/project/GroupDetailsByIDPage/route-pam'
|
||||
import { Route as projectRoleDetailsBySlugPageRouteKmsImport } from './pages/project/RoleDetailsBySlugPage/route-kms'
|
||||
import { Route as projectMemberDetailsByIDPageRouteKmsImport } from './pages/project/MemberDetailsByIDPage/route-kms'
|
||||
import { Route as projectIdentityDetailsByIDPageRouteKmsImport } from './pages/project/IdentityDetailsByIDPage/route-kms'
|
||||
@@ -137,11 +147,13 @@ import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/
|
||||
import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route'
|
||||
import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route'
|
||||
import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route'
|
||||
import { Route as pamPamSessionsByIDPageRouteImport } from './pages/pam/PamSessionsByIDPage/route'
|
||||
import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSubscriberDetailsByIDPage/route'
|
||||
import { Route as certManagerPkiSyncDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSyncDetailsByIDPage/route'
|
||||
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
|
||||
import { Route as secretScanningSecretScanningDataSourcesPageRouteImport } from './pages/secret-scanning/SecretScanningDataSourcesPage/route'
|
||||
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route'
|
||||
import { Route as pamPamSessionsPageRouteImport } from './pages/pam/PamSessionsPage/route'
|
||||
import { Route as certManagerPkiSubscribersPageRouteImport } from './pages/cert-manager/PkiSubscribersPage/route'
|
||||
import { Route as certManagerIntegrationsListPageRouteImport } from './pages/cert-manager/IntegrationsListPage/route'
|
||||
import { Route as certManagerPkiTemplateListPageRouteImport } from './pages/cert-manager/PkiTemplateListPage/route'
|
||||
@@ -277,6 +289,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdImpo
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId',
|
||||
@@ -293,6 +309,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecr
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers',
|
||||
@@ -692,6 +712,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRout
|
||||
} as any,
|
||||
)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport.update({
|
||||
id: '/projects/pam/$projectId',
|
||||
path: '/projects/pam/$projectId',
|
||||
getParentRoute: () => organizationLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport.update({
|
||||
id: '/projects/kms/$projectId',
|
||||
@@ -805,6 +832,12 @@ const secretManagerLayoutRoute = secretManagerLayoutImport.update({
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRoute,
|
||||
} as any)
|
||||
|
||||
const pamLayoutRoute = pamLayoutImport.update({
|
||||
id: '/_pam-layout',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute,
|
||||
} as any)
|
||||
|
||||
const kmsLayoutRoute = kmsLayoutImport.update({
|
||||
id: '/_kms-layout',
|
||||
getParentRoute: () =>
|
||||
@@ -973,6 +1006,29 @@ const projectAccessControlPageRouteSecretManagerRoute =
|
||||
getParentRoute: () => secretManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport.update(
|
||||
{
|
||||
id: '/sessions',
|
||||
path: '/sessions',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any,
|
||||
)
|
||||
|
||||
const projectAuditLogsPageRoutePamRoute =
|
||||
projectAuditLogsPageRoutePamImport.update({
|
||||
id: '/audit-logs',
|
||||
path: '/audit-logs',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectAccessControlPageRoutePamRoute =
|
||||
projectAccessControlPageRoutePamImport.update({
|
||||
id: '/access-management',
|
||||
path: '/access-management',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectAuditLogsPageRouteKmsRoute =
|
||||
projectAuditLogsPageRouteKmsImport.update({
|
||||
id: '/audit-logs',
|
||||
@@ -1108,6 +1164,24 @@ const secretManagerIPAllowlistPageRouteRoute =
|
||||
getParentRoute: () => secretManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const pamSettingsPageRouteRoute = pamSettingsPageRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const pamPamResourcesPageRouteRoute = pamPamResourcesPageRouteImport.update({
|
||||
id: '/resources',
|
||||
path: '/resources',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const pamPamAccountsPageRouteRoute = pamPamAccountsPageRouteImport.update({
|
||||
id: '/accounts',
|
||||
path: '/accounts',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const kmsSettingsPageRouteRoute = kmsSettingsPageRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
@@ -1246,6 +1320,34 @@ const projectGroupDetailsByIDPageRouteSecretManagerRoute =
|
||||
getParentRoute: () => secretManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectRoleDetailsBySlugPageRoutePamRoute =
|
||||
projectRoleDetailsBySlugPageRoutePamImport.update({
|
||||
id: '/roles/$roleSlug',
|
||||
path: '/roles/$roleSlug',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectMemberDetailsByIDPageRoutePamRoute =
|
||||
projectMemberDetailsByIDPageRoutePamImport.update({
|
||||
id: '/members/$membershipId',
|
||||
path: '/members/$membershipId',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectIdentityDetailsByIDPageRoutePamRoute =
|
||||
projectIdentityDetailsByIDPageRoutePamImport.update({
|
||||
id: '/identities/$identityId',
|
||||
path: '/identities/$identityId',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectGroupDetailsByIDPageRoutePamRoute =
|
||||
projectGroupDetailsByIDPageRoutePamImport.update({
|
||||
id: '/groups/$groupId',
|
||||
path: '/groups/$groupId',
|
||||
getParentRoute: () => pamLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectRoleDetailsBySlugPageRouteKmsRoute =
|
||||
projectRoleDetailsBySlugPageRouteKmsImport.update({
|
||||
id: '/roles/$roleSlug',
|
||||
@@ -1345,6 +1447,14 @@ const secretManagerIntegrationsDetailsByIDPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute,
|
||||
} as any)
|
||||
|
||||
const pamPamSessionsByIDPageRouteRoute =
|
||||
pamPamSessionsByIDPageRouteImport.update({
|
||||
id: '/$sessionId',
|
||||
path: '/$sessionId',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute,
|
||||
} as any)
|
||||
|
||||
const certManagerPkiSubscriberDetailsByIDPageRouteRoute =
|
||||
certManagerPkiSubscriberDetailsByIDPageRouteImport.update({
|
||||
id: '/$subscriberName',
|
||||
@@ -1384,6 +1494,13 @@ const secretManagerIntegrationsListPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute,
|
||||
} as any)
|
||||
|
||||
const pamPamSessionsPageRouteRoute = pamPamSessionsPageRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute,
|
||||
} as any)
|
||||
|
||||
const certManagerPkiSubscribersPageRouteRoute =
|
||||
certManagerPkiSubscribersPageRouteImport.update({
|
||||
id: '/',
|
||||
@@ -2512,6 +2629,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport
|
||||
parentRoute: typeof organizationLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId'
|
||||
path: '/projects/pam/$projectId'
|
||||
fullPath: '/projects/pam/$projectId'
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport
|
||||
parentRoute: typeof organizationLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId'
|
||||
path: '/projects/secret-management/$projectId'
|
||||
@@ -2624,6 +2748,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof kmsLayoutImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout'
|
||||
path: ''
|
||||
fullPath: '/projects/pam/$projectId'
|
||||
preLoaderRoute: typeof pamLayoutImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout'
|
||||
path: ''
|
||||
@@ -2701,6 +2832,27 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof kmsSettingsPageRouteImport
|
||||
parentRoute: typeof kmsLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts'
|
||||
path: '/accounts'
|
||||
fullPath: '/projects/pam/$projectId/accounts'
|
||||
preLoaderRoute: typeof pamPamAccountsPageRouteImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources'
|
||||
path: '/resources'
|
||||
fullPath: '/projects/pam/$projectId/resources'
|
||||
preLoaderRoute: typeof pamPamResourcesPageRouteImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings'
|
||||
path: '/settings'
|
||||
fullPath: '/projects/pam/$projectId/settings'
|
||||
preLoaderRoute: typeof pamSettingsPageRouteImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist'
|
||||
path: '/allowlist'
|
||||
@@ -2834,6 +2986,27 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof projectAuditLogsPageRouteKmsImport
|
||||
parentRoute: typeof kmsLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management'
|
||||
path: '/access-management'
|
||||
fullPath: '/projects/pam/$projectId/access-management'
|
||||
preLoaderRoute: typeof projectAccessControlPageRoutePamImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs'
|
||||
path: '/audit-logs'
|
||||
fullPath: '/projects/pam/$projectId/audit-logs'
|
||||
preLoaderRoute: typeof projectAuditLogsPageRoutePamImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions'
|
||||
path: '/sessions'
|
||||
fullPath: '/projects/pam/$projectId/sessions'
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management'
|
||||
path: '/access-management'
|
||||
@@ -2925,6 +3098,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof certManagerPkiSubscribersPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/'
|
||||
path: '/'
|
||||
fullPath: '/projects/pam/$projectId/sessions/'
|
||||
preLoaderRoute: typeof pamPamSessionsPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/'
|
||||
path: '/'
|
||||
@@ -2960,6 +3140,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId'
|
||||
path: '/$sessionId'
|
||||
fullPath: '/projects/pam/$projectId/sessions/$sessionId'
|
||||
preLoaderRoute: typeof pamPamSessionsByIDPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId'
|
||||
path: '/$integrationId'
|
||||
@@ -3058,6 +3245,34 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteKmsImport
|
||||
parentRoute: typeof kmsLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId'
|
||||
path: '/groups/$groupId'
|
||||
fullPath: '/projects/pam/$projectId/groups/$groupId'
|
||||
preLoaderRoute: typeof projectGroupDetailsByIDPageRoutePamImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId'
|
||||
path: '/identities/$identityId'
|
||||
fullPath: '/projects/pam/$projectId/identities/$identityId'
|
||||
preLoaderRoute: typeof projectIdentityDetailsByIDPageRoutePamImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId'
|
||||
path: '/members/$membershipId'
|
||||
fullPath: '/projects/pam/$projectId/members/$membershipId'
|
||||
preLoaderRoute: typeof projectMemberDetailsByIDPageRoutePamImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug'
|
||||
path: '/roles/$roleSlug'
|
||||
fullPath: '/projects/pam/$projectId/roles/$roleSlug'
|
||||
preLoaderRoute: typeof projectRoleDetailsBySlugPageRoutePamImport
|
||||
parentRoute: typeof pamLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId'
|
||||
path: '/groups/$groupId'
|
||||
@@ -4044,6 +4259,71 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren {
|
||||
pamPamSessionsPageRouteRoute: typeof pamPamSessionsPageRouteRoute
|
||||
pamPamSessionsByIDPageRouteRoute: typeof pamPamSessionsByIDPageRouteRoute
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren =
|
||||
{
|
||||
pamPamSessionsPageRouteRoute: pamPamSessionsPageRouteRoute,
|
||||
pamPamSessionsByIDPageRouteRoute: pamPamSessionsByIDPageRouteRoute,
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute._addFileChildren(
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren,
|
||||
)
|
||||
|
||||
interface pamLayoutRouteChildren {
|
||||
pamPamAccountsPageRouteRoute: typeof pamPamAccountsPageRouteRoute
|
||||
pamPamResourcesPageRouteRoute: typeof pamPamResourcesPageRouteRoute
|
||||
pamSettingsPageRouteRoute: typeof pamSettingsPageRouteRoute
|
||||
projectAccessControlPageRoutePamRoute: typeof projectAccessControlPageRoutePamRoute
|
||||
projectAuditLogsPageRoutePamRoute: typeof projectAuditLogsPageRoutePamRoute
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren
|
||||
projectGroupDetailsByIDPageRoutePamRoute: typeof projectGroupDetailsByIDPageRoutePamRoute
|
||||
projectIdentityDetailsByIDPageRoutePamRoute: typeof projectIdentityDetailsByIDPageRoutePamRoute
|
||||
projectMemberDetailsByIDPageRoutePamRoute: typeof projectMemberDetailsByIDPageRoutePamRoute
|
||||
projectRoleDetailsBySlugPageRoutePamRoute: typeof projectRoleDetailsBySlugPageRoutePamRoute
|
||||
}
|
||||
|
||||
const pamLayoutRouteChildren: pamLayoutRouteChildren = {
|
||||
pamPamAccountsPageRouteRoute: pamPamAccountsPageRouteRoute,
|
||||
pamPamResourcesPageRouteRoute: pamPamResourcesPageRouteRoute,
|
||||
pamSettingsPageRouteRoute: pamSettingsPageRouteRoute,
|
||||
projectAccessControlPageRoutePamRoute: projectAccessControlPageRoutePamRoute,
|
||||
projectAuditLogsPageRoutePamRoute: projectAuditLogsPageRoutePamRoute,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren,
|
||||
projectGroupDetailsByIDPageRoutePamRoute:
|
||||
projectGroupDetailsByIDPageRoutePamRoute,
|
||||
projectIdentityDetailsByIDPageRoutePamRoute:
|
||||
projectIdentityDetailsByIDPageRoutePamRoute,
|
||||
projectMemberDetailsByIDPageRoutePamRoute:
|
||||
projectMemberDetailsByIDPageRoutePamRoute,
|
||||
projectRoleDetailsBySlugPageRoutePamRoute:
|
||||
projectRoleDetailsBySlugPageRoutePamRoute,
|
||||
}
|
||||
|
||||
const pamLayoutRouteWithChildren = pamLayoutRoute._addFileChildren(
|
||||
pamLayoutRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren {
|
||||
pamLayoutRoute: typeof pamLayoutRouteWithChildren
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren =
|
||||
{
|
||||
pamLayoutRoute: pamLayoutRouteWithChildren,
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute._addFileChildren(
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteChildren {
|
||||
secretManagerIntegrationsListPageRouteRoute: typeof secretManagerIntegrationsListPageRouteRoute
|
||||
secretManagerIntegrationsDetailsByIDPageRouteRoute: typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
@@ -4520,6 +4800,7 @@ interface organizationLayoutRouteChildren {
|
||||
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSshProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSshProjectIdRouteWithChildren
|
||||
@@ -4536,6 +4817,8 @@ const organizationLayoutRouteChildren: organizationLayoutRouteChildren = {
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRoute:
|
||||
@@ -4781,6 +5064,7 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute
|
||||
'/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren
|
||||
'/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren
|
||||
'/projects/pam/$projectId': typeof pamLayoutRouteWithChildren
|
||||
'/projects/secret-management/$projectId': typeof secretManagerLayoutRouteWithChildren
|
||||
'/projects/secret-scanning/$projectId': typeof secretScanningLayoutRouteWithChildren
|
||||
'/projects/ssh/$projectId': typeof sshLayoutRouteWithChildren
|
||||
@@ -4803,6 +5087,9 @@ export interface FileRoutesByFullPath {
|
||||
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
||||
'/projects/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
|
||||
'/projects/pam/$projectId/accounts': typeof pamPamAccountsPageRouteRoute
|
||||
'/projects/pam/$projectId/resources': typeof pamPamResourcesPageRouteRoute
|
||||
'/projects/pam/$projectId/settings': typeof pamSettingsPageRouteRoute
|
||||
'/projects/secret-management/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
|
||||
'/projects/secret-management/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
|
||||
'/projects/secret-management/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
|
||||
@@ -4822,6 +5109,9 @@ export interface FileRoutesByFullPath {
|
||||
'/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
|
||||
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
|
||||
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
|
||||
'/projects/pam/$projectId/access-management': typeof projectAccessControlPageRoutePamRoute
|
||||
'/projects/pam/$projectId/audit-logs': typeof projectAuditLogsPageRoutePamRoute
|
||||
'/projects/pam/$projectId/sessions': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren
|
||||
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
|
||||
@@ -4835,11 +5125,13 @@ export interface FileRoutesByFullPath {
|
||||
'/projects/cert-management/$projectId/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
|
||||
'/projects/cert-management/$projectId/integrations/': typeof certManagerIntegrationsListPageRouteRoute
|
||||
'/projects/cert-management/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
|
||||
'/projects/pam/$projectId/sessions/': typeof pamPamSessionsPageRouteRoute
|
||||
'/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/projects/secret-scanning/$projectId/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
||||
'/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||
'/projects/pam/$projectId/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute
|
||||
'/projects/secret-management/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
'/projects/secret-management/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||
'/projects/secret-management/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
@@ -4854,6 +5146,10 @@ export interface FileRoutesByFullPath {
|
||||
'/projects/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute
|
||||
'/projects/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute
|
||||
'/projects/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute
|
||||
'/projects/pam/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRoutePamRoute
|
||||
'/projects/pam/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRoutePamRoute
|
||||
'/projects/pam/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRoutePamRoute
|
||||
'/projects/pam/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRoutePamRoute
|
||||
'/projects/secret-management/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
||||
@@ -5000,6 +5296,7 @@ export interface FileRoutesByTo {
|
||||
'/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute
|
||||
'/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren
|
||||
'/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren
|
||||
'/projects/pam/$projectId': typeof pamLayoutRouteWithChildren
|
||||
'/projects/secret-management/$projectId': typeof secretManagerLayoutRouteWithChildren
|
||||
'/projects/secret-scanning/$projectId': typeof secretScanningLayoutRouteWithChildren
|
||||
'/projects/ssh/$projectId': typeof sshLayoutRouteWithChildren
|
||||
@@ -5022,6 +5319,9 @@ export interface FileRoutesByTo {
|
||||
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
||||
'/projects/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
|
||||
'/projects/pam/$projectId/accounts': typeof pamPamAccountsPageRouteRoute
|
||||
'/projects/pam/$projectId/resources': typeof pamPamResourcesPageRouteRoute
|
||||
'/projects/pam/$projectId/settings': typeof pamSettingsPageRouteRoute
|
||||
'/projects/secret-management/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
|
||||
'/projects/secret-management/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
|
||||
'/projects/secret-management/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
|
||||
@@ -5038,6 +5338,8 @@ export interface FileRoutesByTo {
|
||||
'/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
|
||||
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
|
||||
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
|
||||
'/projects/pam/$projectId/access-management': typeof projectAccessControlPageRoutePamRoute
|
||||
'/projects/pam/$projectId/audit-logs': typeof projectAuditLogsPageRoutePamRoute
|
||||
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
|
||||
@@ -5049,11 +5351,13 @@ export interface FileRoutesByTo {
|
||||
'/projects/cert-management/$projectId/certificate-templates': typeof certManagerPkiTemplateListPageRouteRoute
|
||||
'/projects/cert-management/$projectId/integrations': typeof certManagerIntegrationsListPageRouteRoute
|
||||
'/projects/cert-management/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute
|
||||
'/projects/pam/$projectId/sessions': typeof pamPamSessionsPageRouteRoute
|
||||
'/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/projects/secret-scanning/$projectId/data-sources': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
||||
'/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||
'/projects/pam/$projectId/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute
|
||||
'/projects/secret-management/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
'/projects/secret-management/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||
'/projects/secret-management/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
@@ -5068,6 +5372,10 @@ export interface FileRoutesByTo {
|
||||
'/projects/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute
|
||||
'/projects/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute
|
||||
'/projects/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute
|
||||
'/projects/pam/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRoutePamRoute
|
||||
'/projects/pam/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRoutePamRoute
|
||||
'/projects/pam/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRoutePamRoute
|
||||
'/projects/pam/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRoutePamRoute
|
||||
'/projects/secret-management/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
||||
'/projects/secret-management/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
||||
@@ -5226,6 +5534,7 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': typeof adminResourceOverviewPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSshProjectIdRouteWithChildren
|
||||
@@ -5242,6 +5551,7 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsRouteVercelOauthRedirectRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout': typeof certManagerLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout': typeof kmsLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout': typeof pamLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout': typeof secretManagerLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout': typeof secretScanningLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren
|
||||
@@ -5253,6 +5563,9 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings': typeof kmsSettingsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts': typeof pamPamAccountsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources': typeof pamPamResourcesPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings': typeof pamSettingsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval': typeof secretManagerSecretApprovalsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute
|
||||
@@ -5272,6 +5585,9 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management': typeof projectAccessControlPageRoutePamRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs': typeof projectAuditLogsPageRoutePamRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
|
||||
@@ -5285,11 +5601,13 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/': typeof certManagerIntegrationsListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/': typeof pamPamSessionsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
@@ -5304,6 +5622,10 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRoutePamRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRoutePamRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRoutePamRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRoutePamRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
||||
@@ -5460,6 +5782,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/resources/overview'
|
||||
| '/projects/cert-management/$projectId'
|
||||
| '/projects/kms/$projectId'
|
||||
| '/projects/pam/$projectId'
|
||||
| '/projects/secret-management/$projectId'
|
||||
| '/projects/secret-scanning/$projectId'
|
||||
| '/projects/ssh/$projectId'
|
||||
@@ -5482,6 +5805,9 @@ export interface FileRouteTypes {
|
||||
| '/projects/kms/$projectId/kmip'
|
||||
| '/projects/kms/$projectId/overview'
|
||||
| '/projects/kms/$projectId/settings'
|
||||
| '/projects/pam/$projectId/accounts'
|
||||
| '/projects/pam/$projectId/resources'
|
||||
| '/projects/pam/$projectId/settings'
|
||||
| '/projects/secret-management/$projectId/allowlist'
|
||||
| '/projects/secret-management/$projectId/approval'
|
||||
| '/projects/secret-management/$projectId/overview'
|
||||
@@ -5501,6 +5827,9 @@ export interface FileRouteTypes {
|
||||
| '/projects/cert-management/$projectId/subscribers'
|
||||
| '/projects/kms/$projectId/access-management'
|
||||
| '/projects/kms/$projectId/audit-logs'
|
||||
| '/projects/pam/$projectId/access-management'
|
||||
| '/projects/pam/$projectId/audit-logs'
|
||||
| '/projects/pam/$projectId/sessions'
|
||||
| '/projects/secret-management/$projectId/access-management'
|
||||
| '/projects/secret-management/$projectId/app-connections'
|
||||
| '/projects/secret-management/$projectId/audit-logs'
|
||||
@@ -5514,11 +5843,13 @@ export interface FileRouteTypes {
|
||||
| '/projects/cert-management/$projectId/certificate-templates/'
|
||||
| '/projects/cert-management/$projectId/integrations/'
|
||||
| '/projects/cert-management/$projectId/subscribers/'
|
||||
| '/projects/pam/$projectId/sessions/'
|
||||
| '/projects/secret-management/$projectId/integrations/'
|
||||
| '/projects/secret-scanning/$projectId/data-sources/'
|
||||
| '/projects/cert-management/$projectId/ca/$caName'
|
||||
| '/projects/cert-management/$projectId/integrations/$syncId'
|
||||
| '/projects/cert-management/$projectId/subscribers/$subscriberName'
|
||||
| '/projects/pam/$projectId/sessions/$sessionId'
|
||||
| '/projects/secret-management/$projectId/integrations/$integrationId'
|
||||
| '/projects/secret-management/$projectId/integrations/select-integration-auth'
|
||||
| '/projects/secret-management/$projectId/secrets/$envSlug'
|
||||
@@ -5533,6 +5864,10 @@ export interface FileRouteTypes {
|
||||
| '/projects/kms/$projectId/identities/$identityId'
|
||||
| '/projects/kms/$projectId/members/$membershipId'
|
||||
| '/projects/kms/$projectId/roles/$roleSlug'
|
||||
| '/projects/pam/$projectId/groups/$groupId'
|
||||
| '/projects/pam/$projectId/identities/$identityId'
|
||||
| '/projects/pam/$projectId/members/$membershipId'
|
||||
| '/projects/pam/$projectId/roles/$roleSlug'
|
||||
| '/projects/secret-management/$projectId/groups/$groupId'
|
||||
| '/projects/secret-management/$projectId/identities/$identityId'
|
||||
| '/projects/secret-management/$projectId/members/$membershipId'
|
||||
@@ -5678,6 +6013,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/resources/overview'
|
||||
| '/projects/cert-management/$projectId'
|
||||
| '/projects/kms/$projectId'
|
||||
| '/projects/pam/$projectId'
|
||||
| '/projects/secret-management/$projectId'
|
||||
| '/projects/secret-scanning/$projectId'
|
||||
| '/projects/ssh/$projectId'
|
||||
@@ -5700,6 +6036,9 @@ export interface FileRouteTypes {
|
||||
| '/projects/kms/$projectId/kmip'
|
||||
| '/projects/kms/$projectId/overview'
|
||||
| '/projects/kms/$projectId/settings'
|
||||
| '/projects/pam/$projectId/accounts'
|
||||
| '/projects/pam/$projectId/resources'
|
||||
| '/projects/pam/$projectId/settings'
|
||||
| '/projects/secret-management/$projectId/allowlist'
|
||||
| '/projects/secret-management/$projectId/approval'
|
||||
| '/projects/secret-management/$projectId/overview'
|
||||
@@ -5716,6 +6055,8 @@ export interface FileRouteTypes {
|
||||
| '/projects/cert-management/$projectId/audit-logs'
|
||||
| '/projects/kms/$projectId/access-management'
|
||||
| '/projects/kms/$projectId/audit-logs'
|
||||
| '/projects/pam/$projectId/access-management'
|
||||
| '/projects/pam/$projectId/audit-logs'
|
||||
| '/projects/secret-management/$projectId/access-management'
|
||||
| '/projects/secret-management/$projectId/app-connections'
|
||||
| '/projects/secret-management/$projectId/audit-logs'
|
||||
@@ -5727,11 +6068,13 @@ export interface FileRouteTypes {
|
||||
| '/projects/cert-management/$projectId/certificate-templates'
|
||||
| '/projects/cert-management/$projectId/integrations'
|
||||
| '/projects/cert-management/$projectId/subscribers'
|
||||
| '/projects/pam/$projectId/sessions'
|
||||
| '/projects/secret-management/$projectId/integrations'
|
||||
| '/projects/secret-scanning/$projectId/data-sources'
|
||||
| '/projects/cert-management/$projectId/ca/$caName'
|
||||
| '/projects/cert-management/$projectId/integrations/$syncId'
|
||||
| '/projects/cert-management/$projectId/subscribers/$subscriberName'
|
||||
| '/projects/pam/$projectId/sessions/$sessionId'
|
||||
| '/projects/secret-management/$projectId/integrations/$integrationId'
|
||||
| '/projects/secret-management/$projectId/integrations/select-integration-auth'
|
||||
| '/projects/secret-management/$projectId/secrets/$envSlug'
|
||||
@@ -5746,6 +6089,10 @@ export interface FileRouteTypes {
|
||||
| '/projects/kms/$projectId/identities/$identityId'
|
||||
| '/projects/kms/$projectId/members/$membershipId'
|
||||
| '/projects/kms/$projectId/roles/$roleSlug'
|
||||
| '/projects/pam/$projectId/groups/$groupId'
|
||||
| '/projects/pam/$projectId/identities/$identityId'
|
||||
| '/projects/pam/$projectId/members/$membershipId'
|
||||
| '/projects/pam/$projectId/roles/$roleSlug'
|
||||
| '/projects/secret-management/$projectId/groups/$groupId'
|
||||
| '/projects/secret-management/$projectId/identities/$identityId'
|
||||
| '/projects/secret-management/$projectId/members/$membershipId'
|
||||
@@ -5902,6 +6249,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId'
|
||||
@@ -5918,6 +6266,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/integrations/vercel/oauth2/callback'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout'
|
||||
@@ -5929,6 +6278,9 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview'
|
||||
@@ -5948,6 +6300,9 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs'
|
||||
@@ -5961,11 +6316,13 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug'
|
||||
@@ -5980,6 +6337,10 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/identities/$identityId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/members/$membershipId'
|
||||
@@ -6260,6 +6621,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId"
|
||||
@@ -6474,6 +6836,13 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout",
|
||||
@@ -6576,6 +6945,22 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout": {
|
||||
"filePath": "pam/layout.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout": {
|
||||
"filePath": "secret-manager/layout.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId",
|
||||
@@ -6663,6 +7048,18 @@ export const routeTree = rootRoute
|
||||
"filePath": "kms/SettingsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts": {
|
||||
"filePath": "pam/PamAccountsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources": {
|
||||
"filePath": "pam/PamResourcesPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings": {
|
||||
"filePath": "pam/SettingsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist": {
|
||||
"filePath": "secret-manager/IPAllowlistPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
|
||||
@@ -6750,6 +7147,22 @@ export const routeTree = rootRoute
|
||||
"filePath": "project/AuditLogsPage/route-kms.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management": {
|
||||
"filePath": "project/AccessControlPage/route-pam.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs": {
|
||||
"filePath": "project/AuditLogsPage/route-pam.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management": {
|
||||
"filePath": "project/AccessControlPage/route-secret-manager.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
|
||||
@@ -6886,6 +7299,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "cert-manager/PkiSubscribersPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/": {
|
||||
"filePath": "pam/PamSessionsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/": {
|
||||
"filePath": "secret-manager/IntegrationsListPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations"
|
||||
@@ -6906,6 +7323,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId": {
|
||||
"filePath": "pam/PamSessionsByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId": {
|
||||
"filePath": "secret-manager/IntegrationsDetailsByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations"
|
||||
@@ -6962,6 +7383,22 @@ export const routeTree = rootRoute
|
||||
"filePath": "project/RoleDetailsBySlugPage/route-kms.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId": {
|
||||
"filePath": "project/GroupDetailsByIDPage/route-pam.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId": {
|
||||
"filePath": "project/IdentityDetailsByIDPage/route-pam.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId": {
|
||||
"filePath": "project/MemberDetailsByIDPage/route-pam.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug": {
|
||||
"filePath": "project/RoleDetailsBySlugPage/route-pam.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId": {
|
||||
"filePath": "project/GroupDetailsByIDPage/route-secret-manager.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
|
||||
|
||||
@@ -375,6 +375,26 @@ const secretScanningRoutes = route("/projects/secret-scanning/$projectId", [
|
||||
])
|
||||
]);
|
||||
|
||||
const pamRoutes = route("/projects/pam/$projectId", [
|
||||
layout("pam-layout", "pam/layout.tsx", [
|
||||
route("/accounts", "pam/PamAccountsPage/route.tsx"),
|
||||
route("/sessions", [
|
||||
index("pam/PamSessionsPage/route.tsx"),
|
||||
route("/$sessionId", "pam/PamSessionsByIDPage/route.tsx")
|
||||
]),
|
||||
route("/resources", "pam/PamResourcesPage/route.tsx"),
|
||||
route("/audit-logs", "project/AuditLogsPage/route-pam.tsx"),
|
||||
route("/settings", "pam/SettingsPage/route.tsx"),
|
||||
|
||||
// Access Management
|
||||
route("/access-management", "project/AccessControlPage/route-pam.tsx"),
|
||||
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-pam.tsx"),
|
||||
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-pam.tsx"),
|
||||
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-pam.tsx"),
|
||||
route("/groups/$groupId", "project/GroupDetailsByIDPage/route-pam.tsx")
|
||||
])
|
||||
]);
|
||||
|
||||
export const routes = rootRoute("root.tsx", [
|
||||
index("index.tsx"),
|
||||
route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"),
|
||||
@@ -420,7 +440,8 @@ export const routes = rootRoute("root.tsx", [
|
||||
certManagerRoutes,
|
||||
kmsRoutes,
|
||||
sshRoutes,
|
||||
secretScanningRoutes
|
||||
secretScanningRoutes,
|
||||
pamRoutes
|
||||
])
|
||||
])
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user