mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-24 00:15:26 +00:00
Compare commits
47 Commits
infisical-
...
vercel-pro
Author | SHA1 | Date | |
---|---|---|---|
fa82d4953e | |||
12d9fe9ffd | |||
86acf88a13 | |||
63c7c39e21 | |||
151edc7efa | |||
5fa7f56285 | |||
810b27d121 | |||
51fe7450ae | |||
938c06a2ed | |||
38d431ec77 | |||
d202fdf5c8 | |||
f1b2028542 | |||
5c9b46dfba | |||
a516e50984 | |||
569439f208 | |||
9afc282679 | |||
8db85cfb84 | |||
664b2f0089 | |||
5e9bd3a7c6 | |||
2c13af6db3 | |||
ec9171d0bc | |||
81362bec8f | |||
5a4d7541a2 | |||
3c97c45455 | |||
4f015d77fb | |||
78e894c2bb | |||
23513158ed | |||
934ef8ab27 | |||
23e9c52f67 | |||
e276752e7c | |||
01ae19fa2b | |||
9df8cf60ef | |||
1b1fe2a700 | |||
338961480c | |||
03debcab5a | |||
4a6f759900 | |||
b9d06ff686 | |||
5cc5a4f03d | |||
5ef2be1a9c | |||
8de9ddfb8b | |||
5b40de16cf | |||
11aac3f5dc | |||
9823c7d1aa | |||
3ba396f7fa | |||
9c561266ed | |||
bd459d994c | |||
440f93f392 |
.github/workflows
backend/src
db
ee/services
audit-log-stream
ldap-config
saml-config
scim
secret-approval-request
server/routes
services
docs/integrations/platforms/kubernetes
frontend
public
locales
lotties
src
components
const
hooks
layouts
AdminLayout
OrganizationLayout
PersonalSettingsLayout
ProjectLayout
pages
auth
LoginPage
LoginSsoPage
SelectOrgPage
SignUpSsoPage
cert-manager
kms
organization
AccessManagementPage
AdminPage
AppConnections/GithubOauthCallbackPage
AuditLogsPage
BillingPage
CertManagerOverviewPage
GroupDetailsByIDPage
IdentityDetailsByIDPage
KmsOverviewPage
NoOrgPage
RoleByIDPage
SecretManagerOverviewPage
SecretScanningPage
SecretSharingPage
SettingsPage
SshOverviewPage
UserDetailsByIDPage
layout.tsxproject
AccessControlPage
IdentityDetailsByIDPage
MemberDetailsByIDPage
RoleDetailsBySlugPage
secret-manager
IPAllowlistPage
IntegrationsDetailsByIDPage
IntegrationsListPage
IntegrationsListPage.tsxIntegrationsListPage.utils.tsxroute.tsx
components
CloudIntegrationSection
FrameworkIntegrationSection
InfrastructureIntegrationSection
IntegrationsSection
OverviewPage
SecretApprovalsPage
SecretDashboardPage
SecretRotationPage
SettingsPage
integrations
AwsParameterStoreAuthorizePage
AwsParameterStoreConfigurePage
AwsSecretManagerAuthorizePage
AwsSecretManagerConfigurePage
AzureAppConfigurationConfigurePage
AzureAppConfigurationOauthCallbackPage
AzureDevopsAuthorizePage
AzureDevopsConfigurePage
AzureKeyVaultAuthorizePage
AzureKeyVaultConfigurePage
AzureKeyVaultOauthCallbackPage
BitbucketConfigurePage
BitbucketOauthCallbackPage
ChecklyAuthorizePage
ChecklyConfigurePage
CircleCIAuthorizePage
CircleCIConfigurePage
Cloud66AuthorizePage
Cloud66ConfigurePage
CloudflarePagesAuthorizePage
CloudflarePagesConfigurePage
CloudflareWorkersAuthorizePage
CloudflareWorkersConfigurePage
CodefreshAuthorizePage
CodefreshConfigurePage
DatabricksAuthorizePage
DatabricksConfigurePage
DigitalOceanAppPlatformAuthorizePage
DigitalOceanAppPlatformConfigurePage
FlyioAuthorizePage
FlyioConfigurePage
GcpSecretManagerAuthorizePage
GcpSecretManagerConfigurePage
GcpSecretManagerOauthCallbackPage
GithubAuthorizePage
GithubConfigurePage
GithubOauthCallbackPage
GitlabAuthorizePage
GitlabConfigurePage
GitlabOauthCallbackPage
HashicorpVaultAuthorizePage
HashicorpVaultConfigurePage
HasuraCloudAuthorizePage
HasuraCloudConfigurePage
HerokuConfigurePage
HerokuOauthCallbackPage
LaravelForgeAuthorizePage
LaravelForgeConfigurePage
NetlifyConfigurePage
NetlifyOauthCallbackPage
NorthflankAuthorizePage
NorthflankConfigurePage
OctopusDeployAuthorizePage
OctopusDeployConfigurePage
QoveryAuthorizePage
QoveryConfigurePage
RailwayAuthorizePage
RailwayConfigurePage
RenderAuthorizePage
RenderConfigurePage
RundeckAuthorizePage
RundeckConfigurePage
SelectIntegrationAuthPage
SupabaseAuthorizePage
SupabaseConfigurePage
TeamcityAuthorizePage
TeamcityConfigurePage
TerraformCloudAuthorizePage
TerraformCloudConfigurePage
TravisCIAuthorizePage
TravisCIConfigurePage
VercelConfigurePage
VercelOauthCallbackPage
WindmillAuthorizePage
WindmillConfigurePage
route-azure-app-configurations-oauth-redirect.tsxroute-azure-key-vault-oauth-redirect.tsxroute-bitbucket-oauth-redirect.tsxroute-gcp-oauth-redirect.tsxroute-github-oauth-redirect.tsxroute-gitlab-oauth-redirect.tsxroute-heroku-oauth-redirect.tsxroute-netlify-oauth-redirect.tsxroute-vercel-oauth-redirect.tsxssh
helm-charts/secrets-operator
k8-operator
config/samples/crd/infisicalsecret
controllers/infisicalsecret
58
.github/workflows/deployment-pipeline.yml
vendored
58
.github/workflows/deployment-pipeline.yml
vendored
@ -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 }}>"
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
@ -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>;
|
||||
|
@ -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({
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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(",")}`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -895,7 +895,8 @@ export const registerRoutes = async (
|
||||
certificateTemplateDAL,
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL,
|
||||
projectTemplateService
|
||||
projectTemplateService,
|
||||
groupProjectDAL
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
|
@ -932,15 +932,22 @@ const syncSecretsAWSParameterStore = async ({
|
||||
logger.info(
|
||||
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]`
|
||||
);
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
|
||||
Overwrite: true
|
||||
})
|
||||
.promise();
|
||||
|
||||
try {
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
|
||||
Overwrite: true
|
||||
})
|
||||
.promise();
|
||||
} catch (error) {
|
||||
(error as { secretKey: string }).secretKey = key;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (metadata.secretAWSTag?.length) {
|
||||
try {
|
||||
await ssm
|
||||
@ -987,15 +994,20 @@ const syncSecretsAWSParameterStore = async ({
|
||||
|
||||
// we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS
|
||||
if (secrets[key].value && (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value)) {
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
Overwrite: true,
|
||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId })
|
||||
})
|
||||
.promise();
|
||||
try {
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
Overwrite: true,
|
||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId })
|
||||
})
|
||||
.promise();
|
||||
} catch (error) {
|
||||
(error as { secretKey: string }).secretKey = key;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (awsParameterStoreSecretsObj[key].Name) {
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -5,6 +5,7 @@ description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Inf
|
||||
---
|
||||
|
||||
Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD).
|
||||
In this CRD, you'll define the authentication method to use, the secrets to fetch, and the target location to store the secrets within your cluster.
|
||||
|
||||
```yaml example-infisical-secret-crd.yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
@ -19,101 +20,28 @@ spec:
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval: 10
|
||||
authentication:
|
||||
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||
# If you have multiple authentication methods defined, it may cause issues.
|
||||
|
||||
# (Deprecated) Service Token Auth
|
||||
serviceToken:
|
||||
serviceTokenSecretReference:
|
||||
secretName: service-token
|
||||
secretNamespace: default
|
||||
secretsScope:
|
||||
envSlug: <env-slug>
|
||||
secretsPath: <secrets-path>
|
||||
recursive: true
|
||||
|
||||
# Universal Auth
|
||||
universalAuth:
|
||||
secretsScope:
|
||||
projectSlug: new-ob-em
|
||||
envSlug: dev # "dev", "staging", "prod", etc..
|
||||
secretsPath: "/" # Root is "/"
|
||||
recursive: true # Whether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
|
||||
credentialsRef:
|
||||
secretName: universal-auth-credentials
|
||||
secretNamespace: default
|
||||
|
||||
# Native Kubernetes Auth
|
||||
kubernetesAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountRef:
|
||||
name: <service-account-name>
|
||||
namespace: <service-account-namespace>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# AWS IAM Auth
|
||||
awsIamAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# Azure Auth
|
||||
azureAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/"
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# GCP ID Token Auth
|
||||
gcpIdTokenAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# GCP IAM Auth
|
||||
gcpIamAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
# template:
|
||||
# includeAllSecrets: true
|
||||
# data:
|
||||
# CUSTOM_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
creationPolicy: "Orphan"
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
NEW_KEY_NAME: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
|
||||
KEY_WITH_BINARY_VALUE: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
|
||||
```
|
||||
|
||||
### InfisicalSecret CRD properties
|
||||
## CRD properties
|
||||
|
||||
### Generic
|
||||
|
||||
The following properties help define what instance of Infisical the operator will interact with, the interval it will sync secrets and any CA certificates that may be required to connect.
|
||||
|
||||
<Accordion title="hostAPI">
|
||||
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
|
||||
@ -165,10 +93,12 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
CA certificate to use for connecting to the Infisical instance with SSL/TLS.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="authentication">
|
||||
This block defines the method that will be used to authenticate with Infisical
|
||||
so that secrets can be fetched
|
||||
</Accordion>
|
||||
### Authentication methods
|
||||
|
||||
To retrieve the requested secrets, the operator must first authenticate with Infisical.
|
||||
The list of available authentication methods are shown below.
|
||||
|
||||
<Accordion title="authentication"></Accordion>
|
||||
|
||||
<Accordion title="authentication.universalAuth">
|
||||
The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials.
|
||||
@ -605,13 +535,13 @@ spec:
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Operator managed secrets
|
||||
|
||||
The managed secret properties specify where to store the secrets retrieved from your Infisical project.
|
||||
This includes defining the name and namespace of the Kubernetes secret that will hold these secrets.
|
||||
The Infisical operator will automatically create the Kubernetes secret in the specified name/namespace and ensure it stays up-to-date.
|
||||
|
||||
<Accordion title="managedSecretReference">
|
||||
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
|
||||
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
|
||||
The Infisical operator will automatically create the Kubernetes secret with the specified name/namespace and keep it continuously updated.
|
||||
|
||||
Note: The managed secret be should be created in the same namespace as the deployment that will use it.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.secretName">
|
||||
The name of the managed Kubernetes secret to be created
|
||||
@ -621,57 +551,6 @@ The namespace of the managed Kubernetes secret to be created.
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.secretType">
|
||||
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.template">
|
||||
Templates enable you to transform data from Infisical before storing it as a Kubernetes Secret.
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.template.includeAllSecrets">
|
||||
When set to true, this option injects all secrets retrieved from Infisical into your configuration.
|
||||
Secrets defined in the template will override the automatically injected secrets.
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.template.data">
|
||||
Define secret keys and their corresponding templates.
|
||||
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
|
||||
|
||||
Secrets are structured as follows:
|
||||
|
||||
```golang
|
||||
type TemplateSecret struct {
|
||||
Value string `json:"value"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Example template configuration:
|
||||
|
||||
```golang
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
NEW_KEY: "{{ .KEY1.SecretPath }} {{ .KEY1.Value }}"
|
||||
```
|
||||
|
||||
When you run the following command:
|
||||
|
||||
```bash
|
||||
kubectl get secret managed-secret -o jsonpath='{.data}'
|
||||
```
|
||||
|
||||
You'll receive Kubernetes secrets output that includes the NEW_KEY:
|
||||
|
||||
```bash
|
||||
{... "KEY":"d29ybGQ=","NEW_KEY":"LyBoZWxsbw=="}
|
||||
```
|
||||
|
||||
When you set `includeAllSecrets` as `false` the Kubernetes secrets outputs will be:
|
||||
|
||||
```bash
|
||||
{"NEW_KEY":"LyBoZWxsbw=="}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.creationPolicy">
|
||||
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
|
||||
@ -689,7 +568,104 @@ This is useful for tools such as ArgoCD, where every resource requires an owner
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Apply the InfisicalSecret CRD to your cluster
|
||||
### Manged secret templating
|
||||
|
||||
Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful.
|
||||
Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Secrets.
|
||||
|
||||
<Accordion title="managedSecretReference.template">
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.template.includeAllSecrets">
|
||||
This property controls what secrets are included in your managed secret when using templates.
|
||||
When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes secret resource.
|
||||
**Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.**
|
||||
|
||||
When set to `false`, only secrets defined in the `managedSecretReference.template.data` field of the template will be included in the managed secret.
|
||||
Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.template.data">
|
||||
Define secret keys and their corresponding templates.
|
||||
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
|
||||
|
||||
Secrets are structured as follows:
|
||||
|
||||
```golang
|
||||
type TemplateSecret struct {
|
||||
Value string `json:"value"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Example template configuration:
|
||||
|
||||
```yaml
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
# Create new secret key that doesn't exist in your Infisical project using values of other secrets
|
||||
NEW_KEY: "{{ .DB_PASSWORD.Value }}"
|
||||
# Override an existing secret key in Infisical project with a new value using values of other secrets
|
||||
API_URL: "https://api.{{.COMPANY_NAME.Value}}.{{.REGION.Value}}.com"
|
||||
```
|
||||
|
||||
For this example, let's assume the following secrets exist in your Infisical project:
|
||||
|
||||
```
|
||||
DB_PASSWORD = "secret123"
|
||||
COMPANY_NAME = "acme"
|
||||
REGION = "us-east-1"
|
||||
API_URL = "old-url" # This will be overridden
|
||||
```
|
||||
|
||||
The resulting managed Kubernetes secret will then contain:
|
||||
|
||||
```
|
||||
# Original secrets (from includeAllSecrets: true)
|
||||
DB_PASSWORD = "secret123"
|
||||
COMPANY_NAME = "acme"
|
||||
REGION = "us-east-1"
|
||||
|
||||
# New and overridden templated secrets
|
||||
NEW_KEY = "secret123" # New secret created from template
|
||||
API_URL = "https://api.acme.us-east-1.com" # Existing secret overridden by template
|
||||
```
|
||||
|
||||
To help transform your secrets further, the operator provides a set of built-in functions that you can use in your templates.
|
||||
|
||||
### Available templating functions
|
||||
|
||||
<Accordion title="decodeBase64ToBytes">
|
||||
**Function name**: decodeBase64ToBytes
|
||||
|
||||
**Description**:
|
||||
Given a base64 encoded string, this function will decodes the base64-encoded string.
|
||||
This function is useful when your secrets are already stored as base64 encoded value in Infisical.
|
||||
|
||||
**Returns**: The decoded base64 string as bytes.
|
||||
|
||||
**Example**:
|
||||
The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical.
|
||||
The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`.
|
||||
|
||||
```yaml
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Applying CRD
|
||||
|
||||
Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster.
|
||||
After applying, you should notice that the managed secret has been created in the desired namespace your specified.
|
||||
@ -698,8 +674,6 @@ After applying, you should notice that the managed secret has been created in th
|
||||
kubectl apply -f example-infisical-secret-crd.yaml
|
||||
```
|
||||
|
||||
### Verify managed secret creation
|
||||
|
||||
To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified.
|
||||
|
||||
```bash
|
||||
@ -714,7 +688,7 @@ kubectl get secrets -n <namespace of managed secret>
|
||||
|
||||
## Using managed secret in your deployment
|
||||
|
||||
Incorporating the managed secret created by the operator into your deployment can be achieved through several methods.
|
||||
To make use of the managed secret created by the operator into your deployment can be achieved through several methods.
|
||||
Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/)
|
||||
|
||||
<Accordion title="envFrom">
|
||||
@ -960,4 +934,4 @@ metadata:
|
||||
type: Opaque
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
1
frontend/public/lotties/three-ellipsis.json
Normal file
1
frontend/public/lotties/three-ellipsis.json
Normal file
File diff suppressed because one or more lines are too long
1
frontend/public/lotties/user.json
Normal file
1
frontend/public/lotties/user.json
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
);
|
||||
};
|
217
frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx
Normal file
217
frontend/src/components/v2/Breadcrumb/Breadcrumb.tsx
Normal 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
|
||||
};
|
1
frontend/src/components/v2/Breadcrumb/index.tsx
Normal file
1
frontend/src/components/v2/Breadcrumb/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Breadcrumb";
|
@ -58,7 +58,7 @@ export type FormHelperTextProps = {
|
||||
export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
"mt-2 flex items-center font-inter text-xs text-mineshaft-300 opacity-90",
|
||||
"mt-2 flex items-center font-inter text-xs text-mineshaft-300 text-opacity-90",
|
||||
isError && "text-red-600"
|
||||
)}
|
||||
>
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
|
19
frontend/src/components/v2/PageHeader/PageHeader.tsx
Normal file
19
frontend/src/components/v2/PageHeader/PageHeader.tsx
Normal 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>
|
||||
);
|
1
frontend/src/components/v2/PageHeader/index.tsx
Normal file
1
frontend/src/components/v2/PageHeader/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { PageHeader } from "./PageHeader";
|
@ -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";
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
}, []);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -1,174 +1,127 @@
|
||||
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 = !(
|
||||
[
|
||||
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">
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { MenuIconButton } from "./MenuIconButton";
|
488
frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
Normal file
488
frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
Normal 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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
|
@ -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>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { SidebarFooter } from "./SidebarFooter";
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -80,7 +80,7 @@ export const LoginPage = () => {
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="pb-28">{renderView()}</div>;
|
||||
<div className="pb-28">{renderView()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -59,7 +59,7 @@ export const LoginSsoPage = () => {
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div>{renderView()}</div>;
|
||||
<div>{renderView()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -85,8 +85,7 @@ export const SelectOrganizationPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
window.open(url);
|
||||
window.close();
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ export const SignupSsoPage = () => {
|
||||
alt="Infisical Logo"
|
||||
/>
|
||||
</div>
|
||||
<div>{renderView()}</div>;
|
||||
<div>{renderView()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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} />
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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) => (
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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 }
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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) => (
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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 }
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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),
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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" })
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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} />
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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} />
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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" })
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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} />
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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..."
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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" })
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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} />
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user