Compare commits

...

36 Commits

Author SHA1 Message Date
Daniel Hougaard
6cdc71b9b1 Merge pull request #3023 from Infisical/fix-org-sidebar-check
Fix: Correct Display Org Sidebar Check
2025-01-22 03:02:43 +01:00
Scott Wilson
f88d6a183f fix: correct display org sidebar check 2025-01-21 17:46:14 -08:00
Maidul Islam
86acf88a13 Merge pull request #3021 from akhilmhdh/fix/project-delete
fix: resolved org sidebar not showing in org member detail window
2025-01-21 15:14:12 -05:00
=
63c7c39e21 fix: resolved org sidebar not showing in member detail window and other detail window 2025-01-22 01:39:44 +05:30
Maidul Islam
151edc7efa Merge pull request #3020 from Infisical/remove-enterprise-identity-limit
Remove Enterprise Member and Identity Limits for SAML/LDAP Login
2025-01-21 13:17:16 -05:00
Tuan Dang
5fa7f56285 Remove member and identity limits for enterprise saml and ldap login case 2025-01-21 10:12:23 -08:00
Scott Wilson
810b27d121 Merge pull request #3019 from Infisical/audit-log-stream-setup-error-improvement
Improvement: Propagate Upstream Error on Audit Log Stream Setup Fail
2025-01-21 10:06:23 -08:00
Scott Wilson
51fe7450ae improvement: propogate upstream error on audit log stream setup 2025-01-21 09:54:33 -08:00
Maidul Islam
938c06a2ed Merge pull request #3018 from Infisical/misc/added-more-scoping-for-namespace
misc: added more scoping logic for namespace installation
2025-01-21 11:09:45 -05:00
Sheen Capadngan
38d431ec77 misc: added more scoping logic for namespace installation 2025-01-21 23:57:11 +08:00
Maidul Islam
d202fdf5c8 Merge pull request #3017 from akhilmhdh/fix/project-delete
fix: resolved failing project delete
2025-01-21 10:42:59 -05:00
=
f1b2028542 fix: resolved failing project delete 2025-01-21 20:55:19 +05:30
Sheen
5c9b46dfba Merge pull request #3015 from Infisical/misc/address-scim-email-verification-patch-issue
misc: address scim email verification patch issue
2025-01-21 20:23:08 +08:00
Sheen Capadngan
a516e50984 misc: address scim email verification patch issue 2025-01-21 17:01:34 +08:00
Sheen
569439f208 Merge pull request #2999 from Infisical/misc/added-handling-for-case-enforcement
misc: added handling of secret case enforcement
2025-01-21 13:58:48 +08:00
Maidul Islam
9afc282679 Update deployment-pipeline.yml 2025-01-21 00:49:31 -05:00
Maidul Islam
8db85cfb84 Add slack alert 2025-01-21 00:04:14 -05:00
Maidul Islam
664b2f0089 add concurrency to git workflow 2025-01-20 23:52:47 -05:00
Maidul Islam
5e9bd3a7c6 Merge pull request #3014 from Infisical/expanded-secret-overview-adjustment
Fix: Adjust Secret Overview Expanded Secret View
2025-01-20 21:39:39 -05:00
Maidul Islam
2c13af6db3 correct helm verion 2025-01-20 21:37:13 -05:00
Scott Wilson
ec9171d0bc fix: adjust secret overview expanded secret view to accomodate nav restructure 2025-01-20 18:30:54 -08:00
Vlad Matsiiako
81362bec8f Merge pull request #3013 from Infisical/daniel/cli-fix-2
fix: saml redirect failing due to blocked pop-ups
2025-01-20 13:21:32 -08:00
Maidul Islam
3c97c45455 others => other 2025-01-20 15:26:15 -05:00
Maidul Islam
4f015d77fb Merge pull request #3003 from akhilmhdh/feat/nav-restructure
Navigation restructure
2025-01-20 15:07:39 -05:00
=
78e894c2bb feat: changed user icon 2025-01-21 01:34:21 +05:30
=
23513158ed feat: updated nav ui based on feedback 2025-01-20 22:59:00 +05:30
=
934ef8ab27 feat: resolved plan text generator 2025-01-20 22:59:00 +05:30
=
23e9c52f67 feat: added missing breadcrumbs for integration pages 2025-01-20 22:59:00 +05:30
=
e276752e7c feat: removed trailing comma from ui 2025-01-20 22:59:00 +05:30
=
01ae19fa2b feat: completed all nav restructing and breadcrumbs cleanup 2025-01-20 22:58:59 +05:30
=
9df8cf60ef feat: finished breadcrumbs for all project types except secret manager 2025-01-20 22:58:59 +05:30
=
1b1fe2a700 feat: completed org breadcrumbs 2025-01-20 22:58:59 +05:30
=
338961480c feat: resolved accessibility issue with menu 2025-01-20 22:58:59 +05:30
=
03debcab5a feat: completed sidebar changes 2025-01-20 22:58:59 +05:30
Sheen Capadngan
bd459d994c misc: finalized error message 2025-01-16 19:55:21 +08:00
Sheen Capadngan
440f93f392 misc: added handling for case enforcement 2025-01-16 19:10:00 +08:00
229 changed files with 6246 additions and 3804 deletions

View File

@@ -5,6 +5,10 @@ permissions:
id-token: write
contents: read
concurrency:
group: "infisical-core-deployment"
cancel-in-progress: false
jobs:
infisical-tests:
name: Integration tests
@@ -113,10 +117,6 @@ jobs:
steps:
- uses: twingate/github-action@v1
with:
# The Twingate Service Key used to connect Twingate to the proper service
# Learn more about [Twingate Services](https://docs.twingate.com/docs/services)
#
# Required
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Checkout code
uses: actions/checkout@v2
@@ -159,6 +159,31 @@ jobs:
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true
- name: Post slack message
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*Deployment Status Update*: ${{ job.status }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Deployment Status Update*: ${{ job.status }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Application:*\nInfisical Core"
- type: "mrkdwn"
text: "*Instance Type:*\nShared Infisical Cloud"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Region:*\nUS"
- type: "mrkdwn"
text: "*Git Tag:*\n<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"
production-eu:
name: EU production deploy
@@ -210,3 +235,28 @@ jobs:
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true
- name: Post slack message
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*Deployment Status Update*: ${{ job.status }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Deployment Status Update*: ${{ job.status }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Application:*\nInfisical Core"
- type: "mrkdwn"
text: "*Instance Type:*\nShared Infisical Cloud"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Region:*\nEU"
- type: "mrkdwn"
text: "*Git Tag:*\n<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"

View File

@@ -0,0 +1,33 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization");
const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization");
await knex.schema.alterTable(TableName.Project, (t) => {
if (!hasEnforceCapitalizationCol) {
t.boolean("enforceCapitalization").defaultTo(false).notNullable();
}
if (hasAutoCapitalizationCol) {
t.boolean("autoCapitalization").defaultTo(false).alter();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization");
const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization");
await knex.schema.alterTable(TableName.Project, (t) => {
if (hasEnforceCapitalizationCol) {
t.dropColumn("enforceCapitalization");
}
if (hasAutoCapitalizationCol) {
t.boolean("autoCapitalization").defaultTo(true).alter();
}
});
}

View File

@@ -13,7 +13,7 @@ export const ProjectsSchema = z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
autoCapitalization: z.boolean().default(true).nullable().optional(),
autoCapitalization: z.boolean().default(false).nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
@@ -25,7 +25,8 @@ export const ProjectsSchema = z.object({
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional(),
type: z.string()
type: z.string(),
enforceCapitalization: z.boolean().default(false)
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -93,7 +93,7 @@ export const auditLogStreamServiceFactory = ({
}
)
.catch((err) => {
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` });
});
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
const logStream = await auditLogStreamDAL.create({

View File

@@ -476,14 +476,14 @@ export const ldapConfigServiceFactory = ({
});
} else {
const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."

View File

@@ -421,14 +421,14 @@ export const samlConfigServiceFactory = ({
});
} else {
const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."

View File

@@ -531,7 +531,7 @@ export const scimServiceFactory = ({
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : true
isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
},
tx
);

View File

@@ -1267,9 +1267,10 @@ export const secretApprovalRequestServiceFactory = ({
type: SecretType.Shared
}))
);
if (secrets.length)
if (secrets.length !== secretsWithNewName.length)
throw new NotFoundError({
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
message: `Secret does not exist: ${secrets.map((el) => el.key).join(",")}`
});
}

View File

@@ -895,7 +895,8 @@ export const registerRoutes = async (
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL,
projectTemplateService
projectTemplateService,
groupProjectDAL
});
const projectEnvService = projectEnvServiceFactory({

View File

@@ -29,6 +29,7 @@ import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
@@ -100,7 +101,8 @@ type TProjectServiceFactoryDep = {
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
@@ -120,7 +122,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
@@ -169,7 +171,8 @@ export const projectServiceFactory = ({
projectBotDAL,
projectSlackConfigDAL,
slackIntegrationDAL,
projectTemplateService
projectTemplateService,
groupProjectDAL
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -444,13 +447,32 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
const deletedProject = await projectDAL.transaction(async (tx) => {
// delete these so that project custom roles can be deleted in cascade effect
// direct deletion of project without these will cause fk error
await projectMembershipDAL.delete({ projectId: project.id }, tx);
await groupProjectDAL.delete({ projectId: project.id }, tx);
const delProject = await projectDAL.deleteById(project.id, tx);
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null);
// akhilmhdh: before removing those kms checking any other project uses it
// happened due to project split
if (delProject.kmsCertificateKeyId) {
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
const projectsLinkedToForiegnKey = await projectDAL.find(
{ kmsCertificateKeyId: delProject.kmsCertificateKeyId },
{ tx }
);
if (!projectsLinkedToForiegnKey.length) {
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
}
}
if (delProject.kmsSecretManagerKeyId) {
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx);
const projectsLinkedToForiegnKey = await projectDAL.find(
{ kmsSecretManagerKeyId: delProject.kmsSecretManagerKeyId },
{ tx }
);
if (!projectsLinkedToForiegnKey.length) {
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx);
}
}
// Delete the org membership for the ghost user if it's found.
if (projectGhostUser) {
@@ -544,8 +566,10 @@ export const projectServiceFactory = ({
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
autoCapitalization: update.autoCapitalization
autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization
});
return updatedProject;
};
@@ -567,7 +591,11 @@ export const projectServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization });
const updatedProject = await projectDAL.updateById(projectId, {
autoCapitalization,
enforceCapitalization: autoCapitalization
});
return updatedProject;
};

View File

@@ -88,7 +88,7 @@ type TSecretServiceFactoryDep = {
secretDAL: TSecretDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug" | "findById">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick<
TSecretFolderDALFactory,
@@ -1466,6 +1466,16 @@ export const secretServiceFactory = ({
secretMetadata
}: TCreateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
if (secretName !== secretName.toUpperCase()) {
throw new BadRequestError({
message:
"Secret name must be in UPPERCASE per project requirements. You can disable this requirement in project settings."
});
}
}
const policy =
actor === ActorType.USER && type === SecretType.Shared
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
@@ -1609,6 +1619,16 @@ export const secretServiceFactory = ({
secretMetadata
}: TUpdateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
if (newSecretName && newSecretName !== newSecretName.toUpperCase()) {
throw new BadRequestError({
message:
"Secret name must be in UPPERCASE per project requirements. You can disable this requirement in project settings."
});
}
}
const policy =
actor === ActorType.USER && type === SecretType.Shared
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
@@ -1858,7 +1878,23 @@ export const secretServiceFactory = ({
actor === ActorType.USER
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
: undefined;
if (shouldUseSecretV2Bridge) {
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
const caseViolatingSecretKeys = inputSecrets
.filter((sec) => sec.secretKey !== sec.secretKey.toUpperCase())
.map((sec) => sec.secretKey);
if (caseViolatingSecretKeys.length) {
throw new BadRequestError({
message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join(
", "
)}. You can disable this requirement in project settings`
});
}
}
if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({
policy,
@@ -1987,6 +2023,21 @@ export const secretServiceFactory = ({
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
: undefined;
if (shouldUseSecretV2Bridge) {
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
const caseViolatingSecretKeys = inputSecrets
.filter((sec) => sec.newSecretName && sec.newSecretName !== sec.newSecretName.toUpperCase())
.map((sec) => sec.newSecretName);
if (caseViolatingSecretKeys.length) {
throw new BadRequestError({
message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join(
", "
)}. You can disable this requirement in project settings`
});
}
}
if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({
policy,

View File

@@ -301,8 +301,8 @@
"project-id-description2": "For more guidance, including code snipets for various languages and frameworks, see ",
"auto-generated": "This is your project's auto-generated unique identifier. It can't be changed.",
"docs": "Infisical Docs",
"auto-capitalization": "Auto Capitalization",
"auto-capitalization-description": "According to standards, Infisical will automatically capitalize your keys. If you want to disable this feature, you can do so here."
"enforce-capitalization": "Enforce Capitalization",
"enforce-capitalization-description": "According to standards, Infisical enforces uppercase secret keys. If you want to disable this feature, you can do so here."
}
},
"signup": {

View File

@@ -289,8 +289,8 @@
"project-id-description2": "Para más guías, incluyendo ejemplos de código en diferentes lenguajes y frameworks, visita ",
"auto-generated": "Este es el ID único y autogenerado de proyecto. No se puede modificar.",
"docs": "Documentación de Infisical",
"auto-capitalization": "Mayúsculas automáticas",
"auto-capitalization-description": "De acuerdo con los estándares, Infisical pondrá en mayúsculas tus claves. Si quieres desactivar esta funcionalidad, lo puedes hacer aquí."
"enforce-capitalization": "Hacer cumplir la capitalización",
"enforce-capitalization-description": "Según los estándares, Infisical aplica claves secretas en mayúsculas. Si desea desactivar esta función, puede hacerlo aquí."
}
},
"signup": {

View File

@@ -266,8 +266,8 @@
"project-id-description2": "Para obter mais orientações, incluindo trechos de código para várias linguagens e frameworks, consulte ",
"auto-generated": "Este é o identificador exclusivo - gerado automaticamente - do seu projeto. Não pode ser alterado.",
"docs": "Documentação do Infisical",
"auto-capitalization": "Converter em caixa alta automaticamente",
"auto-capitalization-description": "Por padrão, Infisical converte automaticamente as chaves em caixa alta. Se você quiser desativar essa funcionalidade, pode fazê-lo aqui."
"enforce-capitalization": "Aplicar capitalização",
"enforce-capitalization-description": "De acordo com os padrões, o Infisical impõe chaves secretas em letras maiúsculas. Se quiser desabilitar esse recurso, você pode fazer isso aqui."
}
},
"signup": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,87 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { useTimedReset } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { createNotification } from "../notifications";
import { IconButton, Tooltip } from "../v2";
type Props = {
secretPathSegments: string[];
selectedPathSegmentIndex: number;
environmentSlug: string;
projectId: string;
};
export const SecretDashboardPathBreadcrumb = ({
secretPathSegments,
selectedPathSegmentIndex,
environmentSlug,
projectId
}: Props) => {
const [, isCopying, setIsCopying] = useTimedReset({
initialState: false
});
const newSecretPath = `/${secretPathSegments.slice(0, selectedPathSegmentIndex + 1).join("/")}`;
const isLastItem = secretPathSegments.length === selectedPathSegmentIndex + 1;
const folderName = secretPathSegments.at(selectedPathSegmentIndex);
return (
<div className="flex items-center space-x-3">
{isLastItem ? (
<div className="group flex items-center space-x-2">
<span
className={twMerge(
"text-sm font-semibold transition-all",
isCopying ? "text-bunker-200" : "text-bunker-300"
)}
>
{folderName}
</span>
<Tooltip className="relative right-2" position="bottom" content="Copy secret path">
<IconButton
variant="plain"
ariaLabel="copy"
onClick={() => {
if (isCopying) return;
setIsCopying(true);
navigator.clipboard.writeText(newSecretPath);
createNotification({
text: "Copied secret path to clipboard",
type: "info"
});
}}
className="opacity-0 transition duration-75 hover:bg-bunker-100/10 group-hover:opacity-100"
>
<FontAwesomeIcon
icon={!isCopying ? faCopy : faCheck}
size="sm"
className="cursor-pointer"
/>
</IconButton>
</Tooltip>
</div>
) : (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
params={{
projectId,
envSlug: environmentSlug
}}
search={(query) => ({ ...query, secretPath: newSecretPath })}
className={twMerge(
"text-sm font-semibold transition-all hover:text-primary",
isCopying && "text-primary"
)}
>
{folderName}
</Link>
)}
</div>
);
};

View File

@@ -0,0 +1,217 @@
/* eslint-disable react/prop-types */
import React from "react";
import { faCaretDown, faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, ReactNode } from "@tanstack/react-router";
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
import { twMerge } from "tailwind-merge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger
} from "../Dropdown";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={twMerge(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-bunker-100 sm:gap-2.5",
className
)}
{...props}
/>
)
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li
ref={ref}
className={twMerge("inline-flex items-center gap-1.5 font-medium", className)}
{...props}
/>
)
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<"div"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
return (
<div
ref={ref}
className={twMerge("transition-colors hover:text-primary-400", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={twMerge("font-normal text-bunker-200 last:text-bunker-300", className)}
{...props}
/>
)
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={twMerge("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}
>
{children ?? <FontAwesomeIcon icon={faChevronRight} />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={twMerge("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<FontAwesomeIcon icon={faEllipsis} className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
enum BreadcrumbTypes {
Dropdown = "dropdown",
Component = "component"
}
export type TBreadcrumbFormat =
| {
type: BreadcrumbTypes.Dropdown;
label: string;
dropdownTitle?: string;
links: { label: string; link: LinkComponentProps }[];
}
| {
type: BreadcrumbTypes.Component;
component: ReactNode;
}
| {
type: undefined;
link?: LinkComponentProps;
label: string;
icon?: ReactNode;
};
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
<div className="mx-auto max-w-7xl py-4 capitalize text-white">
<Breadcrumb>
<BreadcrumbList>
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage;
if (el.type === BreadcrumbTypes.Dropdown) {
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<DropdownMenu>
<DropdownMenuTrigger>
<BreadcrumbItem>
<BreadcrumbSegment>
{el.label} <FontAwesomeIcon icon={faCaretDown} size="sm" />
</BreadcrumbSegment>
</BreadcrumbItem>
</DropdownMenuTrigger>
<DropdownMenuContent>
{el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>}
{el.links.map((i, dropIndex) => (
<Link
{...i.link}
key={`breadcrumb-group-${index + 1}-dropdown-${dropIndex + 1}`}
>
<DropdownMenuItem>{i.label}</DropdownMenuItem>
</Link>
))}
</DropdownMenuContent>
</DropdownMenu>
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
}
if (el.type === BreadcrumbTypes.Component) {
const Component = el.component;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<BreadcrumbItem>
<BreadcrumbSegment>
<Component />
</BreadcrumbSegment>
</BreadcrumbItem>
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
}
const Icon = el?.icon;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
{"link" in el && isNotLastCrumb ? (
<Link {...el.link}>
<BreadcrumbItem>
<BreadcrumbLink className="inline-flex items-center gap-1.5">
{Icon && <Icon />}
{el.label}
</BreadcrumbLink>
</BreadcrumbItem>
</Link>
) : (
<BreadcrumbItem>
<BreadcrumbPage className="inline-flex items-center gap-1.5">
{Icon && <Icon />}
{el.label}
</BreadcrumbPage>
</BreadcrumbItem>
)}
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
</div>
);
export {
Breadcrumb,
BreadcrumbContainer,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbTypes
};

View File

@@ -0,0 +1 @@
export * from "./Breadcrumb";

View File

@@ -1,6 +1,5 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
export type MenuProps = {
@@ -30,7 +29,7 @@ export const MenuItem = <T extends ElementType = "button">({
className,
isDisabled,
isSelected,
as: Item = "button",
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
@@ -38,46 +37,40 @@ export const MenuItem = <T extends ElementType = "button">({
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
<li
className={twMerge(
"duration-50 group mt-0.5 flex cursor-pointer flex-col rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
>
<motion.span className="flex w-full flex-row items-center justify-start rounded-sm">
<Item
type="button"
role="menuitem"
className="relative flex items-center"
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "visisble" : "invisible"
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
/>
{icon && (
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
/>
</div>
)}
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}
</motion.span>
</li>
</div>
<Item
type="button"
role="menuitem"
className={twMerge(
"duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
ref={inputRef}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
{...props}
>
<div
className={`${
isSelected ? "visisble" : "invisible"
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
/>
{icon && (
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
/>
</div>
)}
<span className="flex-grow text-left">{children}</span>
{description && <span className="mt-2 text-xs">{description}</span>}
</Item>
);
};
@@ -90,7 +83,7 @@ export type MenuGroupProps = {
export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
<>
<li className="p-2 text-xs text-gray-400">{title}</li>
<li className="px-2 pt-3 text-xs uppercase text-gray-400">{title}</li>
{children}
</>
);

View File

@@ -0,0 +1,19 @@
import { ReactNode } from "@tanstack/react-router";
type Props = {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
};
export const PageHeader = ({ title, description, children }: Props) => (
<div className="mb-4">
<div className="flex w-full justify-between">
<div>
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
</div>
<div>{children}</div>
</div>
<div className="mt-2 text-gray-400">{description}</div>
</div>
);

View File

@@ -0,0 +1 @@
export { PageHeader } from "./PageHeader";

View File

@@ -2,6 +2,7 @@
export * from "./Accordion";
export * from "./Alert";
export * from "./Badge";
export * from "./Breadcrumb";
export * from "./Button";
export * from "./Card";
export * from "./Checkbox";
@@ -21,6 +22,7 @@ export * from "./Input";
export * from "./Menu";
export * from "./Modal";
export * from "./NoticeBanner";
export * from "./PageHeader";
export * from "./Pagination";
export * from "./Popoverv2";
export * from "./SecretInput";

View File

@@ -18,261 +18,261 @@ export const ROUTE_PATHS = Object.freeze({
Organization: {
SecretScanning: setRoute(
"/organization/secret-scanning",
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning"
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
),
SettingsPage: setRoute(
"/organization/settings",
"/_authenticate/_inject-org-details/organization/_layout/settings"
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
),
GroupDetailsByIDPage: setRoute(
"/organization/groups/$groupId",
"/_authenticate/_inject-org-details/organization/_layout/groups/$groupId"
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
),
IdentityDetailsByIDPage: setRoute(
"/organization/identities/$identityId",
"/_authenticate/_inject-org-details/organization/_layout/identities/$identityId"
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId"
),
UserDetailsByIDPage: setRoute(
"/organization/members/$membershipId",
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId"
"/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId"
),
AccessControlPage: setRoute(
"/organization/access-management",
"/_authenticate/_inject-org-details/organization/_layout/access-management"
"/_authenticate/_inject-org-details/_org-layout/organization/access-management"
),
RoleByIDPage: setRoute(
"/organization/roles/$roleId",
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId"
"/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
),
AppConnections: {
GithubOauthCallbackPage: setRoute(
"/organization/app-connections/github/oauth/callback",
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback"
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
)
}
},
SecretManager: {
ApprovalPage: setRoute(
"/secret-manager/$projectId/approval",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/approval"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval"
),
SecretDashboardPage: setRoute(
"/secret-manager/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
),
OverviewPage: setRoute(
"/secret-manager/$projectId/overview",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/overview"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
),
IntegrationDetailsByIDPage: setRoute(
"/secret-manager/$projectId/integrations/$integrationId",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
),
Integratons: {
SelectIntegrationAuth: setRoute(
"/secret-manager/$projectId/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth"
),
HerokuOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
),
HerokuConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/heroku/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
),
AwsParameterStoreConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
),
AwsSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
),
AzureAppConfigurationsOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
),
AzureAppConfigurationsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
),
AzureDevopsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
),
AzureKeyVaultAuthorizePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
),
AzureKeyVaultOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
),
AzureKeyVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
),
BitbucketOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
),
BitbucketConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
),
ChecklyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/checkly/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
),
CircleConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/circleci/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
),
CloudflarePagesConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
),
DigitalOceanAppPlatformConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
),
CloudflareWorkersConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
),
CodefreshConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/codefresh/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
),
GcpSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
),
GcpSecretManagerOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
),
GithubConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/github/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
),
GithubOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
),
GitlabConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
),
GitlabOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
),
VercelOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
),
VercelConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/vercel/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create"
),
FlyioConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/flyio/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
),
HashicorpVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
),
HasuraCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
),
LaravelForgeConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
),
NorthflankConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/northflank/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create"
),
RailwayConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/railway/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create"
),
RenderConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/render/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/render/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/render/create"
),
RundeckConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/rundeck/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create"
),
WindmillConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/windmill/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create"
),
TravisCIConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/travisci/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create"
),
TerraformCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
),
TeamcityConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/teamcity/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create"
),
SupabaseConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/supabase/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create"
),
OctopusDeployCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
),
DatabricksConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/databricks/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
),
QoveryConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/qovery/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create"
),
Cloud66ConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
),
NetlifyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/netlify/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create"
),
NetlifyOuathCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
)
}
},
CertManager: {
CertAuthDetailsByIDPage: setRoute(
"/cert-manager/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
),
OverviewPage: setRoute(
"/cert-manager/$projectId/overview",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview"
),
PkiCollectionDetailsByIDPage: setRoute(
"/cert-manager/$projectId/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
)
},
Ssh: {
SshCaByIDPage: setRoute(
"/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId"
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId"
)
},
Public: {

View File

@@ -30,8 +30,9 @@ export const useToggle = (initialState = false): UseToggleReturn => {
const timedToggle = useCallback((timeout = 2000) => {
setValue((prev) => !prev);
setTimeout(() => {
const timeoutRef = setTimeout(() => {
setValue(false);
clearTimeout(timeoutRef);
}, timeout);
}, []);

View File

@@ -14,7 +14,7 @@ import { envConfig } from "@app/config/env";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/SidebarFooter/SidebarFooter";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
export const AdminLayout = () => {
const { t } = useTranslation();

View File

@@ -1,174 +1,129 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-router";
import { Link, linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Mfa } from "@app/components/auth/Mfa";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Menu, MenuItem } from "@app/components/v2";
import { useUser } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { useSelectOrganization, workspaceKeys } from "@app/hooks/api";
import { authKeys } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import {
BreadcrumbContainer,
Menu,
MenuGroup,
MenuItem,
TBreadcrumbFormat
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { SidebarFooter } from "./components/SidebarFooter";
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
import { SidebarHeader } from "./components/SidebarHeader";
export const OrganizationLayout = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { user } = useUser();
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const location = useLocation();
const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
const breadcrumbs =
isOrganizationSpecificPage && matches && "breadcrumbs" in matches
? matches.breadcrumbs
: undefined;
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { mutateAsync: selectOrganization } = useSelectOrganization();
const navigate = useNavigate();
const router = useRouter();
const queryClient = useQueryClient();
const { t } = useTranslation();
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
const shouldShowOrgSidebar =
location.pathname.startsWith("/organization") &&
!(
[
linkOptions({ to: "/organization/secret-manager/overview" }).to,
linkOptions({ to: "/organization/cert-manager/overview" }).to,
linkOptions({ to: "/organization/ssh/overview" }).to,
linkOptions({ to: "/organization/kms/overview" }).to,
linkOptions({ to: "/organization/secret-scanning" }).to,
linkOptions({ to: "/organization/secret-sharing" }).to
] as string[]
).includes(location.pathname);
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<SidebarHeader onChangeOrg={handleOrgChange} />
<div className="px-1">
<Menu className="mt-4">
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="sliding-carousel">
Secret Management
</MenuItem>
)}
</Link>
<Link to={`/organization/${ProjectType.CertificateManager}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="note">
Cert Management
</MenuItem>
)}
</Link>
<Link to={`/organization/${ProjectType.KMS}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="unlock">
Key Management
</MenuItem>
)}
</Link>
<Link to={`/organization/${ProjectType.SSH}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="verified">
SSH
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="secret-scan" className="text-white">
Secret Scanning
</MenuItem>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Secret Sharing
</MenuItem>
)}
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
<MinimizedOrgSidebar />
<AnimatePresence mode="popLayout">
{shouldShowOrgSidebar && (
<motion.div
key="menu-list-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="p-2 pt-3">
<SidebarHeader />
</div>
<Menu>
<MenuGroup title="Organization Control">
<Link to="/organization/audit-logs">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="spinning-coin">
Usage & Billing
<MenuItem isSelected={isActive} icon="moving-block">
Audit Logs
</MenuItem>
)}
</Link>
)}
<Link to="/organization/audit-logs">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="moving-block">
Audit Logs
</MenuItem>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="spinning-coin">
Usage & Billing
</MenuItem>
)}
</Link>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Organization Settings
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Other">
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Organization Settings
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
</div>
<SidebarFooter />
</nav>
</aside>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
</nav>
</motion.div>
)}
</AnimatePresence>
<main
className={twMerge(
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
!isOrganizationSpecificPage && "overflow-hidden p-0"
)}
>
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet />
</main>
</div>
</div>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">

View File

@@ -0,0 +1,63 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2";
export const MenuIconButton = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
lottieIconMode = "forward",
...props
}: MenuItemProps<T> &
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
);
};

View File

@@ -0,0 +1 @@
export { MenuIconButton } from "./MenuIconButton";

View File

@@ -0,0 +1,488 @@
import { useState } from "react";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faArrowUpRightFromSquare,
faBook,
faCheck,
faCog,
faEnvelope,
faInfinity,
faInfo,
faInfoCircle,
faMoneyBill,
faSignOut,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
import { Mfa } from "@app/components/auth/Mfa";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetOrganizations,
useGetOrgTrialUrl,
useLogoutUser,
useSelectOrganization,
workspaceKeys
} from "@app/hooks/api";
import { authKeys } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.dynamicSecret) return "Enterprise Plan";
if (subscription.pitRecovery) return "Pro Plan";
return "Free Plan";
};
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:support@infisical.com"
]
];
export const MinimizedOrgSidebar = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { subscription } = useSubscription();
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { data: orgs } = useGetOrganizations();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { mutateAsync: selectOrganization } = useSelectOrganization();
const navigate = useNavigate();
const router = useRouter();
const location = useLocation();
const queryClient = useQueryClient();
const isMoreSelected = (
[
linkOptions({ to: "/organization/access-management" }).to,
linkOptions({ to: "/organization/settings" }).to,
linkOptions({ to: "/organization/audit-logs" }).to
] as string[]
).includes(location.pathname);
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return (
<>
<aside
className="dark z-10 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150"
style={{ width: "72px" }}
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
<DropdownMenu modal>
<DropdownMenuTrigger asChild>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="right"
className="p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "320px" }}
>
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
{currentOrg?.name.charAt(0)}
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{currentOrg?.name}
</div>
<div className="text-xs text-mineshaft-400">{getPlan(subscription)}</div>
</div>
</div>
</div>
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">
organizations
</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-1 px-1">
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.SecretManager}`)
}
icon="sliding-carousel"
>
Secret Manager
</MenuIconButton>
)}
</Link>
<Link to="/organization/cert-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.CertificateManager}`)
}
icon="note"
>
Cert Manager
</MenuIconButton>
)}
</Link>
<Link to="/organization/kms/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.KMS}`)
}
icon="unlock"
>
KMS
</MenuIconButton>
)}
</Link>
<Link to="/organization/ssh/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.SSH}`)
}
icon="verified"
>
SSH
</MenuIconButton>
)}
</Link>
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
Secret Scanning
</MenuIconButton>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Secret Sharing
</MenuIconButton>
)}
</Link>
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full">
<MenuIconButton
lottieIconMode="reverse"
icon="three-ellipsis"
isSelected={isMoreSelected}
>
More
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="right" className="p-1">
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUsers} />}>
Access Control
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faMoneyBill} />}>
Usage & Billing
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/audit-logs">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faBook} />}>
Audit Logs
</DropdownMenuItem>
</Link>
<Link to="/organization/settings">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faCog} />}>
Organization Settings
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
<DropdownMenu>
<DropdownMenuTrigger className="w-full">
<MenuIconButton>
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
Support
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<button
type="button"
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
className="mt-1.5 w-full"
>
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon icon={faInfinity} className="ml-0.5 mr-3 py-2 text-primary" />
Start Free Pro Trial
</div>
</button>
)}
<DropdownMenu>
<DropdownMenuTrigger className="w-full" asChild>
<div>
<MenuIconButton icon="user">User</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="p-2">
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{user?.firstName} {user?.lastName}
</div>
<div className="text-xs text-mineshaft-300">{user.email}</div>
</div>
</div>
</div>
<Link to="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
{user?.superAdmin && (
<Link to="/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Server Admin Console
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
</aside>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};

View File

@@ -0,0 +1 @@
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";

View File

@@ -1,130 +0,0 @@
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faBook,
faEnvelope,
faInfinity,
faInfo,
faPlus,
faQuestion
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { WishForm } from "@app/components/features/WishForm";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription } from "@app/context";
import { useGetOrgTrialUrl } from "@app/hooks/api";
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:support@infisical.com"
]
];
export const SidebarFooter = () => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { mutateAsync } = useGetOrgTrialUrl();
return (
<div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
<Link
to="/organization/access-management"
search={{
action: "invite"
}}
className="w-full"
>
<div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faPlus} className="mr-3" />
Invite people
</div>
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<button
type="button"
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
className="mt-1.5 w-full"
>
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon icon={faInfinity} className="ml-0.5 mr-3 py-2 text-primary" />
Start Free Pro Trial
</div>
</button>
)}
</div>
);
};

View File

@@ -1 +0,0 @@
export { SidebarFooter } from "./SidebarFooter";

View File

@@ -1,169 +1,27 @@
import { faAngleDown, faArrowUpRightFromSquare, faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate } from "@tanstack/react-router";
import { useOrganization, useSubscription } from "@app/context";
import { SubscriptionPlan } from "@app/hooks/api/types";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { useOrganization, useUser } from "@app/context";
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
import { AuthMethod } from "@app/hooks/api/users/types";
type Prop = {
onChangeOrg: (orgId: string) => void;
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.dynamicSecret) return "Enterprise Plan";
if (subscription.pitRecovery) return "Pro Plan";
return "Free Plan";
};
export const SidebarHeader = ({ onChangeOrg }: Prop) => {
export const SidebarHeader = () => {
const { currentOrg } = useOrganization();
const { user } = useUser();
const navigate = useNavigate();
const { data: orgs } = useGetOrganizations();
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
const { subscription } = useSubscription();
return (
<div className="flex h-12 cursor-default items-center px-3 pt-6">
<DropdownMenu>
<DropdownMenuTrigger asChild className="max-w-[160px] data-[state=open]:bg-mineshaft-600">
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
<div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm">
{currentOrg?.name.charAt(0)}
</div>
<div
className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100"
style={{ maxWidth: "140px" }}
>
{currentOrg?.name}
</div>
<FontAwesomeIcon icon={faAngleDown} className="pl-1 pt-1 text-xs text-mineshaft-300" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
onChangeOrg(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
</button>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
>
<div
className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
style={{ fontSize: "11px", width: "26px", height: "26px" }}
>
{user?.firstName?.charAt(0)}
{user?.lastName && user?.lastName?.charAt(0)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
<Link to="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
{user?.superAdmin && (
<Link to="/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Server Admin Console
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
</button>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{currentOrg?.name}
</div>
<div className="text-xs text-mineshaft-400">{getPlan(subscription)}</div>
</div>
</div>
);
};

View File

@@ -14,7 +14,7 @@ import { envConfig } from "@app/config/env";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/SidebarFooter/SidebarFooter";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
export const PersonalSettingsLayout = () => {
const { t } = useTranslation();

View File

@@ -1,48 +1,30 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-router";
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Mfa } from "@app/components/auth/Mfa";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Menu, MenuItem } from "@app/components/v2";
import { useUser, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import {
useGetAccessRequestsCount,
useGetSecretApprovalRequestCount,
useSelectOrganization,
workspaceKeys
} from "@app/hooks/api";
import { authKeys } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
BreadcrumbContainer,
Menu,
MenuGroup,
MenuItem,
TBreadcrumbFormat
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { SidebarFooter } from "../OrganizationLayout/components/SidebarFooter";
import { ProjectSelect } from "./components/ProjectSelect";
import { SidebarHeader } from "./components/SidebarHeader";
// This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed.
export const ProjectLayout = () => {
const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { user } = useUser();
const { mutateAsync: selectOrganization } = useSelectOrganization();
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const { t } = useTranslation();
const router = useRouter();
const queryClient = useQueryClient();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
@@ -63,191 +45,168 @@ export const ProjectLayout = () => {
const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<SidebarHeader onChangeOrg={handleOrgChange} />
<ProjectSelect />
<div className="px-1">
<Menu>
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
{t("nav.menu.secrets")}
</MenuItem>
)}
</Link>
)}
{isCertManager && (
<Link
to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
<MenuGroup title="Main Menu">
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
{t("nav.menu.secrets")}
</MenuItem>
)}
</Link>
)}
</Link>
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
{t("nav.menu.integrations")}
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
{pendingRequestsCount}
</span>
)}
</MenuItem>
)}
</Link>
)}
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
{isCertManager && (
<Link
to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
</Link>
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
{t("nav.menu.integrations")}
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
{pendingRequestsCount}
</span>
)}
</MenuItem>
)}
</Link>
)}
</MenuGroup>
<MenuGroup title="Other">
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
</div>
<SidebarFooter />
</nav>
</aside>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet />
</main>
</div>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">

View File

@@ -167,7 +167,7 @@ export const ProjectSelect = () => {
}, [workspaces, projectFavorites, currentWorkspace]);
return (
<div className="mb-4 mt-5 w-full p-3">
<div className="mt-2 w-full p-3">
<p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400">
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"}
</p>

View File

@@ -80,7 +80,7 @@ export const LoginPage = () => {
/>
</div>
</Link>
<div className="pb-28">{renderView()}</div>;
<div className="pb-28">{renderView()}</div>
</div>
);
};

View File

@@ -59,7 +59,7 @@ export const LoginSsoPage = () => {
/>
</div>
</Link>
<div>{renderView()}</div>;
<div>{renderView()}</div>
</div>
);
};

View File

@@ -89,7 +89,7 @@ export const SignupSsoPage = () => {
alt="Infisical Logo"
/>
</div>
<div>{renderView()}</div>;
<div>{renderView()}</div>
</div>
);
};

View File

@@ -1,6 +1,4 @@
import { Helmet } from "react-helmet";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -13,6 +11,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
@@ -80,34 +79,17 @@ const Page = () => {
return (
<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 px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() =>
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
params: {
projectId
}
})
}
className="mb-4"
>
Certificate Authorities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.friendlyName}</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.friendlyName}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
@@ -133,7 +115,7 @@ const Page = () => {
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />

View File

@@ -1,9 +1,25 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
)({
component: CertAuthDetailsByIDPage
component: CertAuthDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Authorities",
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: {
projectId: params.projectId
}
})
}
]
};
}
});

View File

@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CaTab, CertificatesTab, PkiAlertsTab } from "./components";
@@ -19,11 +19,9 @@ export const CertificatesPage = () => {
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">Internal PKI</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Overview" />
<Tabs defaultValue={TabSections.Certificates}>
<TabList>
<Tab value={TabSections.Certificates}>Certificates</Tab>

View File

@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { CertificatesPage } from "./CertificatesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview"
)({
component: CertificatesPage
});

View File

@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
@@ -74,34 +73,17 @@ export const PkiCollectionPage = () => {
return (
<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 px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
params: {
projectId
}
});
}}
className="mb-4"
>
Certificate Collections
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PkiCollections}
@@ -147,7 +129,7 @@ export const PkiCollectionPage = () => {
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<PkiCollectionDetailsSection

View File

@@ -1,9 +1,25 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
)({
component: PkiCollectionDetailsByIDPage
component: PkiCollectionDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Collections",
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: {
projectId: params.projectId
}
})
}
]
};
}
});

View File

@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
@@ -14,12 +14,9 @@ export const SettingsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="w-full max-w-7xl px-6">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<div className="w-full max-w-7xl">
<PageHeader title={t("settings.project.title")} />
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (

View File

@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/settings"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings"
)({
component: SettingsPage
component: SettingsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Settings"
}
]
};
}
});

View File

@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
@@ -6,11 +6,11 @@ import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
@@ -21,5 +21,21 @@ export const Route = createFileRoute(
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
breadcrumbs: [
{
label: "Cert Managers",
link: linkOptions({ to: "/organization/cert-manager/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: { projectId: project.id }
})
}
]
};
}
});

View File

@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CmekTable } from "./components";
@@ -13,15 +14,13 @@ export const OverviewPage = () => {
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<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 px-6 py-6">
<p className="mr-4 text-3xl font-semibold text-white">Key Management System</p>
<p className="text-md mb-4 text-bunker-300">
Manage keys and perform cryptographic operations.
</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="OverviewPage"
description="Manage keys and perform cryptographic operations."
/>
<ProjectPermissionCan
passThrough={false}
renderGuardBanner

View File

@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/overview"
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview"
)({
component: OverviewPage
});

View File

@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
@@ -14,12 +14,9 @@ export const SettingsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="w-full max-w-7xl px-6">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<div className="w-full max-w-7xl">
<PageHeader title={t("settings.project.title")} />
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (

View File

@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/settings"
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings"
)({
component: SettingsPage
component: SettingsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Settings"
}
]
};
}
});

View File

@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
@@ -6,11 +6,11 @@ import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout"
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
@@ -21,5 +21,21 @@ export const Route = createFileRoute(
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
breadcrumbs: [
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/kms/$projectId/overview",
params: { projectId: project.id }
})
}
]
};
}
});

View File

@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgAccessControlTabSections } from "@app/types/org";
@@ -59,8 +59,11 @@ export const AccessManagementPage = () => {
<Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">Organization Access Control</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Organization Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
{tabSections

View File

@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -12,12 +14,24 @@ const AccessControlPageQuerySchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/access-management"
"/_authenticate/_inject-org-details/_org-layout/organization/access-management"
)({
component: AccessManagementPage,
validateSearch: zodValidator(AccessControlPageQuerySchema),
search: {
// strip default values
middlewares: [stripSearchParams({ action: "" })]
}
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "access control"
}
]
})
});

View File

@@ -2,7 +2,7 @@ import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgAdminProjects } from "./components/OrgAdminProjects";
@@ -17,13 +17,13 @@ export const AdminPage = () => {
<>
<Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
<div className="w-full max-w-6xl px-6">
<div className="mb-4">
<p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p>
</div>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Organization Admin Console"
description="View and manage resources across your organization."
/>
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
<TabList>
<Tab value={TabSections.Projects}>Projects</Tab>

View File

@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AdminPage } from "./AdminPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/admin"
"/_authenticate/_inject-org-details/_org-layout/organization/admin"
)({
component: AdminPage
component: AdminPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Admin Console"
}
]
})
});

View File

@@ -11,7 +11,7 @@ const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback"
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
)({
component: GitHubOAuthCallbackPage,
validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema),

View File

@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { LogsSection } from "./components";
export const AuditLogsPage = () => {
@@ -11,11 +13,11 @@ export const AuditLogsPage = () => {
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6">
<div className="bg-bunker-800 py-6">
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
<div />
</div>
<div className="w-full max-w-7xl">
<PageHeader
title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
</div>
</div>

View File

@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/audit-logs"
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs"
)({
component: AuditLogsPage
component: AuditLogsPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Audit Logs"
}
]
})
});

View File

@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { BillingPage } from "./BillingPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/billing"
"/_authenticate/_inject-org-details/_org-layout/organization/billing"
)({
component: BillingPage
});

View File

@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertManagerOverviewPage } from "./CertManagerOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/cert-manager/overview"
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview"
)({
component: CertManagerOverviewPage
component: CertManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Cert Management",
link: linkOptions({ to: "/organization/cert-manager/overview" })
}
]
})
});

View File

@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Spinner,
Tooltip
} from "@app/components/v2";
@@ -83,34 +82,17 @@ const Page = () => {
return (
<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 px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: TabSections.Groups
}
});
}}
className="mb-4"
>
Groups
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.group.name}</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.group.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="end" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<DropdownMenuItem
@@ -153,7 +135,7 @@ const Page = () => {
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />

View File

@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/groups/$groupId"
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
)({
component: GroupDetailsByIDPage
component: GroupDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "groups"
}
]
})
});

View File

@@ -1,8 +1,6 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -16,6 +14,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
@@ -165,30 +164,13 @@ const Page = () => {
return (
<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 px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management",
search: {
selectedTab: OrgAccessControlTabSections.Identities
}
});
}}
className="mb-4"
>
Identities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.identity.name}</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.identity.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
@@ -257,7 +239,7 @@ const Page = () => {
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />

View File

@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/identities/$identityId"
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId"
)({
component: IdentityDetailsByIDPage
component: IdentityDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "identities"
}
]
})
});

View File

@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { KmsOverviewPage } from "./KmsOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/kms/overview"
"/_authenticate/_inject-org-details/_org-layout/organization/kms/overview"
)({
component: KmsOverviewPage
component: KmsOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
}
]
})
});

View File

@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { NoOrgPage } from "./NoOrgPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/none"
"/_authenticate/_inject-org-details/_org-layout/organization/none"
)({
component: NoOrgPage
});

View File

@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
@@ -78,31 +77,14 @@ export const Page = () => {
return (
<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 px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Roles
}
});
}}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
{isCustomRole && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
@@ -144,7 +126,7 @@ export const Page = () => {
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />

View File

@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleByIDPage } from "./RoleByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId"
"/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
)({
component: RoleByIDPage
component: RoleByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "Roles"
}
]
})
});

View File

@@ -22,7 +22,15 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects";
import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2";
import {
Button,
IconButton,
Input,
PageHeader,
Pagination,
Skeleton,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@@ -50,13 +58,6 @@ enum ProjectOrderBy {
Name = "name"
}
const formatTitle = (type: ProjectType) => {
if (type === ProjectType.SecretManager) return "Secret Management";
if (type === ProjectType.CertificateManager) return "Cert Management";
if (type === ProjectType.KMS) return "Key Management";
return "SSH";
};
const formatDescription = (type: ProjectType) => {
if (type === ProjectType.SecretManager)
return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
@@ -365,13 +366,13 @@ export const ProductOverviewPage = ({ type }: Props) => {
}
return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
{!serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<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">
<FontAwesomeIcon
@@ -393,14 +394,9 @@ export const ProductOverviewPage = ({ type }: Props) => {
</div>
</div>
)}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0">
<div className="flex w-full justify-between">
<p className="mr-4 text-3xl font-semibold text-white">{formatTitle(type)}</p>
</div>
<div>
<p className="mr-4 mt-2 text-gray-400">{formatDescription(type)}</p>
</div>
<div className="mt-6 flex w-full flex-row">
<div className="mb-4 flex flex-col items-start justify-start">
<PageHeader title="Projects" description={formatDescription(type)} />
<div className="flex w-full flex-row">
<Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by project name..."

View File

@@ -1,9 +1,22 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute } from "@tanstack/react-router";
import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview"
"/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview"
)({
component: SecretManagerOverviewPage
component: SecretManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Management"
}
]
})
});

View File

@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner, Pagination } from "@app/components/v2";
import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
OrgPermissionActions,
@@ -111,11 +111,11 @@ export const SecretScanningPage = withPermission(
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
<div className="mb-6 text-lg text-mineshaft-300">
Automatically monitor your GitHub activity and prevent secret leaks
</div>
<div className="w-full max-w-7xl">
<PageHeader
title="Secret Scanning"
description="Automatically monitor your GitHub activity and prevent secret leaks"
/>
{config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased

View File

@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -10,10 +12,10 @@ const SecretScanningQueryParams = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning"
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
)({
component: SecretScanningPage,
validateSearch: zodValidator(SecretScanningQueryParams),
component: SecretScanningPage,
search: {
middlewares: [
stripSearchParams({
@@ -21,5 +23,17 @@ export const Route = createFileRoute(
state: ""
})
]
}
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Secret Scanning"
}
]
})
});

View File

@@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2";
import { ShareSecretSection } from "./components";
export const SecretSharingPage = () => {
@@ -18,28 +20,25 @@ export const SecretSharingPage = () => {
<meta name="og:description" content={String(t("approval.og-description"))} />
</Helmet>
<div className="h-full">
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white">
<div className="flex items-center justify-between py-6">
<div className="flex w-full flex-col">
<h2 className="text-3xl font-semibold text-gray-200">Secret Sharing</h2>
<p className="text-bunker-300">Share secrets securely using a shareable link</p>
</div>
<div className="flex w-max justify-center">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/secret-sharing"
>
<div className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</div>
</a>
</div>
</div>
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<PageHeader
title="Secret Sharing"
description="Share secrets securely using a shareable link"
>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/secret-sharing"
>
<div className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</div>
</a>
</PageHeader>
<ShareSecretSection />
</div>
</div>

View File

@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretSharingPage } from "./SecretSharingPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-sharing"
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
)({
component: SecretSharingPage
component: SecretSharingPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "secret sharing"
}
]
})
});

View File

@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { OrgTabGroup } from "./components";
export const SettingsPage = () => {
@@ -11,11 +13,9 @@ export const SettingsPage = () => {
<Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
<div className="w-full max-w-7xl px-6">
<div className="mb-4">
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
</div>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title={t("settings.org.title")} />
<OrgTabGroup />
</div>
</div>

View File

@@ -103,7 +103,7 @@ export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
console.log(err);
createNotification({
type: "error",
text: "Failed to create stream"
text: (err as Error)?.message ?? "Failed to create stream"
});
}
};

View File

@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -9,11 +11,23 @@ const SettingsPageQueryParams = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/settings"
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
)({
component: SettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams),
search: {
middlewares: [stripSearchParams({ selectedTab: "" })]
}
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Settings"
}
]
})
});

View File

@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshOverviewPage } from "./SshOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/ssh/overview"
"/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview"
)({
component: SshOverviewPage
component: SshOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "SSH",
link: linkOptions({ to: "/organization/ssh/overview" })
}
]
})
});

View File

@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
@@ -118,143 +117,132 @@ const Page = withPermission(
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{membership && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Member
}
});
}}
className="mb-4"
>
Users
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{membership.user.firstName || membership.user.lastName
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title={
membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-"}
</p>
{userId !== membership.user.id && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("orgMembership", {
membershipId: membership.id,
role: membership.role,
roleId: membership.roleId
})
}
disabled={!isAllowed}
>
Edit User
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
: "-"
}
>
<div>
{userId !== membership.user.id && (
<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" size="sm">
More
</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("orgMembership", {
membershipId: membership.id,
role: membership.role,
roleId: membership.roleId
})
}
if (!membership.isActive) {
// activate user
await updateOrgMembership({
organizationId: orgId,
membershipId,
isActive: true
});
return;
disabled={!isAllowed}
>
Edit User
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
// deactivate user
handlePopUpOpen("deactivateMember", {
orgMembershipId: membershipId,
username: membership.user.username
});
}}
disabled={!isAllowed}
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
if (!membership.isActive) {
// activate user
await updateOrgMembership({
organizationId: orgId,
membershipId,
isActive: true
});
return;
}
// deactivate user
handlePopUpOpen("deactivateMember", {
orgMembershipId: membershipId,
username: membership.user.username
});
return;
}
}}
disabled={!isAllowed}
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", {
orgMembershipId: membershipId,
username: membership.user.username
});
}}
disabled={!isAllowed}
>
Remove User
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
handlePopUpOpen("removeMember", {
orgMembershipId: membershipId,
username: membership.user.username
});
}}
disabled={!isAllowed}
>
Remove User
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />

View File

@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { UserDetailsByIDPage } from "./UserDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId"
"/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId"
)({
component: UserDetailsByIDPage
component: UserDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "Users"
}
]
})
});

View File

@@ -2,6 +2,6 @@ import { createFileRoute } from "@tanstack/react-router";
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
export const Route = createFileRoute("/_authenticate/_inject-org-details/organization/_layout")({
export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({
component: OrganizationLayout
});

View File

@@ -2,9 +2,8 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { withProjectPermission } from "@app/hoc";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectAccessControlTabs } from "@app/types/project";
@@ -38,11 +37,11 @@ const Page = withProjectPermission(
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 px-6 py-6">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} Access
Control
</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={ProjectAccessControlTabs.Member}>Users</Tab>

View File

@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema)
validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
});

View File

@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/access-management"
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema)
validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
});

View File

@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/access-management"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema)
validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
});

View File

@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
@@ -11,8 +11,24 @@ const AccessControlPageQuerySchema = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management"
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema)
validateSearch: zodValidator(AccessControlPageQuerySchema),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
}
]
};
}
});

View File

@@ -1,16 +1,13 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { format } from "date-fns";
import { formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, EmptyState, Spinner } from "@app/components/v2";
import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import {
useDeleteIdentityFromWorkspace,
@@ -82,78 +79,37 @@ const Page = () => {
}
return (
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white">
<div className="mb-4">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: `/${currentWorkspace.type}/$projectId/access-management` as const,
params: {
projectId: workspaceId
},
search: {
selectedTab: "identities"
}
});
}}
className="mb-4"
>
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} Access
Control
</Button>
</div>
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
{identityMembershipDetails ? (
<>
<div className="mb-4">
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-xl font-semibold text-mineshaft-100">
Project Identity Access
</h3>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Identity, {
identityId: identityMembershipDetails?.identity?.id
})}
renderTooltip
allowedLabel="Remove from project"
<PageHeader
title={identityMembershipDetails?.identity?.name}
description={`Identity joined on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Identity, {
identityId: identityMembershipDetails?.identity?.id
})}
renderTooltip
allowedLabel="Remove from project"
>
{(isAllowed) => (
<Button
colorSchema="danger"
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
isLoading={isDeletingIdentity}
onClick={() => handlePopUpOpen("deleteIdentity")}
>
{(isAllowed) => (
<Button
colorSchema="danger"
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
isLoading={isDeletingIdentity}
onClick={() => handlePopUpOpen("deleteIdentity")}
>
Remove Identity
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<div className="flex gap-12">
<div>
<span className="text-xs font-semibold text-gray-400">Name</span>
{identityMembershipDetails && (
<p className="text-lg capitalize">
{identityMembershipDetails?.identity?.name}
</p>
)}
</div>
</div>
<div className="mt-4 text-sm text-gray-400">
Joined on{" "}
{identityMembershipDetails?.createdAt &&
format(new Date(identityMembershipDetails?.createdAt || ""), "yyyy-MM-dd")}
</div>
Remove Identity
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
</PageHeader>
<IdentityRoleDetailsSection
identityMembershipDetails={identityMembershipDetails}
isMembershipDetailsLoading={isMembershipDetailsLoading}

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
});

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/identities/$identityId"
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
});

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
});

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId"
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Identities"
}
]
};
}
});

View File

@@ -1,21 +1,18 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { format } from "date-fns";
import { formatRelative } from "date-fns";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, EmptyState, Spinner } from "@app/components/v2";
import { Button, DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { useDeleteUserFromWorkspace, useGetWorkspaceUserDetails } from "@app/hooks/api";
@@ -82,77 +79,37 @@ export const Page = () => {
}
return (
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white">
<div className="mb-4">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: `/${currentWorkspace.type}/$projectId/access-management` as const,
params: {
projectId: currentWorkspace.id
}
});
}}
className="mb-4"
>
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} Access
Control
</Button>
</div>
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 text-white">
{membershipDetails ? (
<>
<div className="mb-4">
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-xl font-semibold text-mineshaft-100">Project User Access</h3>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
renderTooltip
allowedLabel="Remove from project"
>
{(isAllowed) => (
<Button
colorSchema="danger"
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
isLoading={isRemovingUserFromWorkspace}
onClick={() => handlePopUpOpen("removeMember")}
>
Remove User
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<div className="flex gap-12">
<div>
<span className="text-xs font-semibold text-gray-400">Name</span>
{membershipDetails && (
<p className="text-lg capitalize">
{membershipDetails.user.firstName || membershipDetails.user.lastName
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
: "-"}
</p>
)}
</div>
<div>
<span className="text-xs font-semibold text-gray-400">Email</span>
{membershipDetails && <p className="text-lg">{membershipDetails?.user?.email}</p>}
</div>
</div>
<div className="mt-4 text-sm text-gray-400">
Joined on{" "}
{membershipDetails?.createdAt &&
format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")}
</div>
</div>
</div>
<PageHeader
title={
membershipDetails.user.firstName || membershipDetails.user.lastName
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
: "-"
}
description={`User joined on ${membershipDetails?.createdAt && formatRelative(new Date(membershipDetails?.createdAt || ""), new Date())}`}
>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
renderTooltip
allowedLabel="Remove from project"
>
{(isAllowed) => (
<Button
colorSchema="danger"
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
isLoading={isRemovingUserFromWorkspace}
onClick={() => handlePopUpOpen("removeMember")}
>
Remove User
</Button>
)}
</ProjectPermissionCan>
</PageHeader>
<MemberRoleDetailsSection
membershipDetails={membershipDetails}
isMembershipDetailsLoading={isMembershipDetailsLoading}

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/members/$membershipId"
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
});

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/members/$membershipId"
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
});

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
});

View File

@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId"
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Users"
}
]
};
}
});

View File

@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
@@ -83,38 +82,18 @@ const Page = () => {
return (
<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 px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() =>
navigate({
to: `/${currentWorkspace?.type}/$projectId/access-management` as const,
params: {
projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Roles
}
})
}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
{isCustomRole && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
@@ -156,7 +135,7 @@ const Page = () => {
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />

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