Merge branch 'heads/main' into daniel/vault-import

This commit is contained in:
Daniel Hougaard
2025-07-24 17:29:55 +04:00
697 changed files with 18099 additions and 7050 deletions
@@ -82,7 +82,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
});
navigate({
to: "/projects/$projectId/cert-manager/pki-collections/$collectionId",
to: "/projects/cert-management/$projectId/pki-collections/$collectionId",
params: {
projectId,
collectionId
@@ -21,7 +21,6 @@ import {
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspacePkiCollections } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -61,7 +60,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
key={`pki-collection-${pkiCollection.id}`}
onClick={() =>
navigate({
to: `/projects/$projectId/${ProjectType.CertificateManager}/pki-collections/$collectionId` as const,
to: "/projects/cert-management/$projectId/pki-collections/$collectionId",
params: {
projectId,
collectionId: pkiCollection.id
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { AlertingPage } from "./AlertingPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/alerting"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting"
)({
component: AlertingPage,
beforeLoad: ({ context }) => {
@@ -18,7 +18,6 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api";
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal";
@@ -71,7 +70,7 @@ const Page = () => {
handlePopUpClose("deleteCa");
navigate({
to: `/projects/$projectId/${ProjectType.CertificateManager}/certificate-authorities` as const,
to: "/projects/cert-management/$projectId/certificate-authorities",
params: {
projectId
}
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/ca/$caName"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName"
)({
component: CertAuthDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
{
label: "Certificate Authorities",
link: linkOptions({
to: "/projects/$projectId/cert-manager/certificate-authorities",
to: "/projects/cert-management/$projectId/certificate-authorities",
params: {
projectId: params.projectId
}
@@ -30,7 +30,6 @@ import {
getCaStatusBadgeVariant
} from "@app/hooks/api/ca/constants";
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -79,7 +78,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
key={`ca-${ca.id}`}
onClick={() =>
navigate({
to: `/projects/$projectId/${ProjectType.CertificateManager}/ca/$caName` as const,
to: "/projects/cert-management/$projectId/ca/$caName",
params: {
projectId: currentWorkspace.id,
caName: ca.name
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { CertificateAuthoritiesPage } from "./CertificateAuthoritiesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-authorities"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
)({
component: CertificateAuthoritiesPage,
beforeLoad: ({ context }) => {
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { CertificatesPage } from "./CertificatesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificates"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
)({
component: CertificatesPage,
beforeLoad: ({ context }) => {
@@ -19,7 +19,6 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api";
import { PkiItemType } from "@app/hooks/api/pkiCollections/constants";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { PkiCollectionModal } from "../AlertingPage/components/PkiCollectionModal";
@@ -57,7 +56,7 @@ export const PkiCollectionPage = () => {
});
handlePopUpClose("deletePkiCollection");
navigate({
to: `/projects/$projectId/${ProjectType.CertificateManager}/certificates` as const,
to: "/projects/cert-management/$projectId/certificates",
params: {
projectId
}
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/pki-collections/$collectionId"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId"
)({
component: PkiCollectionDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
{
label: "Certificate Collections",
link: linkOptions({
to: "/projects/$projectId/cert-manager/certificates",
to: "/projects/cert-management/$projectId/certificates",
params: {
projectId: params.projectId
}
@@ -22,7 +22,6 @@ import {
useWorkspace
} from "@app/context";
import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal";
@@ -61,7 +60,7 @@ const Page = () => {
handlePopUpClose("deletePkiSubscriber");
navigate({
to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers` as const,
to: "/projects/cert-management/$projectId/subscribers",
params: {
projectId
}
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/$subscriberName"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
)({
component: PkiSubscriberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
{
label: "Subscribers",
link: linkOptions({
to: "/projects/$projectId/cert-manager/subscribers",
to: "/projects/cert-management/$projectId/subscribers",
params: {
projectId: params.projectId
}
@@ -38,7 +38,6 @@ import {
PkiSubscriberStatus,
pkiSubscriberStatusToNameMap
} from "@app/hooks/api/pkiSubscriber/constants";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -76,7 +75,7 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => {
key={`pki-subscriber-${subscriber.id}`}
onClick={() =>
navigate({
to: `/projects/$projectId/${ProjectType.CertificateManager}/subscribers/$subscriberName` as const,
to: "/projects/cert-management/$projectId/subscribers/$subscriberName",
params: {
projectId: currentWorkspace.id,
subscriberName: subscriber.name
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { PkiSubscribersPage } from "./PkiSubscribersPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/subscribers/"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/"
)({
component: PkiSubscribersPage,
beforeLoad: ({ context }) => {
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { PkiTemplateListPage } from "./PkiTemplateListPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/certificate-templates/"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/"
)({
component: PkiTemplateListPage,
beforeLoad: ({ context }) => {
@@ -2,12 +2,13 @@ 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";
const tabs = [
{
name: "General",
key: "tab-project-general",
Component: () => <div className="text-white">Coming soon</div>
Component: ProjectGeneralTab
}
];
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout/settings"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings"
)({
component: SettingsPage,
beforeLoad: ({ context }) => {
+30 -2
View File
@@ -1,9 +1,37 @@
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from "@app/components/v2";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager/_cert-manager-layout"
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
)({
component: PkiManagerLayout
component: PkiManagerLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
project,
breadcrumbs: [
{
type: BreadcrumbTypes.Component,
component: ProjectSelect
}
]
};
}
});
+1 -1
View File
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { KmipPage } from "./KmipPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/kmip"
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip"
)({
component: KmipPage,
beforeLoad: ({ context }) => {
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/overview"
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview"
)({
component: OverviewPage,
beforeLoad: ({ context }) => {
@@ -2,12 +2,13 @@ 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";
const tabs = [
{
name: "General",
key: "tab-project-general",
Component: () => <div className="text-white">Coming soon...</div>
Component: ProjectGeneralTab
}
];
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout/settings"
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings"
)({
component: SettingsPage,
beforeLoad: ({ context }) => {
+30 -2
View File
@@ -1,9 +1,37 @@
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from "@app/components/v2";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { KmsLayout } from "@app/layouts/KmsLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/kms/_kms-layout"
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
)({
component: KmsLayout
component: KmsLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
project,
breadcrumbs: [
{
type: BreadcrumbTypes.Component,
component: ProjectSelect
}
]
};
}
});
@@ -105,7 +105,7 @@ export const OrgAdminProjects = withPermission(
<TBody>
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
{!isProjectsLoading &&
projects?.map(({ name, slug, createdAt, id, defaultProduct }) => (
projects?.map(({ name, slug, createdAt, id, type }) => (
<Tr key={`project-${id}`} className="group w-full">
<Td>{name}</Td>
<Td>{slug}</Td>
@@ -126,7 +126,7 @@ export const OrgAdminProjects = withPermission(
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleAccessProject(defaultProduct, id);
handleAccessProject(type, id);
}}
icon={<FontAwesomeIcon icon={faSignIn} />}
disabled={
@@ -21,6 +21,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
import { FlyioConnectionForm } from "./FlyioConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
@@ -33,6 +34,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { OktaConnectionForm } from "./OktaConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { RailwayConnectionForm } from "./RailwayConnectionForm";
@@ -149,6 +151,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <ChecklyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} />;
case AppConnection.DigitalOcean:
return <DigitalOceanConnectionForm onSubmit={onSubmit} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -253,6 +259,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.DigitalOcean:
return <DigitalOceanConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,136 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
DigitalOceanConnectionMethod,
TDigitalOceanConnection
} from "@app/hooks/api/appConnections/types/digital-ocean";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TDigitalOceanConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.DigitalOcean)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(DigitalOceanConnectionMethod.ApiToken),
credentials: z.object({
apiToken: z.string().trim().min(1, "API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const DigitalOceanConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.DigitalOcean,
method: DigitalOceanConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The type of token you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.DigitalOcean].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Token Type"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(DigitalOceanConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Key Value"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to DigitalOcean"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,157 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { OktaConnectionMethod, TOktaConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TOktaConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Okta)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(OktaConnectionMethod.ApiToken),
credentials: z.object({
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.min(1, "Instance URL required")
.max(255),
apiToken: z
.string()
.trim()
.min(1, "API Token required")
.regex(/^00[a-zA-Z0-9_-]{40}$/, "Invalid Okta API Token format")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const OktaConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Okta,
method: OktaConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Okta].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(OktaConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.instanceUrl"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Instance URL"
>
<Input {...field} placeholder="https://example.okta.com" />
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Okta"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
<LogsSection />
<LogsSection pageView />
</div>
</div>
</div>
@@ -1,9 +1,8 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import ms from "ms";
import { twMerge } from "tailwind-merge";
@@ -18,6 +17,7 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { formatDateTime, Timezone } from "@app/helpers/datetime";
import {
auditLogDateFilterFormSchema,
@@ -28,9 +28,19 @@ import {
type Props = {
setFilter: (data: TAuditLogDateFilterFormData) => void;
filter: TAuditLogDateFilterFormData;
setTimezone: (timezone: Timezone) => void;
timezone: Timezone;
};
const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"];
export const LogsDateFilter = ({ setFilter, filter }: Props) => {
const RELATIVE_OPTIONS = [
{ label: "Minutes", unit: "m", values: [5, 10, 15, 30, 45] },
{ label: "Hours", unit: "h", values: [1, 2, 3, 6, 8, 12] },
{ label: "Days", unit: "d", values: [1, 2, 3, 4, 5, 6] },
{ label: "Weeks", unit: "w", values: [1, 2, 3, 4] }
];
export const LogsDateFilter = ({ setFilter, filter, timezone, setTimezone }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const [isPopupOpen, setIsPopOpen] = useState(false);
@@ -59,154 +69,258 @@ export const LogsDateFilter = ({ setFilter, filter }: Props) => {
};
return (
<DropdownMenu open={isPopupOpen} onOpenChange={(el) => setIsPopOpen(el)}>
<div className="mr-2 flex items-center">
{filter.type === AuditLogDateFilterType.Relative ? (
<>
{RELATIVE_VALUES.map((el) => (
<Button
variant="outline_bg"
className={twMerge(
"rounded-none px-3 py-2 first:rounded-l-md",
filter.type === AuditLogDateFilterType.Relative &&
filter.relativeModeValue === el &&
"border-primary/40 bg-primary/[0.1]"
)}
key={`${el}-relative`}
onClick={() =>
setFilter({
relativeModeValue: el,
type: AuditLogDateFilterType.Relative,
endDate: new Date(),
startDate: new Date(Number(new Date()) - ms(el))
})
}
>
{el}
</Button>
))}
</>
) : (
<>
<div className="rounded-l-md border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
{format(filter.startDate, "yyyy-MM-dd HH:mm")}
</div>
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
{format(filter.endDate, "yyyy-MM-dd HH:mm")}
</div>
</>
)}
<DropdownMenuTrigger asChild>
<Button
variant="outline_bg"
className={twMerge(
"rounded-none rounded-r-md px-3 py-2",
(filter.type === AuditLogDateFilterType.Absolute || isCustomRelative) &&
"border-primary/40 bg-primary/[0.1]"
)}
>
<FontAwesomeIcon icon={faCalendar} />
{filter.type === AuditLogDateFilterType.Relative && isCustomRelative && (
<span className="ml-1">({filter.relativeModeValue})</span>
)}
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent className="min-w-80 p-4" align="center" alignOffset={16}>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="type"
render={({ field }) => (
<FormControl className="mb-2 w-full">
<Select
value={field.value}
onValueChange={(el) => field.onChange(el)}
className="w-full"
<>
<DropdownMenu open={isPopupOpen} onOpenChange={(el) => setIsPopOpen(el)}>
<div className="flex items-center">
{filter.type === AuditLogDateFilterType.Relative ? (
<>
{RELATIVE_VALUES.map((el) => (
<Button
variant="outline_bg"
className={twMerge(
"w-[3.82rem] rounded-none px-3 py-2 font-normal first:rounded-l-md",
filter.type === AuditLogDateFilterType.Relative &&
filter.relativeModeValue === el &&
"border-primary/40 bg-primary/[0.1]"
)}
key={`${el}-relative`}
onClick={() =>
setFilter({
relativeModeValue: el,
type: AuditLogDateFilterType.Relative,
endDate: new Date(),
startDate: new Date(Number(new Date()) - ms(el))
})
}
>
<SelectItem value={AuditLogDateFilterType.Relative}>Relative</SelectItem>
<SelectItem value={AuditLogDateFilterType.Absolute}>Absolute</SelectItem>
</Select>
</FormControl>
)}
/>
{selectType === AuditLogDateFilterType.Relative && (
{el}
</Button>
))}
</>
) : (
<div className="flex w-[19.1rem] items-center justify-between rounded-l-md border border-transparent bg-mineshaft-600 px-5 py-2 text-sm text-bunker-200">
<div>
{formatDateTime({
timezone,
timestamp: filter.startDate,
dateFormat: "yyyy/MM/dd HH:mm"
})}
</div>
<div>
<FontAwesomeIcon className="text-bunker-300" size="sm" icon={faChevronRight} />
</div>
<div>
{formatDateTime({
timezone,
timestamp: filter.endDate,
dateFormat: "yyyy/MM/dd HH:mm"
})}
</div>
</div>
)}
<DropdownMenuTrigger asChild>
<Button
variant="outline_bg"
className={twMerge(
"w-[8rem] rounded-none rounded-r-md px-3 py-2 font-normal",
(filter.type === AuditLogDateFilterType.Absolute || isCustomRelative) &&
"border-primary/40 bg-primary/[0.1]"
)}
>
<span>Custom</span> <FontAwesomeIcon className="ml-1" icon={faCalendar} />
{filter.type === AuditLogDateFilterType.Relative && isCustomRelative && (
<span className="ml-1">({filter.relativeModeValue})</span>
)}
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent
className="!min-w-[434px] bg-mineshaft-800 p-4"
align="end"
sideOffset={8}
>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="relativeModeValue"
name="type"
render={({ field }) => (
<FormControl className="mb-0 w-full" helperText="Example: 1h, 1d, 2d">
<Input {...field} value={field.value || ""} />
</FormControl>
<div className="mb-7">
<Button
onClick={() => field.onChange(AuditLogDateFilterType.Absolute)}
variant="outline_bg"
className={twMerge(
"h-8 rounded-r-none font-normal",
field.value === AuditLogDateFilterType.Absolute &&
"border-primary/40 bg-primary/[0.1]"
)}
>
Absolute
</Button>
<Button
onClick={() => field.onChange(AuditLogDateFilterType.Relative)}
variant="outline_bg"
className={twMerge(
"h-8 rounded-l-none font-normal",
field.value === AuditLogDateFilterType.Relative &&
"border-primary/40 bg-primary/[0.1]"
)}
>
Relative
</Button>
</div>
)}
/>
)}
{selectType === AuditLogDateFilterType.Absolute && (
<div className="mb-2 flex h-10 w-full items-center justify-between gap-2">
{selectType === AuditLogDateFilterType.Relative && (
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
name="relativeModeValue"
render={({ field }) => {
const duration = field.value?.substring(0, field.value.length - 1);
const unitOfTime = field.value?.at(-1);
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
<div className="flex flex-col gap-4">
{RELATIVE_OPTIONS.map(({ label, unit, values }) => (
<div key={unit} className="flex items-center gap-2">
<div className="w-16">{label}</div>
{values.map((v) => {
const value = `${v}${unit}`;
return (
<Button
key={value}
variant="outline_bg"
onClick={() => field.onChange(value)}
className={twMerge(
"h-8 w-12",
field.value === value && "border-primary/40 bg-primary/[0.1]"
)}
>
{v}
</Button>
);
})}
</div>
))}
<div className="flex items-center gap-2">
<FormControl className="mb-0 w-28" label="Duration">
<Input
type="number"
value={duration || "1"}
onChange={(val) => field.onChange(`${val}${unitOfTime}`)}
max={60}
min={1}
/>
</FormControl>
<FormControl className="mb-0 w-36" label="Unit of Time">
<Select
value={unitOfTime}
onValueChange={(val) => field.onChange(`${duration}${val}`)}
className="w-full"
position="popper"
>
{RELATIVE_OPTIONS.map((opt) => (
<SelectItem value={opt.unit}>{opt.label}</SelectItem>
))}
</Select>
</FormControl>
</div>
</div>
);
}}
/>
<div className="flex items-center -space-x-3">
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
)}
{selectType === AuditLogDateFilterType.Absolute && (
<div className="flex h-10 w-full items-center justify-between gap-2">
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
label="Start Date"
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
timezone={timezone}
dateFormat="P"
buttonClassName="w-44 h-8 font-normal"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<FontAwesomeIcon
icon={faChevronRight}
size="xs"
className="mt-6 text-mineshaft-400"
/>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
label="End Date"
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
buttonClassName="w-44 h-8 font-normal"
timezone={timezone}
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
)}
<div className="mt-8 w-full justify-end">
<Button
size="sm"
type="submit"
className="h-9 w-24 font-normal"
variant="outline_bg"
isDisabled={!formState.isDirty}
>
Apply
</Button>
</div>
)}
<div className="mt-4">
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
Apply
</Button>
</div>
</form>
</DropdownMenuContent>
</DropdownMenu>
</form>
</DropdownMenuContent>
</DropdownMenu>
<Select
value={timezone}
onValueChange={(val) => setTimezone(val as Timezone)}
className="w-[10.6rem] border !border-mineshaft-500 !bg-mineshaft-600 capitalize"
dropdownContainerClassName="max-w-none"
position="popper"
dropdownContainerStyle={{
width: "100%"
}}
>
{Object.values(Timezone).map((tz) => (
<SelectItem value={tz} className="capitalize" key={tz}>
{tz} Timezone
</SelectItem>
))}
</Select>
</>
);
};
@@ -300,10 +300,10 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
options={workspacesInOrg.map(({ name, id, type }) => ({
name,
id,
type: defaultProduct
type
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
@@ -1,8 +1,11 @@
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { Timezone } from "@app/helpers/datetime";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -20,10 +23,11 @@ type Props = {
presets?: Presets;
refetchInterval?: number;
showFilters?: boolean;
pageView?: boolean;
};
export const LogsSection = withPermission(
({ presets, refetchInterval, showFilters = true }: Props) => {
({ presets, refetchInterval, showFilters = true, pageView = false }: Props) => {
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -31,6 +35,8 @@ export const LogsSection = withPermission(
eventType: presets?.eventType || [],
actor: presets?.actorId
});
const [timezone, setTimezone] = useState<Timezone>(Timezone.Local);
const [dateFilter, setDateFilter] = useState<TAuditLogDateFilterFormData>({
startDate: new Date(Number(new Date()) - ms("1h")),
endDate: new Date(),
@@ -43,10 +49,85 @@ export const LogsSection = withPermission(
handlePopUpOpen("upgradePlan");
}
}, [subscription]);
if (pageView)
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-y-2">
<div>
<div className="flex items-center gap-1 whitespace-nowrap">
<p className="text-xl font-semibold text-mineshaft-100">Audit History</p>
<a
href="https://infisical.com/docs/documentation/platform/audit-logs"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.1rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
{showFilters && (
<LogsDateFilter
filter={dateFilter}
setFilter={setDateFilter}
timezone={timezone}
setTimezone={setTimezone}
/>
)}
{showFilters && (
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
)}
</div>
</div>
<div className="space-y-2">
<LogsTable
refetchInterval={refetchInterval}
filter={{
secretPath: logFilter.secretPath || undefined,
secretKey: logFilter.secretKey || undefined,
eventMetadata: logFilter?.eventMetadata,
projectId: logFilter?.project?.id,
actorType: presets?.actorType,
limit: 15,
eventType: logFilter?.eventType,
userAgentType: logFilter?.userAgentType,
startDate: dateFilter?.startDate,
endDate: dateFilter?.endDate,
environment: logFilter?.environment?.slug,
actor: logFilter?.actor
}}
timezone={timezone}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to a paid Infisical plan."
/>
</div>
</div>
);
return (
<div className="space-y-2">
<div className="flex w-full justify-end">
{showFilters && <LogsDateFilter filter={dateFilter} setFilter={setDateFilter} />}
{showFilters && (
<LogsDateFilter
filter={dateFilter}
setFilter={setDateFilter}
timezone={timezone}
setTimezone={setTimezone}
/>
)}
{showFilters && (
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
)}
@@ -67,6 +148,7 @@ export const LogsSection = withPermission(
environment: logFilter?.environment?.slug,
actor: logFilter?.actor
}}
timezone={timezone}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
@@ -1,6 +1,5 @@
import { Fragment } from "react";
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFile } from "@fortawesome/free-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import {
@@ -14,9 +13,9 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { Timezone } from "@app/helpers/datetime";
import { useGetAuditLogs } from "@app/hooks/api";
import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
@@ -25,11 +24,12 @@ import { LogsTableRow } from "./LogsTableRow";
type Props = {
filter: TGetAuditLogsFilter;
refetchInterval?: number;
timezone: Timezone;
};
const AUDIT_LOG_LIMIT = 30;
export const LogsTable = ({ filter, refetchInterval }: Props) => {
export const LogsTable = ({ filter, refetchInterval, timezone }: Props) => {
// Determine the project ID for filtering
const filterProjectId =
// Use the projectId from the filter if it exists
@@ -57,16 +57,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
<Th className="w-24">
<Spinner size="xs" className={twMerge(isPending ? "opacity-100" : "opacity-0")} />
</Th>
<Th className="w-64">
Timestamp
<Tooltip
className="normal-case"
content="Time displayed in your system's time zone."
sideOffset={10}
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-1" />
</Tooltip>
</Th>
<Th className="w-64">Timestamp</Th>
<Th>Event</Th>
</Tr>
</THead>
@@ -79,6 +70,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
rowNumber={index + i * AUDIT_LOG_LIMIT + 1}
auditLog={auditLog}
key={`audit-log-${auditLog.id}`}
timezone={timezone}
/>
))}
</Fragment>
@@ -96,7 +88,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
</TableContainer>
{!isEmpty && (
<Button
className="mb-20 mt-4 px-4 py-3 text-sm"
className="mt-4 px-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
@@ -1,8 +1,8 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { Td, Tr } from "@app/components/v2";
import { formatDateTime, Timezone } from "@app/helpers/datetime";
import { useToggle } from "@app/hooks";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { AuditLog } from "@app/hooks/api/auditLogs/types";
@@ -10,6 +10,7 @@ import { AuditLog } from "@app/hooks/api/auditLogs/types";
type Props = {
auditLog: AuditLog;
rowNumber: number;
timezone: Timezone;
};
type TagProps = {
@@ -27,7 +28,7 @@ const Tag = ({ label, value }: TagProps) => {
);
};
export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
export const LogsTableRow = ({ auditLog, rowNumber, timezone }: Props) => {
const [isOpen, setIsOpen] = useToggle();
return (
@@ -46,9 +47,7 @@ export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
<FontAwesomeIcon icon={isOpen ? faCaretDown : faCaretRight} />
{rowNumber}
</Td>
<Td className="align-top">
{format(new Date(auditLog.createdAt), "MMM do yyyy, hh:mm a")}
</Td>
<Td className="align-top">{formatDateTime({ timestamp: auditLog.createdAt, timezone })}</Td>
<Td>
<div className="flex flex-wrap gap-4 text-sm">
<Tag label="event" value={auditLog.event.type} />
@@ -6,6 +6,7 @@ import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { IdentityMembership } from "@app/hooks/api/identities/types";
@@ -50,7 +51,7 @@ export const IdentityProjectRow = ({
onClick={() => {
if (isAccessible) {
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(project.type)}/access-management` as const,
params: {
projectId: project.id
},
@@ -38,7 +38,8 @@ export const ProjectsPage = () => {
"upgradePlan"
] as const);
const { data: serverDetails } = useFetchServerStatus();
const { data: serverDetails, isLoading } = useFetchServerStatus();
const { subscription } = useSubscription();
const isAddingProjectsAllowed = subscription?.workspaceLimit
@@ -51,7 +52,8 @@ export const ProjectsPage = () => {
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
{!serverDetails?.redisConfigured && (
{!isLoading && !serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start text-3xl">
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p>
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
@@ -4,6 +4,7 @@ import {
faArrowDownAZ,
faBorderAll,
faCheck,
faCheckCircle,
faFolderOpen,
faList,
faMagnifyingGlass,
@@ -16,10 +17,17 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
IconButton,
Input,
Lottie,
Modal,
ModalContent,
Pagination,
@@ -27,7 +35,7 @@ import {
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { getProjectHomePage } from "@app/helpers/project";
import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project";
import {
getUserTablePreference,
PreferenceKey,
@@ -35,7 +43,7 @@ import {
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useRequestProjectAccess, useSearchProjects } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/workspace/types";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
type Props = {
onAddNewProject: () => void;
@@ -98,6 +106,7 @@ export const AllProjectView = ({
const navigate = useNavigate();
const [searchFilter, setSearchFilter] = useState("");
const [debouncedSearch] = useDebounce(searchFilter);
const [projectTypeFilter, setProjectTypeFilter] = useState<ProjectType>();
const {
setPage,
perPage,
@@ -124,7 +133,8 @@ export const AllProjectView = ({
limit,
offset,
name: debouncedSearch || undefined,
orderDirection
orderDirection,
type: projectTypeFilter
});
useResetPageHelper({
@@ -134,6 +144,9 @@ export const AllProjectView = ({
});
const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Workspace;
const handleToggleFilterByProjectType = (el: ProjectType) =>
setProjectTypeFilter((state) => (state === el ? undefined : el));
return (
<div>
<div className="flex w-full flex-row">
@@ -160,16 +173,62 @@ export const AllProjectView = ({
</IconButton>
</Tooltip>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className={twMerge(
"ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1",
projectTypeFilter && "border-primary-400 text-primary-400"
)}
>
<Tooltip content="Choose visible project type" className="mb-2">
<IconButton
ariaLabel="project-types"
className={twMerge(
"min-w-[2.4rem] border-none hover:bg-mineshaft-600",
projectTypeFilter && "text-primary-400"
)}
variant="plain"
size="xs"
colorSchema="secondary"
>
<FontAwesomeIcon icon={faList} />
</IconButton>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar overflow-y-auto" align="end">
<DropdownMenuLabel>Filter By Project Type</DropdownMenuLabel>
{Object.values(ProjectType).map((el) => (
<DropdownMenuItem
key={`filter-item-${el}`}
onClick={(e) => {
e.preventDefault();
handleToggleFilterByProjectType(el);
}}
icon={projectTypeFilter === el && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<span>{getProjectTitle(el)}</span>
</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Tooltip content="Disabled across All Project view.">
<IconButton
variant="outline_bg"
ariaLabel="grid"
size="xs"
className="min-w-[2.4rem] border-none bg-transparent hover:bg-mineshaft-600"
>
<FontAwesomeIcon icon={faBorderAll} />
</IconButton>
<div className="flex cursor-not-allowed items-center justify-center">
<IconButton
variant="outline_bg"
ariaLabel="grid"
size="xs"
isDisabled
className="pointer-events-none min-w-[2.4rem] border-none bg-transparent hover:bg-mineshaft-600"
>
<FontAwesomeIcon icon={faBorderAll} />
</IconButton>
</div>
</Tooltip>
<IconButton
variant="outline_bg"
@@ -184,7 +243,7 @@ export const AllProjectView = ({
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
@@ -222,7 +281,7 @@ export const AllProjectView = ({
onKeyDown={(evt) => {
if (evt.key === "Enter" && workspace.isMember) {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
@@ -232,7 +291,7 @@ export const AllProjectView = ({
onClick={() => {
if (workspace.isMember) {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
@@ -241,38 +300,41 @@ export const AllProjectView = ({
}}
key={workspace.id}
className={twMerge(
"group flex min-w-72 grid-cols-6 items-center justify-center border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 first:rounded-t-md",
"group flex min-w-72 items-center justify-center border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 first:rounded-t-md",
workspace.isMember ? "cursor-pointer hover:bg-mineshaft-700" : "cursor-default"
)}
>
<div className="w-full items-center">
<div className="flex flex-grow items-center">
<div className="flex-grow truncate text-sm text-mineshaft-100">
{workspace.name}
</div>
<div className="flex items-center">
{workspace.isMember ? (
<div className="flex items-center text-center text-sm text-primary">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
Joined
</div>
) : (
<div className="opacity-0 transition-all group-hover:opacity-100">
<Button
size="xs"
variant="outline_bg"
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
>
Request Access
</Button>
</div>
)}
</div>
<div className="mr-3 flex min-w-0 flex-1 items-center gap-3">
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1 shadow-inner">
<Lottie
className="h-[1.35rem] w-[1.35rem] shrink-0"
icon={getProjectLottieIcon(workspace.type)}
/>
</div>
<div className="mt-1 max-w-lg overflow-hidden text-ellipsis whitespace-nowrap text-xs text-mineshaft-300">
{workspace.description}
<div className="-mt-0.5 flex min-w-0 flex-col">
<p className="truncate text-sm text-mineshaft-100">{workspace.name}</p>
<p className="truncate text-xs leading-4 text-mineshaft-300">
{getProjectTitle(workspace.type)}{" "}
{workspace.description ? `- ${workspace.description}` : ""}
</p>
</div>
</div>
{workspace.isMember ? (
<Badge className="flex items-center" variant="success">
<FontAwesomeIcon icon={faCheck} className="mr-1" />
<span>Joined</span>
</Badge>
) : (
<div className="opacity-0 transition-all group-hover:opacity-100">
<Button
size="xs"
variant="outline_bg"
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
>
Request Access
</Button>
</div>
)}
</div>
))}
</div>
@@ -2,9 +2,9 @@ import { ReactNode, useMemo, useState } from "react";
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
import {
faArrowDownAZ,
faArrowRight,
faArrowUpZA,
faBorderAll,
faCheckCircle,
faList,
faMagnifyingGlass,
faPlus,
@@ -13,12 +13,26 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
IconButton,
Input,
Lottie,
Pagination,
Skeleton,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { getProjectHomePage } from "@app/helpers/project";
import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project";
import {
getUserTablePreference,
PreferenceKey,
@@ -29,7 +43,7 @@ import { useGetUserWorkspaces } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { Workspace } from "@app/hooks/api/workspace/types";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
type Props = {
onAddNewProject: () => void;
@@ -53,6 +67,9 @@ export const MyProjectView = ({
}: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const [projectTypeFilter, setProjectTypeFilter] = useState<Partial<Record<ProjectType, boolean>>>(
{}
);
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces();
const {
@@ -67,6 +84,7 @@ export const MyProjectView = ({
} = usePagination(ProjectOrderBy.Name, {
initPerPage: getUserTablePreference("myProjectsTable", PreferenceKey.PerPage, 24)
});
const isTableFilteredByType = Boolean(Object.values(projectTypeFilter).some((el) => el));
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
@@ -88,13 +106,18 @@ export const MyProjectView = ({
const filteredWorkspaces = useMemo(
() =>
workspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.filter((ws) => {
if (isTableFilteredByType && !projectTypeFilter?.[ws.type]) {
return false;
}
return ws?.name?.toLowerCase().includes(searchFilter.toLowerCase());
})
.sort((a, b) =>
orderDirection === OrderByDirection.ASC
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
),
[searchFilter, orderDirection, workspaces]
[searchFilter, orderDirection, workspaces, projectTypeFilter]
);
useResetPageHelper({
@@ -117,6 +140,15 @@ export const MyProjectView = ({
};
}, [filteredWorkspaces, projectFavorites, offset, limit, page]);
const handleToggleFilterByProjectType = (type: ProjectType) => {
setProjectTypeFilter((state) => {
return {
...(state || {}),
[type]: !state?.[type]
};
});
};
const addProjectToFavorites = async (projectId: string) => {
try {
if (currentOrg?.id) {
@@ -153,61 +185,53 @@ export const MyProjectView = ({
<div
onClick={() => {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
});
}}
key={workspace.id}
className="flex h-40 min-w-72 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
className="cursor-pointer overflow-clip rounded border border-l-[4px] border-mineshaft-600 border-l-mineshaft-400 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:border-l-primary hover:bg-mineshaft-700"
>
<div className="flex flex-row justify-between">
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
}}
<div className="flex items-center gap-4">
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1.5 shadow-inner">
<Lottie
className="h-[1.75rem] w-[1.75rem] shrink-0"
icon={getProjectLottieIcon(workspace.type)}
/>
) : (
<FontAwesomeIcon
icon={faStar}
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(workspace.id);
}}
/>
)}
</div>
<div
className="mb-2.5 mt-1 grow text-sm text-mineshaft-300"
style={{
overflow: "hidden",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2
}}
>
{workspace.description}
</div>
<div className="flex w-full flex-row items-end justify-between place-self-end">
<button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 px-4 py-2 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{" "}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-mineshaft-100">{workspace.name}</p>
<p className="truncate text-sm leading-4 text-mineshaft-300">
{getProjectTitle(workspace.type)}
</p>
</div>
<div className="mt-0.5 self-start">
{isFavorite ? (
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
icon={faSolidStar}
className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
}}
/>
</div>
</button>
) : (
<FontAwesomeIcon
icon={faStar}
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(workspace.id);
}}
/>
)}
</div>
</div>
<p className="mt-4 truncate text-sm text-mineshaft-400">
{workspace.description || "No description"}
</p>
</div>
);
const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
@@ -215,21 +239,33 @@ export const MyProjectView = ({
<div
onClick={() => {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
});
}}
key={workspace.id}
className={`group grid h-14 min-w-72 cursor-pointer grid-cols-6 border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
className={`group flex min-w-72 cursor-pointer border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 hover:bg-mineshaft-700 ${
index === 0 && "rounded-t-md"
}`}
>
<div className="flex items-center sm:col-span-3 lg:col-span-4">
<div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1 shadow-inner">
<Lottie
className="h-[1.35rem] w-[1.35rem] shrink-0"
icon={getProjectLottieIcon(workspace.type)}
/>
</div>
<div className="-mt-0.5 flex min-w-0 flex-col">
<p className="truncate text-sm text-mineshaft-100">{workspace.name}</p>
<p className="truncate text-xs leading-4 text-mineshaft-300">
{getProjectTitle(workspace.type)}{" "}
{workspace.description ? `- ${workspace.description}` : ""}
</p>
</div>
</div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
<div className="flex items-center justify-end">
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
@@ -258,7 +294,7 @@ export const MyProjectView = ({
switch (projectsViewMode) {
case ProjectsViewMode.GRID:
projectsComponents = (
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<div className="mt-4 grid w-full grid-cols-1 gap-5 lg:grid-cols-2 xl:grid-cols-3">
{isProjectViewLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
@@ -320,7 +356,18 @@ export const MyProjectView = ({
<div className="text-center font-light">No projects match search...</div>
</div>
);
} else if (filteredWorkspaces.length === 0 && isTableFilteredByType) {
projectsComponents = (
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon
icon={faSearch}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/>
<div className="text-center font-light">No projects match filters...</div>
</div>
);
}
return (
<div>
<div className="flex w-full flex-row">
@@ -351,6 +398,49 @@ export const MyProjectView = ({
</IconButton>
</Tooltip>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className={twMerge(
"ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1",
isTableFilteredByType && "border-primary-400 text-primary-400"
)}
>
<Tooltip content="Choose visible project type" className="mb-2">
<IconButton
ariaLabel="project-types"
className={twMerge(
"min-w-[2.4rem] border-none hover:bg-mineshaft-600",
isTableFilteredByType && "text-primary-400"
)}
variant="plain"
size="xs"
colorSchema="secondary"
>
<FontAwesomeIcon icon={faList} />
</IconButton>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar overflow-y-auto" align="end">
<DropdownMenuLabel>Filter By Project Type</DropdownMenuLabel>
{Object.values(ProjectType).map((el) => (
<DropdownMenuItem
key={`filter-item-${el}`}
onClick={(e) => {
e.preventDefault();
handleToggleFilterByProjectType(el);
}}
icon={projectTypeFilter?.[el] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<span>{getProjectTitle(el)}</span>
</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<IconButton
variant="outline_bg"
@@ -385,7 +475,7 @@ export const MyProjectView = ({
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
@@ -417,7 +507,7 @@ export const MyProjectView = ({
onChangePerPage={handlePerPageChange}
/>
)}
{isWorkspaceEmpty && (
{isWorkspaceEmpty && !isTableFilteredByType && (
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon
icon={faFolderOpen}
@@ -27,7 +27,7 @@ export const ProjectListToggle = ({ value, onChange }: Props) => {
value={value}
onValueChange={onChange}
className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
dropdownContainerClassName="mt-10 ml-5"
position="popper"
>
<SelectItem value={ProjectListView.MyProjects}>My Projects</SelectItem>
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>
@@ -1,7 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
@@ -12,8 +13,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
PageHeader
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
@@ -22,7 +22,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
import { OrgAccessControlTabSections } from "@app/types/org";
import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components";
import { RoleModal, RolePermissionsSection } from "./components";
export const Page = () => {
const navigate = useNavigate();
@@ -80,29 +80,64 @@ export const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
<PageHeader
title={
<div className="flex flex-col">
<div>
<span>{data.name}</span>
<p className="text-sm font-[400] normal-case leading-3 text-mineshaft-400">
{data.slug} {data.description && `- ${data.description}`}
</p>
</div>
</div>
}
>
{isCustomRole && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
<DropdownMenuTrigger asChild>
<Button
colorSchema="secondary"
rightIcon={<FontAwesomeIcon icon={faEllipsisV} className="ml-2" />}
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="end" sideOffset={2} className="p-1">
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(data.id);
createNotification({
text: "Copied ID to clipboard",
type: "info"
});
}}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(data.slug);
createNotification({
text: "Copied slug to clipboard",
type: "info"
});
}}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Copy Slug
</DropdownMenuItem>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
onClick={() => {
handlePopUpOpen("role", {
roleId
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Edit Role
</DropdownMenuItem>
@@ -111,13 +146,10 @@ export const Page = () => {
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
handlePopUpOpen("duplicateRole");
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Duplicate Role
</DropdownMenuItem>
@@ -126,15 +158,10 @@ export const Page = () => {
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
onClick={() => {
handlePopUpOpen("deleteOrgRole");
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Delete Role
</DropdownMenuItem>
@@ -144,12 +171,7 @@ export const Page = () => {
</DropdownMenu>
)}
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />
</div>
<RolePermissionsSection roleId={roleId} />
</div>
<RolePermissionsSection roleId={roleId} />
</div>
)}
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -1,95 +0,0 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetOrgRole } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: object) => void;
};
export const RoleDetailsSection = ({ roleId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId);
const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Org Role Details</h3>
{isCustomRole && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
{(isAllowed) => {
return (
<Tooltip content="Edit Role">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() =>
handlePopUpOpen("role", {
roleId
})
}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{roleId}</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(roleId);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<p className="text-sm text-mineshaft-300">{data.slug}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">
{data.description?.length ? data.description : "-"}
</p>
</div>
</div>
</div>
) : (
<div />
);
};
@@ -76,17 +76,18 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }:
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Organization Admin Console</Td>
<Td className="w-full select-none">Organization Admin Console</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -95,11 +96,8 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }:
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -124,17 +124,18 @@ export const OrgPermissionAppConnectionRow = ({ isEditable, control, setValue }:
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>App Connections</Td>
<Td className="w-full select-none">App Connections</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -145,11 +146,8 @@ export const OrgPermissionAppConnectionRow = ({ isEditable, control, setValue }:
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -112,17 +112,18 @@ export const OrgPermissionBillingRow = ({ isEditable, control, setValue }: Props
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Billing</Td>
<Td className="w-full select-none">Billing</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -133,11 +134,8 @@ export const OrgPermissionBillingRow = ({ isEditable, control, setValue }: Props
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -121,17 +121,18 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Gateways</Td>
<Td className="w-full select-none">Gateways</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -142,11 +143,8 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -145,17 +145,18 @@ export const OrgPermissionGroupRow = ({ isEditable, control, setValue }: Props)
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Group Management</Td>
<Td className="w-full select-none">Group Management</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -166,11 +167,8 @@ export const OrgPermissionGroupRow = ({ isEditable, control, setValue }: Props)
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -155,17 +155,18 @@ export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Prop
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Machine Identity Management</Td>
<Td className="w-full select-none">Machine Identity Management</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -176,11 +177,8 @@ export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Prop
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -73,17 +73,18 @@ export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) =
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>KMIP</Td>
<Td className="w-full select-none">KMIP</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -92,11 +93,8 @@ export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) =
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -70,17 +70,18 @@ export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: P
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Secret Share</Td>
<Td className="w-full select-none">Secret Share</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -89,11 +90,8 @@ export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: P
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -70,17 +70,18 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Project</Td>
<Td className="w-full select-none">Project</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -89,11 +90,8 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -158,17 +158,18 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>{title}</Td>
<Td className="w-full select-none">{title}</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -179,11 +180,8 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{getPermissionList(formName).map(({ action, label }) => {
return (
<Controller
@@ -1,8 +1,10 @@
import { useForm } from "react-hook-form";
import { faSave } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
import { Button, Table, TableContainer, TBody } from "@app/components/v2";
import { OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { OrgPermissionAppConnectionRow } from "@app/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAppConnectionRow";
@@ -117,39 +119,39 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Permissions</h3>
<div>
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
<p className="text-sm leading-3 text-mineshaft-400">Configure granular access policies</p>
</div>
{isCustomRole && (
<div className="flex items-center">
{isDirty && (
<Button
className="mr-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
onClick={() => reset()}
>
Discard
</Button>
)}
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
className="h-10 border"
leftIcon={<FontAwesomeIcon icon={faSave} />}
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
>
Save
</Button>
<Button
className="ml-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
onClick={() => reset()}
>
Cancel
</Button>
</div>
)}
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-5" />
<Th>Resource</Th>
<Th>Permission</Th>
</Tr>
</THead>
<TBody>
{SIMPLE_PERMISSION_OPTIONS.map((permission) => {
return (
@@ -1,3 +1,2 @@
export { RoleDetailsSection } from "./RoleDetailsSection";
export { RoleModal } from "./RoleModal";
export { RolePermissionsSection } from "./RolePermissionsSection";
@@ -24,6 +24,7 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
initialData: template,
enabled: !isInfisicalTemplate
});
const finalTemplate = isInfisicalTemplate ? template : projectTemplate;
return (
<div>
@@ -41,10 +42,10 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
<div className="flex h-[60vh] w-full items-center justify-center p-24">
<Spinner />
</div>
) : projectTemplate ? (
) : finalTemplate ? (
<EditProjectTemplate
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
projectTemplate={finalTemplate}
onBack={onBack}
/>
) : (
@@ -5,8 +5,10 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
@@ -24,7 +26,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
"editDetails"
] as const);
const { id: templateId, name, description } = projectTemplate;
const { id: templateId, name, description, type } = projectTemplate;
const deleteProjectTemplate = useDeleteProjectTemplate();
@@ -53,7 +55,10 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
<div className="mb-4 flex items-start justify-between border-b border-bunker-400 pb-4">
<div className="flex-col">
<h3 className="text-xl font-semibold">{name}</h3>
<h2 className="text-sm text-mineshaft-400">{description || "Project Template"}</h2>
<h2 className="text-sm text-mineshaft-400">
{`${getProjectTitle(type)} - `}
{description || "Project Template"}
</h2>
</div>
{!isInfisicalTemplate && (
<div className="flex gap-2">
@@ -94,10 +99,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
</div>
)}
</div>
<ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
{type === ProjectType.SecretManager && (
<ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
)}
<ProjectTemplateRolesSection
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
@@ -126,7 +126,7 @@ export const ProjectTemplateEditRoleForm = ({
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} />
<AddPoliciesButton isDisabled={isDisabled} projectType={projectTemplate.type} />
</div>
</div>
)}
@@ -1,5 +1,6 @@
import { useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -7,21 +8,25 @@ import {
Button,
FormControl,
Input,
Lottie,
Modal,
ModalClose,
ModalContent,
TextArea
} from "@app/components/v2";
import { getProjectLottieIcon } from "@app/helpers/project";
import {
TProjectTemplate,
useCreateProjectTemplate,
useUpdateProjectTemplate
} from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({
name: slugSchema({ min: 1, max: 64, field: "Name" }),
description: z.string().max(500).optional()
description: z.string().max(500).optional(),
type: z.nativeEnum(ProjectType).optional()
});
export type FormData = z.infer<typeof formSchema>;
@@ -38,6 +43,29 @@ type FormProps = {
onComplete: (template: TProjectTemplate) => void;
};
const PROJECT_TYPE_MENU_ITEMS = [
{
label: "Secrets Management",
value: ProjectType.SecretManager
},
{
label: "Certificates Management",
value: ProjectType.CertificateManager
},
{
label: "KMS",
value: ProjectType.KMS
},
{
label: "SSH",
value: ProjectType.SSH
},
{
label: "Secret Scanning",
value: ProjectType.SecretScanning
}
];
const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
const createProjectTemplate = useCreateProjectTemplate();
const updateProjectTemplate = useUpdateProjectTemplate();
@@ -45,12 +73,14 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
const {
handleSubmit,
register,
control,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: projectTemplate?.name,
description: projectTemplate?.description
description: projectTemplate?.description,
type: ProjectType.SecretManager
}
});
@@ -90,6 +120,42 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
>
<Input autoFocus placeholder="my-project-template" {...register("name")} />
</FormControl>
<Controller
control={control}
name="type"
defaultValue={ProjectType.SecretManager}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Type"
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<div className="mt-2 grid grid-cols-5 gap-4">
{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",
field.value === el.value && "border-primary-400 bg-mineshaft-600 opacity-100"
)}
onClick={() => field.onChange(el.value)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") {
field.onChange(el.value);
}
}}
>
<Lottie icon={getProjectLottieIcon(el.value)} className="h-8 w-8" />
<div className="text-center text-xs">{el.label}</div>
</div>
))}
</div>
</FormControl>
)}
/>
<FormControl
label="Description (optional)"
errorText={errors.description?.message}
@@ -23,6 +23,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
@@ -66,8 +67,8 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
<THead>
<Tr>
<Th>Name</Th>
<Th>Type</Th>
<Th>Roles</Th>
<Th>Environments</Th>
<Th />
</Tr>
</THead>
@@ -80,7 +81,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
/>
)}
{filteredTemplates.map((template) => {
const { id, name, roles, environments = [], description } = template;
const { id, name, roles, description, type } = template;
return (
<Tr
onClick={() => onEdit(template)}
@@ -99,6 +100,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
</Tooltip>
)}
</Td>
<Td>{getProjectTitle(type)}</Td>
<Td className="pl-8">
{roles.length}
{roles.length > 0 && (
@@ -119,26 +121,6 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
</Tooltip>
)}
</Td>
<Td className="pl-14">
{environments?.length || 0}
{environments?.length && (
<Tooltip
content={
<ul className="ml-2 list-disc">
{environments
?.sort((a, b) => (a.position > b.position ? 1 : -1))
.map((env) => <li key={env.slug}>{env.name}</li>)}
</ul>
}
>
<FontAwesomeIcon
size="sm"
className="ml-2 text-mineshaft-400"
icon={faCircleInfo}
/>
</Tooltip>
)}
</Td>
<Td className="w-5">
{name !== "default" && (
<OrgPermissionCan
@@ -5,6 +5,7 @@ import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { TWorkspaceUser } from "@app/hooks/api/types";
@@ -43,7 +44,7 @@ export const UserProjectRow = ({
onClick={() => {
if (isAccessible) {
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(project.type)}/access-management` as const,
params: {
projectId: project.id
},
@@ -4,6 +4,8 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectAccessControlTabs } from "@app/types/project";
import {
@@ -24,7 +26,7 @@ const Page = () => {
const updateSelectedTab = (tab: string) => {
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const,
search: (prev) => ({ ...prev, selectedTab: tab }),
params: {
projectId: currentWorkspace.id
@@ -32,6 +34,8 @@ const Page = () => {
});
};
const isSecretManager = currentWorkspace.type === ProjectType.SecretManager;
return (
<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">
@@ -48,7 +52,9 @@ const Page = () => {
<p>Machine Identities</p>
</div>
</Tab>
<Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab>
{isSecretManager && (
<Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab>
)}
<Tab value={ProjectAccessControlTabs.Roles}>Project Roles</Tab>
</TabList>
<TabPanel value={ProjectAccessControlTabs.Member}>
@@ -60,9 +66,11 @@ const Page = () => {
<TabPanel value={ProjectAccessControlTabs.Identities}>
<IdentityTab />
</TabPanel>
<TabPanel value={ProjectAccessControlTabs.ServiceTokens}>
<ServiceTokenTab />
</TabPanel>
{isSecretManager && (
<TabPanel value={ProjectAccessControlTabs.ServiceTokens}>
<ServiceTokenTab />
</TabPanel>
)}
<TabPanel value={ProjectAccessControlTabs.Roles}>
<ProjectRoleListTab />
</TabPanel>
@@ -33,6 +33,7 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import {
getUserTablePreference,
PreferenceKey,
@@ -158,7 +159,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({
to: "/projects/$projectId/groups/$groupId",
to: `${getProjectBaseURL(currentWorkspace.type)}/groups/$groupId` as const,
params: {
projectId: currentWorkspace.id,
groupId: id
@@ -168,7 +169,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
}}
onClick={() =>
navigate({
to: "/projects/$projectId/groups/$groupId",
to: `${getProjectBaseURL(currentWorkspace.type)}/groups/$groupId` as const,
params: {
projectId: currentWorkspace.id,
groupId: id
@@ -46,6 +46,7 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import {
getUserTablePreference,
@@ -260,7 +261,7 @@ export const IdentityTab = withProjectPermission(
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({
to: "/projects/$projectId/identities/$identityId",
to: `${getProjectBaseURL(currentWorkspace.type)}/identities/$identityId` as const,
params: {
projectId: currentWorkspace.id,
identityId: id
@@ -270,7 +271,7 @@ export const IdentityTab = withProjectPermission(
}}
onClick={() =>
navigate({
to: "/projects/$projectId/identities/$identityId",
to: `${getProjectBaseURL(currentWorkspace.type)}/identities/$identityId` as const,
params: {
projectId: currentWorkspace.id,
identityId: id
@@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
import { Alert, AlertDescription } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { MemberRbacSection } from "./MemberRbacSection";
@@ -21,7 +22,7 @@ export const MemberRoleForm = ({ projectMember, onOpenUpgradeModal }: Props) =>
>
<AlertDescription>
<Link
to="/projects/$projectId/members/$membershipId"
to={`${getProjectBaseURL(currentWorkspace.type)}/members/$membershipId` as const}
params={{
projectId: currentWorkspace.id,
membershipId: projectMember.id
@@ -50,6 +50,7 @@ import {
useUser,
useWorkspace
} from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import {
getUserTablePreference,
@@ -311,7 +312,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({
to: "/projects/$projectId/members/$membershipId",
to: `${getProjectBaseURL(currentWorkspace.type)}/members/$membershipId`,
params: {
projectId: workspaceId,
membershipId
@@ -321,7 +322,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
}}
onClick={() =>
navigate({
to: "/projects/$projectId/members/$membershipId",
to: `${getProjectBaseURL(currentWorkspace.type)}/members/$membershipId`,
params: {
projectId: workspaceId,
membershipId
@@ -40,6 +40,7 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { isCustomProjectRole } from "@app/helpers/roles";
import {
getUserTablePreference,
@@ -249,7 +250,7 @@ export const ProjectRoleList = () => {
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() =>
navigate({
to: "/projects/$projectId/roles/$roleSlug",
to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug`,
params: {
projectId: currentWorkspace.id,
roleSlug: slug
@@ -291,7 +292,7 @@ export const ProjectRoleList = () => {
onClick={(e) => {
e.stopPropagation();
navigate({
to: "/projects/$projectId/roles/$roleSlug",
to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug`,
params: {
projectId: currentWorkspace.id,
roleSlug: slug
@@ -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/cert-management/$projectId/_cert-manager-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema),
search: {
middlewares: [stripSearchParams({ requesterEmail: "" })]
},
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control"
}
]
};
}
});
@@ -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/kms/$projectId/_kms-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema),
search: {
middlewares: [stripSearchParams({ requesterEmail: "" })]
},
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control"
}
]
};
}
});
@@ -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/secret-management/$projectId/_secret-manager-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema),
search: {
middlewares: [stripSearchParams({ requesterEmail: "" })]
},
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control"
}
]
};
}
});
@@ -12,7 +12,7 @@ const AccessControlPageQuerySchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/access-management"
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema),
@@ -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/ssh/$projectId/_ssh-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema),
search: {
middlewares: [stripSearchParams({ requesterEmail: "" })]
},
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control"
}
]
};
}
});
@@ -15,6 +15,7 @@ import {
} from "@app/components/v2";
import { CopyButton } from "@app/components/v2/CopyButton";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
import { TGroupMembership } from "@app/hooks/api/groups/types";
@@ -46,7 +47,7 @@ export const GroupDetailsSection = ({ groupMembership }: Props) => {
});
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(currentWorkspace.type)}/access-management`,
params: {
projectId: currentWorkspace.id
},
@@ -24,7 +24,7 @@ import {
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project";
import { getProjectHomePage } from "@app/helpers/project";
import {
getUserTablePreference,
PreferenceKey,
@@ -36,7 +36,6 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useListProjectGroupUsers } from "@app/hooks/api/groups/queries";
import { EFilterReturnedUsers, TGroupMembership } from "@app/hooks/api/groups/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { GroupMembershipRow } from "./GroupMembershipRow";
@@ -137,9 +136,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
text: "User privilege assumption has started"
});
const url = getProjectHomePage(
getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager
);
const url = getProjectHomePage(currentWorkspace.type);
window.location.href = url.replace("$projectId", currentWorkspace.id);
}
}
@@ -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/cert-management/$projectId/_cert-manager-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/cert-management/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Groups
}
})
},
{
label: "Group"
}
]
};
}
});
@@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/groups/$groupId"
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
{
label: "Access Control",
link: linkOptions({
to: "/projects/$projectId/access-management",
to: "/projects/kms/$projectId/access-management",
params: {
projectId: params.projectId
},
@@ -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/secret-management/$projectId/_secret-manager-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 { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
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.Groups
}
})
},
{
label: "Group"
}
]
};
}
});
@@ -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/ssh/$projectId/_ssh-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/ssh/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Groups
}
})
},
{
label: "Group"
}
]
};
}
});
@@ -20,7 +20,7 @@ import {
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project";
import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import {
useAssumeProjectPrivileges,
@@ -28,7 +28,6 @@ import {
useGetWorkspaceIdentityMembershipDetails
} from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection";
import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSection";
@@ -68,9 +67,7 @@ const Page = () => {
type: "success",
text: "Identity privilege assumption has started"
});
const url = getProjectHomePage(
getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager
);
const url = getProjectHomePage(currentWorkspace.type);
window.location.href = url.replace("$projectId", currentWorkspace.id);
}
}
@@ -89,7 +86,7 @@ const Page = () => {
});
handlePopUpClose("deleteIdentity");
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const,
params: {
projectId: workspaceId
},
@@ -225,7 +225,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} />
<AddPoliciesButton isDisabled={isDisabled} projectType={currentWorkspace.type} />
</div>
</div>
</div>
@@ -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/cert-management/$projectId/_cert-manager-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/cert-management/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Identities
}
})
},
{
label: "Machine Identity"
}
]
};
}
});
@@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/identities/$identityId"
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
{
label: "Access Control",
link: linkOptions({
to: "/projects/$projectId/access-management",
to: "/projects/kms/$projectId/access-management",
params: {
projectId: params.projectId
},
@@ -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/secret-management/$projectId/_secret-manager-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 { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage,
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.Identities
}
})
},
{
label: "Machine Identity"
}
]
};
}
});
@@ -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/ssh/$projectId/_ssh-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/ssh/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Identities
}
})
},
{
label: "Machine Identity"
}
]
};
}
});
@@ -21,7 +21,7 @@ import {
useOrganization,
useWorkspace
} from "@app/context";
import { getCurrentProductFromUrl, getProjectHomePage } from "@app/helpers/project";
import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import {
useAssumeProjectPrivileges,
@@ -29,7 +29,6 @@ import {
useGetWorkspaceUserDetails
} from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
@@ -73,9 +72,7 @@ export const Page = () => {
text: "User privilege assumption has started"
});
const url = getProjectHomePage(
getCurrentProductFromUrl(window.location.href) || ProjectType.SecretManager
);
const url = getProjectHomePage(currentWorkspace.type);
window.location.href = url.replace("$projectId", currentWorkspace.id);
}
}
@@ -96,7 +93,7 @@ export const Page = () => {
type: "success"
});
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const,
params: {
projectId: currentWorkspace.id
}
@@ -221,7 +221,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} />
<AddPoliciesButton isDisabled={isDisabled} projectType={currentWorkspace.type} />
</div>
</div>
</div>
@@ -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/cert-management/$projectId/_cert-manager-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/cert-management/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Member
}
})
},
{
label: "User"
}
]
};
}
});
@@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout/members/$membershipId"
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
{
label: "Access Control",
link: linkOptions({
to: "/projects/$projectId/access-management",
to: "/projects/kms/$projectId/access-management",
params: {
projectId: params.projectId
},
@@ -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/secret-management/$projectId/_secret-manager-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage,
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.Member
}
})
},
{
label: "User"
}
]
};
}
});
@@ -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/secret-scanning/$projectId/_secret-scanning-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,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/ssh/$projectId/_ssh-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/ssh/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Member
}
})
},
{
label: "User"
}
]
};
}
});
@@ -17,6 +17,7 @@ import {
PageHeader
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -60,7 +61,7 @@ const Page = () => {
});
handlePopUpClose("deleteRole");
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(currentWorkspace.type)}/access-management` as const,
params: {
projectId
},
@@ -111,6 +112,32 @@ const Page = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={2} className="p-1">
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(data.id);
createNotification({
text: "Copied ID to clipboard",
type: "info"
});
}}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(data.slug);
createNotification({
text: "Copied slug to clipboard",
type: "info"
});
}}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Copy Slug
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
@@ -9,14 +9,16 @@ import {
IconButton
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal";
type Props = {
isDisabled?: boolean;
projectType: ProjectType;
};
export const AddPoliciesButton = ({ isDisabled }: Props) => {
export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => {
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addPolicy",
"addPolicyOptions",
@@ -66,10 +68,12 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
</DropdownMenuContent>
</DropdownMenu>
<PolicySelectionModal
type={projectType}
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
<PolicyTemplateModal
type={projectType}
isOpen={popUp.applyTemplate.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)}
/>
@@ -7,6 +7,7 @@ import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { getProjectBaseURL } from "@app/helpers/project";
import { useCreateProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { slugSchema } from "@app/lib/schemas";
@@ -80,7 +81,7 @@ const Content = ({ role, onClose }: ContentProps) => {
});
navigate({
to: "/projects/$projectId/roles/$roleSlug",
to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug` as const,
params: {
roleSlug: newRole.slug,
projectId: currentWorkspace.id
@@ -124,7 +124,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
};
return (
<div className="overflow-clip border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
<div className="overflow-clip border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md hover:bg-mineshaft-700">
<div
className="flex h-14 cursor-pointer items-center px-5 py-4 text-sm text-gray-300"
role="button"
@@ -136,9 +136,9 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
}
}}
>
<FontAwesomeIcon className="mr-8" icon={isOpen ? faChevronDown : faChevronRight} />
<FontAwesomeIcon className="mr-6 w-4" icon={isOpen ? faChevronDown : faChevronRight} />
<div className="flex-grow text-base">{title}</div>
<div className="flex-grow select-none text-base">{title}</div>
{fields.length > 1 && (
<div>
<Tag size="xs" className="mr-2 px-2">
@@ -19,26 +19,31 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/workspace/types";
import {
EXCLUDED_PERMISSION_SUBS,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
ProjectTypePermissionSubjects,
TFormSchema
} from "./ProjectRoleModifySection.utils";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
type: ProjectType;
};
type ContentProps = {
onClose: () => void;
type: ProjectType;
};
type TForm = { permissions: Record<ProjectPermissionSub, boolean> };
const Content = ({ onClose }: ContentProps) => {
const Content = ({ onClose, type: projectType }: ContentProps) => {
const rootForm = useFormContext<TFormSchema>();
const [search, setSearch] = useState("");
const {
@@ -56,7 +61,12 @@ const Content = ({ onClose }: ContentProps) => {
});
const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT)
.filter(([, { title }]) => (search ? title.toLowerCase().includes(search.toLowerCase()) : true))
.filter(
([subject, { title }]) =>
ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][
subject as ProjectPermissionSub
] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true)
)
.filter(([subject]) => !EXCLUDED_PERMISSION_SUBS.includes(subject as ProjectPermissionSub))
.sort((a, b) => a[1].title.localeCompare(b[1].title))
.map(([subject]) => subject);
@@ -191,7 +201,7 @@ const Content = ({ onClose }: ContentProps) => {
);
};
export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => {
export const PolicySelectionModal = ({ isOpen, onOpenChange, type }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
@@ -199,7 +209,7 @@ export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => {
subTitle="Select one or more policies to add to this role."
className="max-w-3xl"
>
<Content onClose={() => onOpenChange(false)} />
<Content onClose={() => onOpenChange(false)} type={type} />
</ModalContent>
</Modal>
);
@@ -13,6 +13,7 @@ import {
ModalContent
} from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/workspace/types";
import {
PROJECT_PERMISSION_OBJECT,
@@ -24,19 +25,23 @@ import {
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
type: ProjectType;
};
type ContentProps = {
onClose: () => void;
type: ProjectType;
};
const Content = ({ onClose }: ContentProps) => {
const Content = ({ onClose, type: projectType }: ContentProps) => {
const rootForm = useFormContext<TFormSchema>();
const [selectedTemplate, setSelectedTemplate] = useState<RoleTemplate>();
const [conflictingSubjects, setConflictingSubjects] = useState<ProjectPermissionSub[]>([]);
const [showConflictingSubjects, setShowConflictingSubjects] = useState(false);
const templates = RoleTemplates[projectType ?? ProjectType.SecretManager];
const onSubmit = (skipConflicting = false) => {
if (!selectedTemplate) {
createNotification({ type: "error", text: "Please select a template" });
@@ -121,12 +126,12 @@ const Content = ({ onClose }: ContentProps) => {
type="single"
value={selectedTemplate?.id}
onValueChange={(value) =>
setSelectedTemplate(RoleTemplates.find((template) => template.id === value))
setSelectedTemplate(templates.find((template) => template.id === value))
}
collapsible
className="w-full border-collapse"
>
{RoleTemplates.map(({ name, description, permissions, id }) => (
{templates.map(({ name, description, permissions, id }) => (
<AccordionItem
key={id}
value={id}
@@ -182,7 +187,7 @@ const Content = ({ onClose }: ContentProps) => {
);
};
export const PolicyTemplateModal = ({ isOpen, onOpenChange }: Props) => {
export const PolicyTemplateModal = ({ isOpen, onOpenChange, type }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
@@ -190,7 +195,7 @@ export const PolicyTemplateModal = ({ isOpen, onOpenChange }: Props) => {
subTitle="Select a template with prepopulated policies to get started. You can always add more policies later."
className="max-w-3xl"
>
<Content onClose={() => onOpenChange(false)} />
<Content onClose={() => onOpenChange(false)} type={type} />
</ModalContent>
</Modal>
);
@@ -1645,41 +1645,378 @@ export type RoleTemplate = {
permissions: { subject: ProjectPermissionSub; actions: string[] }[];
};
export const RoleTemplates = [
{
id: "project-manager",
name: "Project Management Policies",
description: "Grants access to manage project members and settings",
permissions: [
const projectManagerTemplate = (
additionalPermissions: RoleTemplate["permissions"] = []
): RoleTemplate => ({
id: "project-manager",
name: "Project Management Policies",
description: "Grants access to manage project members and settings",
permissions: [
{
subject: ProjectPermissionSub.AuditLogs,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.Groups,
actions: Object.values(ProjectPermissionGroupActions)
},
{
subject: ProjectPermissionSub.Member,
actions: Object.values(ProjectPermissionMemberActions)
},
{
subject: ProjectPermissionSub.Identity,
actions: Object.values(ProjectPermissionIdentityActions)
},
{
subject: ProjectPermissionSub.Project,
actions: [ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]
},
{ subject: ProjectPermissionSub.Role, actions: Object.values(ProjectPermissionActions) },
{
subject: ProjectPermissionSub.Settings,
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Edit]
},
...additionalPermissions
]
});
export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
[ProjectType.SSH]: [
{
id: "ssh-viewer",
name: "SSH Viewing Policies",
description: "Grants read access to SSH certificates and hosts",
permissions: [
{
subject: ProjectPermissionSub.SshCertificateAuthorities,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SshCertificates,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SshCertificateTemplates,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SshHosts,
actions: [ProjectPermissionSshHostActions.Read]
},
{
subject: ProjectPermissionSub.SshHostGroups,
actions: [ProjectPermissionActions.Read]
}
]
},
{
id: "ssh-cert-editor",
name: "SSH Certificate Editing Policies",
description: "Grants read and edit access to SSH certificates",
permissions: [
{
subject: ProjectPermissionSub.SshCertificateAuthorities,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SshCertificates,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SshCertificateTemplates,
actions: Object.values(ProjectPermissionActions)
}
]
},
{
id: "ssh-host-editor",
name: "SSH Host Editing Policies",
description: "Grants read and edit access to SSH hosts",
permissions: [
{
subject: ProjectPermissionSub.SshHosts,
actions: Object.values(ProjectPermissionSshHostActions)
},
{
subject: ProjectPermissionSub.SshHostGroups,
actions: Object.values(ProjectPermissionActions)
}
]
},
projectManagerTemplate()
],
[ProjectType.KMS]: [
{
id: "kms-viewer",
name: "KMS Viewing Policies",
description: "Grants read access to KMS keys and KMIP clients",
permissions: [
{
subject: ProjectPermissionSub.Cmek,
actions: [ProjectPermissionCmekActions.Read]
},
{
subject: ProjectPermissionSub.Kmip,
actions: [ProjectPermissionKmipActions.ReadClients]
}
]
},
{
id: "key-editor",
name: "KMS Key Editing Policies",
description: "Grants read and edit access to KMS keys",
permissions: [
{
subject: ProjectPermissionSub.Cmek,
actions: Object.values(ProjectPermissionCmekActions)
}
]
},
{
id: "kmip-editor",
name: "KMIP Client Editing Policies",
description: "Grants read and edit access to KMIP clients",
permissions: [
{
subject: ProjectPermissionSub.Kmip,
actions: Object.values(ProjectPermissionKmipActions)
}
]
},
projectManagerTemplate()
],
[ProjectType.CertificateManager]: [
{
id: "cert-viewer",
name: "Certificate Viewing Policies",
description: "Grants read access to certificates and related resources",
permissions: [
{
subject: ProjectPermissionSub.PkiCollections,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.PkiAlerts,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.CertificateAuthorities,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.CertificateTemplates,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.Certificates,
actions: [
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.ReadPrivateKey
]
}
]
},
{
id: "cert-editor",
name: "Certificate Editing Policies",
description: "Grants read and edit access to certificates and related resources",
permissions: [
{
subject: ProjectPermissionSub.PkiCollections,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.PkiAlerts,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.CertificateAuthorities,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.CertificateTemplates,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.Certificates,
actions: Object.values(ProjectPermissionCertificateActions)
}
]
},
projectManagerTemplate()
],
[ProjectType.SecretScanning]: [
{
id: "scanning-viewer",
name: "Secret Scanning Viewing Policies",
description: "Grants read access to data sources and findings",
permissions: [
{
subject: ProjectPermissionSub.SecretScanningDataSources,
actions: [
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSecretScanningDataSourceActions.ReadResources,
ProjectPermissionSecretScanningDataSourceActions.ReadScans
]
},
{
subject: ProjectPermissionSub.SecretScanningFindings,
actions: [ProjectPermissionSecretScanningFindingActions.Read]
},
{
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: [ProjectPermissionSecretScanningConfigActions.Read]
}
]
},
{
id: "scanning-editor",
name: "Secret Scanning Editing Policies",
description: "Grants read and edit access to data sources and findings",
permissions: [
{
subject: ProjectPermissionSub.SecretScanningDataSources,
actions: Object.values(ProjectPermissionSecretScanningDataSourceActions)
},
{
subject: ProjectPermissionSub.SecretScanningFindings,
actions: Object.values(ProjectPermissionSecretScanningFindingActions)
},
{
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: [ProjectPermissionSecretScanningConfigActions.Read]
}
]
},
projectManagerTemplate([
{
subject: ProjectPermissionSub.AuditLogs,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.Groups,
actions: Object.values(ProjectPermissionGroupActions)
},
{
subject: ProjectPermissionSub.Member,
actions: Object.values(ProjectPermissionMemberActions)
},
{
subject: ProjectPermissionSub.Identity,
actions: Object.values(ProjectPermissionIdentityActions)
},
{
subject: ProjectPermissionSub.Project,
actions: [ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]
},
{ subject: ProjectPermissionSub.Role, actions: Object.values(ProjectPermissionActions) },
{
subject: ProjectPermissionSub.Settings,
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Edit]
},
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: Object.values(ProjectPermissionSecretScanningConfigActions)
}
])
],
[ProjectType.SecretManager]: [
{
id: "secret-viewer",
name: "Secret Viewing Policies",
description: "Grants read access to secrets and related resources",
permissions: [
{
subject: ProjectPermissionSub.SecretRollback,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SecretImports,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.Secrets,
actions: [
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue
]
},
{
subject: ProjectPermissionSub.DynamicSecrets,
actions: [ProjectPermissionDynamicSecretActions.ReadRootCredential]
},
{
subject: ProjectPermissionSub.Environments,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.Tags,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SecretRotation,
actions: [ProjectPermissionSecretRotationActions.Read]
},
{
subject: ProjectPermissionSub.Integrations,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SecretSyncs,
actions: [ProjectPermissionSecretSyncActions.Read]
},
{
subject: ProjectPermissionSub.Commits,
actions: [ProjectPermissionCommitsActions.Read]
}
]
},
{
id: "secret-editor",
name: "Secret Editing Policies",
description: "Grants read and edit access to secrets and related resources",
permissions: [
{
subject: ProjectPermissionSub.Environments,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.DynamicSecrets,
actions: Object.values(ProjectPermissionDynamicSecretActions)
},
{
subject: ProjectPermissionSub.Secrets,
actions: [
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
]
},
{
subject: ProjectPermissionSub.SecretRollback,
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Create]
},
{
subject: ProjectPermissionSub.Tags,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SecretImports,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SecretRotation,
actions: Object.values(ProjectPermissionSecretRotationActions)
},
{
subject: ProjectPermissionSub.SecretFolders,
actions: [
ProjectPermissionActions.Create,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Delete
]
},
{
subject: ProjectPermissionSub.Integrations,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SecretSyncs,
actions: Object.values(ProjectPermissionSecretSyncActions)
},
{
subject: ProjectPermissionSub.Commits,
actions: Object.values(ProjectPermissionCommitsActions)
}
]
},
projectManagerTemplate([
{
subject: ProjectPermissionSub.IpAllowList,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.Kms,
actions: [ProjectPermissionActions.Edit]
},
{
subject: ProjectPermissionSub.SecretApproval,
actions: Object.values(ProjectPermissionActions)
@@ -1692,314 +2029,6 @@ export const RoleTemplates = [
subject: ProjectPermissionSub.Webhooks,
actions: Object.values(ProjectPermissionActions)
}
]
},
{
id: "ssh-viewer",
name: "SSH Viewing Policies",
description: "Grants read access to SSH certificates and hosts",
permissions: [
{
subject: ProjectPermissionSub.SshCertificateAuthorities,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SshCertificates,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SshCertificateTemplates,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SshHosts,
actions: [ProjectPermissionSshHostActions.Read]
},
{
subject: ProjectPermissionSub.SshHostGroups,
actions: [ProjectPermissionActions.Read]
}
]
},
{
id: "ssh-cert-editor",
name: "SSH Certificate Editing Policies",
description: "Grants read and edit access to SSH certificates",
permissions: [
{
subject: ProjectPermissionSub.SshCertificateAuthorities,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SshCertificates,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SshCertificateTemplates,
actions: Object.values(ProjectPermissionActions)
}
]
},
{
id: "ssh-host-editor",
name: "SSH Host Editing Policies",
description: "Grants read and edit access to SSH hosts",
permissions: [
{
subject: ProjectPermissionSub.SshHosts,
actions: Object.values(ProjectPermissionSshHostActions)
},
{
subject: ProjectPermissionSub.SshHostGroups,
actions: Object.values(ProjectPermissionActions)
}
]
},
{
id: "kms-viewer",
name: "KMS Viewing Policies",
description: "Grants read access to KMS keys and KMIP clients",
permissions: [
{
subject: ProjectPermissionSub.Cmek,
actions: [ProjectPermissionCmekActions.Read]
},
{
subject: ProjectPermissionSub.Kmip,
actions: [ProjectPermissionKmipActions.ReadClients]
}
]
},
{
id: "key-editor",
name: "KMS Key Editing Policies",
description: "Grants read and edit access to KMS keys",
permissions: [
{
subject: ProjectPermissionSub.Cmek,
actions: Object.values(ProjectPermissionCmekActions)
}
]
},
{
id: "kmip-editor",
name: "KMIP Client Editing Policies",
description: "Grants read and edit access to KMIP clients",
permissions: [
{
subject: ProjectPermissionSub.Kmip,
actions: Object.values(ProjectPermissionKmipActions)
}
]
},
{
id: "cert-viewer",
name: "Certificate Viewing Policies",
description: "Grants read access to certificates and related resources",
permissions: [
{
subject: ProjectPermissionSub.PkiCollections,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.PkiAlerts,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.CertificateAuthorities,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.CertificateTemplates,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.Certificates,
actions: [
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.ReadPrivateKey
]
}
]
},
{
id: "cert-editor",
name: "Certificate Editing Policies",
description: "Grants read and edit access to certificates and related resources",
permissions: [
{
subject: ProjectPermissionSub.PkiCollections,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.PkiAlerts,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.CertificateAuthorities,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.CertificateTemplates,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.Certificates,
actions: Object.values(ProjectPermissionCertificateActions)
}
]
},
{
id: "scanning-viewer",
name: "Secret Scanning Viewing Policies",
description: "Grants read access to data sources and findings",
permissions: [
{
subject: ProjectPermissionSub.SecretScanningDataSources,
actions: [
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSecretScanningDataSourceActions.ReadResources,
ProjectPermissionSecretScanningDataSourceActions.ReadScans
]
},
{
subject: ProjectPermissionSub.SecretScanningFindings,
actions: [ProjectPermissionSecretScanningFindingActions.Read]
},
{
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: [ProjectPermissionSecretScanningConfigActions.Read]
}
]
},
{
id: "scanning-editor",
name: "Secret Scanning Editing Policies",
description: "Grants read and edit access to data sources and findings",
permissions: [
{
subject: ProjectPermissionSub.SecretScanningDataSources,
actions: Object.values(ProjectPermissionSecretScanningDataSourceActions)
},
{
subject: ProjectPermissionSub.SecretScanningFindings,
actions: Object.values(ProjectPermissionSecretScanningFindingActions)
},
{
subject: ProjectPermissionSub.SecretScanningConfigs,
actions: [ProjectPermissionSecretScanningConfigActions.Read]
}
]
},
{
id: "secret-viewer",
name: "Secret Viewing Policies",
description: "Grants read access to secrets and related resources",
permissions: [
{
subject: ProjectPermissionSub.SecretRollback,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SecretImports,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.Secrets,
actions: [
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue
]
},
{
subject: ProjectPermissionSub.DynamicSecrets,
actions: [ProjectPermissionDynamicSecretActions.ReadRootCredential]
},
{
subject: ProjectPermissionSub.Environments,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.Tags,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SecretRotation,
actions: [ProjectPermissionSecretRotationActions.Read]
},
{
subject: ProjectPermissionSub.Integrations,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.SecretSyncs,
actions: [ProjectPermissionSecretSyncActions.Read]
},
{
subject: ProjectPermissionSub.Commits,
actions: [ProjectPermissionCommitsActions.Read]
}
]
},
{
id: "secret-editor",
name: "Secret Editing Policies",
description: "Grants read and edit access to secrets and related resources",
permissions: [
{
subject: ProjectPermissionSub.Environments,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.DynamicSecrets,
actions: Object.values(ProjectPermissionDynamicSecretActions)
},
{
subject: ProjectPermissionSub.Secrets,
actions: [
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
]
},
{
subject: ProjectPermissionSub.SecretRollback,
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Create]
},
{
subject: ProjectPermissionSub.Tags,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SecretImports,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SecretRotation,
actions: Object.values(ProjectPermissionSecretRotationActions)
},
{
subject: ProjectPermissionSub.SecretFolders,
actions: [
ProjectPermissionActions.Create,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Delete
]
},
{
subject: ProjectPermissionSub.Integrations,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.SecretSyncs,
actions: Object.values(ProjectPermissionSecretSyncActions)
},
{
subject: ProjectPermissionSub.Commits,
actions: Object.values(ProjectPermissionCommitsActions)
}
]
}
];
])
]
};
@@ -7,6 +7,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { getProjectBaseURL } from "@app/helpers/project";
import {
useCreateProjectRole,
useGetProjectRoleBySlug,
@@ -100,7 +101,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
});
navigate({
to: "/projects/$projectId/roles/$roleSlug",
to: `${getProjectBaseURL(currentWorkspace.type)}/roles/$roleSlug` as const,
params: {
roleSlug: newRole.slug,
projectId
@@ -4,7 +4,6 @@ import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
import { faSave } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { AccessTree } from "@app/components/permissions";
@@ -14,6 +13,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AddPoliciesButton } from "./AddPoliciesButton";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
@@ -121,6 +121,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
(role?.slug ?? "") as ProjectMembershipRole
);
const isSecretManagerProject = currentWorkspace.type === ProjectType.SecretManager;
const permissions = form.watch("permissions");
const formattedPermissions = useMemo(
@@ -163,7 +165,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
<Button
colorSchema="secondary"
type="submit"
className={twMerge("h-10 border")}
className="h-10 border"
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
@@ -171,7 +173,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
Save
</Button>
<div className="ml-2 border-l border-mineshaft-500 pl-4">
<AddPoliciesButton isDisabled={isDisabled} />
<AddPoliciesButton isDisabled={isDisabled} projectType={currentWorkspace.type} />
</div>
</div>
)}
@@ -206,7 +208,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
</div>
</FormProvider>
</form>
{showAccessTree && (
{isSecretManagerProject && showAccessTree && (
<AccessTree
permissions={formattedPermissions}
subject={showAccessTree}
@@ -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/cert-management/$projectId/_cert-manager-layout/roles/$roleSlug"
)({
component: RoleDetailsBySlugPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/cert-management/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Roles
}
})
},
{
label: "Roles"
}
]
};
}
});

Some files were not shown because too many files have changed in this diff Show More