mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 15:29:46 +00:00
Merge origin/main to feat/pki-sync-v2
This commit is contained in:
@@ -2,7 +2,7 @@ import { Knex } from "knex";
|
||||
|
||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||
|
||||
import { AccessScope, TableName } from "../schemas";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
||||
@@ -18,8 +18,6 @@ export async function up(knex: Knex): Promise<void> {
|
||||
await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex);
|
||||
t.unique(["rootOrgId", "parentOrgId", "slug"]);
|
||||
});
|
||||
|
||||
// had to switch to raw for null not distinct
|
||||
}
|
||||
|
||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
||||
@@ -28,24 +26,6 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.uuid("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(
|
||||
`
|
||||
UPDATE ?? AS identity
|
||||
SET "orgId" = membership."scopeOrgId"
|
||||
FROM ?? AS membership
|
||||
WHERE
|
||||
membership."actorIdentityId" = identity."id"
|
||||
AND membership."scope" = ?
|
||||
`,
|
||||
[TableName.Identity, TableName.Membership, AccessScope.Organization]
|
||||
);
|
||||
|
||||
await knex.raw(`DELETE FROM ?? WHERE "orgId" IS NULL`, [TableName.Identity]);
|
||||
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.uuid("orgId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { chunkArray } from "@app/lib/fn";
|
||||
|
||||
import { AccessScope, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async (tx) => {
|
||||
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (hasIdentityOrgCol) {
|
||||
const identityMemberships = await tx(TableName.Membership)
|
||||
.where({
|
||||
scope: AccessScope.Organization
|
||||
})
|
||||
.whereNotNull("actorIdentityId")
|
||||
.select("actorIdentityId", "scopeOrgId");
|
||||
|
||||
const identityToOrgMapping: Record<string, string> = {};
|
||||
identityMemberships.forEach((el) => {
|
||||
if (el.actorIdentityId) {
|
||||
identityToOrgMapping[el.actorIdentityId] = el.scopeOrgId;
|
||||
}
|
||||
});
|
||||
|
||||
const batchMemberships = chunkArray(identityMemberships, 500);
|
||||
for await (const membership of batchMemberships) {
|
||||
const identityIds = membership.map((el) => el.actorIdentityId).filter(Boolean) as string[];
|
||||
if (identityIds.length) {
|
||||
const identities = await tx(TableName.Identity).whereIn("id", identityIds).select("*");
|
||||
await tx(TableName.Identity)
|
||||
.insert(
|
||||
identities.map((el) => ({
|
||||
...el,
|
||||
orgId: identityToOrgMapping[el.id]
|
||||
}))
|
||||
)
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
||||
|
||||
const config = { transaction: false };
|
||||
export { config };
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"blockDuplicateSecretSyncDestinations"
|
||||
);
|
||||
if (!hasOrgBlockDuplicateColumn) {
|
||||
await knex.schema.table(TableName.Organization, (table) => {
|
||||
table.boolean("blockDuplicateSecretSyncDestinations").notNullable().defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"blockDuplicateSecretSyncDestinations"
|
||||
);
|
||||
if (hasOrgBlockDuplicateColumn) {
|
||||
await knex.schema.table(TableName.Organization, (table) => {
|
||||
table.dropColumn("blockDuplicateSecretSyncDestinations");
|
||||
});
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async (tx) => {
|
||||
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
|
||||
table.dropForeign("identityId");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async (tx) => {
|
||||
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
|
||||
table.foreign("identityId").references("id").inTable(TableName.Identity);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const config = { transaction: false };
|
||||
export { config };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const MIGRATION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const result = await knex.raw("SHOW statement_timeout");
|
||||
const originalTimeout = result.rows[0].statement_timeout;
|
||||
|
||||
await knex.transaction(async (tx) => {
|
||||
try {
|
||||
await tx.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`);
|
||||
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (hasIdentityOrgCol) {
|
||||
await tx(TableName.Identity).whereNull("orgId").delete();
|
||||
await tx.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.uuid("orgId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await tx.raw(`SET statement_timeout = '${originalTimeout}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
||||
|
||||
const config = { transaction: false };
|
||||
export { config };
|
||||
@@ -40,7 +40,8 @@ export const OrganizationsSchema = z.object({
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
||||
parentOrgId: z.string().uuid().nullable().optional(),
|
||||
rootOrgId: z.string().uuid().nullable().optional()
|
||||
rootOrgId: z.string().uuid().nullable().optional(),
|
||||
blockDuplicateSecretSyncDestinations: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -32,8 +31,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||
@@ -127,8 +126,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
|
||||
path: z
|
||||
|
||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -32,8 +31,8 @@ export const registerKubernetesDynamicSecretLeaseRouter = async (server: Fastify
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { z } from "zod";
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
|
||||
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
@@ -60,8 +59,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
@@ -72,8 +71,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
})
|
||||
.nullable(),
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
@@ -130,8 +129,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
@@ -142,8 +141,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||
|
||||
@@ -182,7 +182,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
algorithm: z.string(),
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
kmipMetadata: z.record(z.any()).nullish()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -384,7 +385,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
isActive: z.boolean(),
|
||||
algorithm: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
kmipMetadata: z.record(z.any()).nullish()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
|
||||
@@ -112,7 +112,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
|
||||
@@ -265,7 +265,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (newName) {
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
|
||||
}
|
||||
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
|
||||
@@ -341,7 +341,8 @@ export const kmipOperationServiceFactory = ({
|
||||
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||
isActive: !key.isDisabled,
|
||||
createdAt: key.createdAt,
|
||||
updatedAt: key.updatedAt
|
||||
updatedAt: key.updatedAt,
|
||||
kmipMetadata: key.kmipMetadata as Record<string, unknown>
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1517,7 +1517,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
if (secrets.length)
|
||||
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
|
||||
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
|
||||
|
||||
commits.push(
|
||||
...createdSecrets.map((createdSecret) => ({
|
||||
|
||||
@@ -1960,6 +1960,8 @@ export const registerRoutes = async (
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectDAL,
|
||||
orgDAL,
|
||||
folderDAL,
|
||||
secretSyncQueue,
|
||||
projectBotService,
|
||||
|
||||
@@ -323,7 +323,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
||||
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
||||
.nullable()
|
||||
.optional(),
|
||||
blockDuplicateSecretSyncDestinations: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Block duplicate secret sync destinations across the organization")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -393,6 +393,56 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/vault/kubernetes-roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
namespace: z.string(),
|
||||
mountPath: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
roles: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
mountPath: z.string(),
|
||||
allowed_kubernetes_namespaces: z.array(z.string()).nullish(),
|
||||
allowed_kubernetes_namespace_selector: z.string().nullish(),
|
||||
token_max_ttl: z.number().nullish(),
|
||||
token_default_ttl: z.number().nullish(),
|
||||
token_default_audiences: z.array(z.string()).nullish(),
|
||||
service_account_name: z.string().nullish(),
|
||||
kubernetes_role_name: z.string().nullish(),
|
||||
kubernetes_role_type: z.string().nullish(),
|
||||
generated_role_rules: z.string().nullish(),
|
||||
name_template: z.string().nullish(),
|
||||
extra_annotations: z.record(z.string()).nullish(),
|
||||
extra_labels: z.record(z.string()).nullish(),
|
||||
config: z.object({
|
||||
kubernetes_host: z.string(),
|
||||
kubernetes_ca_cert: z.string().nullish()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.migration.getVaultKubernetesRoles({
|
||||
actor: req.permission,
|
||||
namespace: req.query.namespace,
|
||||
mountPath: req.query.mountPath
|
||||
});
|
||||
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/vault/secret-paths",
|
||||
|
||||
@@ -5,5 +5,6 @@ export enum GitLabConnectionMethod {
|
||||
|
||||
export enum GitLabAccessTokenType {
|
||||
Project = "project",
|
||||
Personal = "personal"
|
||||
Personal = "personal",
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
THCVaultKubernetesAuthConfig,
|
||||
THCVaultKubernetesAuthRole,
|
||||
THCVaultKubernetesAuthRoleWithConfig,
|
||||
THCVaultKubernetesRole,
|
||||
THCVaultKubernetesSecretsConfig,
|
||||
THCVaultMount,
|
||||
THCVaultMountResponse
|
||||
} from "./hc-vault-connection-types";
|
||||
@@ -816,3 +818,122 @@ export const getHCVaultKubernetesAuthRoles = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getHCVaultKubernetesRoles = async (
|
||||
namespace: string,
|
||||
mountPath: string,
|
||||
connection: THCVaultConnection,
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||
): Promise<THCVaultKubernetesRole[]> => {
|
||||
// Remove trailing slash from mount path
|
||||
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
||||
|
||||
try {
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||
// 1. Get the Kubernetes secrets engine configuration for this mount
|
||||
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesSecretsConfig }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const kubernetesConfig = configResponse.data;
|
||||
|
||||
// 2. List all roles in this mount
|
||||
let roleNames: string[] = [];
|
||||
try {
|
||||
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles?list=true`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
roleNames = roleListResponse.data.keys || [];
|
||||
} catch (error) {
|
||||
// Vault returns 404 when no roles are configured yet
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const axiosError = error as { response?: { status?: number } };
|
||||
if (axiosError.response?.status === 404) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!roleNames || roleNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Fetch details for each role with concurrency control
|
||||
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
|
||||
|
||||
const roleDetailsPromises = roleNames.map((roleName) =>
|
||||
limiter(async () => {
|
||||
const { data: roleResponse } = await requestWithHCVaultGateway<{
|
||||
data: {
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
};
|
||||
}>(connection, gatewayService, {
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles/${roleName}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Merge the role with the config
|
||||
return {
|
||||
...roleResponse.data,
|
||||
name: roleName,
|
||||
config: kubernetesConfig,
|
||||
mountPath: cleanMountPath
|
||||
} as THCVaultKubernetesRole;
|
||||
})
|
||||
);
|
||||
|
||||
const roles = await Promise.all(roleDetailsPromises);
|
||||
|
||||
return roles;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Unable to list HC Vault Kubernetes secrets engine roles");
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Kubernetes secrets engine roles: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Kubernetes secrets engine roles from HashiCorp Vault"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,3 +95,26 @@ export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole &
|
||||
config: THCVaultKubernetesAuthConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesSecretsConfig = {
|
||||
kubernetes_host: string;
|
||||
kubernetes_ca_cert?: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesRole = {
|
||||
name: string;
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
config: THCVaultKubernetesSecretsConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
@@ -82,7 +82,7 @@ export const importDataIntoInfisicalFn = async ({
|
||||
|
||||
if (existingEnv) {
|
||||
throw new BadRequestError({
|
||||
message: `Environment with slug '${slug}' already exist`,
|
||||
message: `Environment with slug '${slug}' already exists`,
|
||||
name: "CreateEnvironment"
|
||||
});
|
||||
}
|
||||
@@ -312,7 +312,7 @@ export const importDataIntoInfisicalFn = async ({
|
||||
);
|
||||
if (secretsByKeys.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
|
||||
message: `Secret already exists: ${secretsByKeys.map((el) => el.key).join(",")}`
|
||||
});
|
||||
}
|
||||
await fnSecretBulkInsert({
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
convertVaultValueToString,
|
||||
getHCVaultAuthMounts,
|
||||
getHCVaultKubernetesAuthRoles,
|
||||
getHCVaultKubernetesRoles,
|
||||
getHCVaultSecretsForPath,
|
||||
HCVaultAuthType,
|
||||
listHCVaultMounts,
|
||||
@@ -762,6 +763,56 @@ export const externalMigrationServiceFactory = ({
|
||||
return roles;
|
||||
};
|
||||
|
||||
const getVaultKubernetesRoles = async ({
|
||||
actor,
|
||||
namespace,
|
||||
mountPath
|
||||
}: {
|
||||
actor: OrgServiceActor;
|
||||
namespace: string;
|
||||
mountPath: string;
|
||||
}) => {
|
||||
const { hasRole } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
if (!hasRole(OrgMembershipRole.Admin)) {
|
||||
throw new ForbiddenRequestError({ message: "Only admins can get Kubernetes roles" });
|
||||
}
|
||||
|
||||
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
|
||||
orgId: actor.orgId,
|
||||
namespace
|
||||
});
|
||||
|
||||
if (!vaultConfig) {
|
||||
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
|
||||
}
|
||||
|
||||
if (!vaultConfig.connection) {
|
||||
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
|
||||
}
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId: vaultConfig.orgId,
|
||||
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: null
|
||||
});
|
||||
|
||||
const connection = {
|
||||
...vaultConfig.connection,
|
||||
credentials
|
||||
} as THCVaultConnection;
|
||||
|
||||
return getHCVaultKubernetesRoles(namespace, mountPath, connection, gatewayService);
|
||||
};
|
||||
|
||||
return {
|
||||
importEnvKeyData,
|
||||
importVaultData,
|
||||
@@ -776,6 +827,7 @@ export const externalMigrationServiceFactory = ({
|
||||
getVaultAuthMounts,
|
||||
getVaultSecretPaths,
|
||||
importVaultSecrets,
|
||||
getVaultKubernetesAuthRoles
|
||||
getVaultKubernetesAuthRoles,
|
||||
getVaultKubernetesRoles
|
||||
};
|
||||
};
|
||||
|
||||
@@ -112,7 +112,8 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
...KmsKeysSchema.parse(entry),
|
||||
isActive: !entry.isDisabled,
|
||||
algorithm: entry.internalKmsEncryptionAlgorithm,
|
||||
version: entry.internalKmsVersion
|
||||
version: entry.internalKmsVersion,
|
||||
kmipMetadata: entry.kmipMetadata as Record<string, unknown>
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find project cmeks" });
|
||||
|
||||
@@ -27,5 +27,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
scannerProductEnabled: true,
|
||||
shareSecretsProductEnabled: true,
|
||||
maxSharedSecretLifetime: true,
|
||||
maxSharedSecretViewLimit: true
|
||||
maxSharedSecretViewLimit: true,
|
||||
blockDuplicateSecretSyncDestinations: true
|
||||
});
|
||||
|
||||
@@ -405,7 +405,8 @@ export const orgServiceFactory = ({
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
}
|
||||
}: TUpdateOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -516,7 +517,7 @@ export const orgServiceFactory = ({
|
||||
if (slug) {
|
||||
const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null });
|
||||
if (existingOrg && existingOrg?.id !== orgId)
|
||||
throw new BadRequestError({ message: `Organization with slug ${slug} already exist` });
|
||||
throw new BadRequestError({ message: `Organization with slug ${slug} already exists` });
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced) {
|
||||
@@ -589,7 +590,8 @@ export const orgServiceFactory = ({
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
});
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
return org;
|
||||
@@ -1147,7 +1149,7 @@ export const orgServiceFactory = ({
|
||||
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
|
||||
if (doesIncidentContactExist) {
|
||||
throw new BadRequestError({
|
||||
message: "Incident contact already exist",
|
||||
message: "Incident contact already exists",
|
||||
name: "Incident contact exist"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export type TUpdateOrgDTO = {
|
||||
shareSecretsProductEnabled: boolean;
|
||||
maxSharedSecretLifetime: number;
|
||||
maxSharedSecretViewLimit: number | null;
|
||||
blockDuplicateSecretSyncDestinations: boolean;
|
||||
}>;
|
||||
} & TOrgPermission;
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const projectEnvServiceFactory = ({
|
||||
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
||||
if (existingEnv)
|
||||
throw new BadRequestError({
|
||||
message: "Environment with slug already exist",
|
||||
message: "Environment with slug already exists",
|
||||
name: "CreateEnvironment"
|
||||
});
|
||||
|
||||
@@ -171,7 +171,7 @@ export const projectEnvServiceFactory = ({
|
||||
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
||||
if (existingEnv && existingEnv.id !== id) {
|
||||
throw new BadRequestError({
|
||||
message: "Environment with slug already exist",
|
||||
message: "Environment with slug already exists",
|
||||
name: "UpdateEnvironment"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
ProjectType,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TProjectEnvironments
|
||||
TProjectEnvironments,
|
||||
TProjects
|
||||
} from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
@@ -40,7 +41,8 @@ import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-hos
|
||||
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
@@ -310,19 +312,34 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await projectDAL.create(
|
||||
const slug = projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`);
|
||||
|
||||
let project: TProjects;
|
||||
try {
|
||||
project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
type,
|
||||
description: workspaceDescription,
|
||||
orgId: organization.id,
|
||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||
slug,
|
||||
kmsSecretManagerKeyId: kmsKeyId,
|
||||
version: ProjectVersion.V3,
|
||||
pitVersionLimit: 10
|
||||
},
|
||||
tx
|
||||
);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof DatabaseError &&
|
||||
(err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `A project with the slug "${slug}" already exists in your organization. Please choose a different name or slug.`
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (type === ProjectType.SSH) {
|
||||
await bootstrapSshProject({
|
||||
@@ -586,24 +603,13 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
if (update.slug) {
|
||||
const existingProject = await projectDAL.findOne({
|
||||
slug: update.slug,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
if (existingProject && existingProject.id !== project.id) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Only admins can update secret detection ignore values"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedProject = await projectDAL.updateById(project.id, {
|
||||
name: update.name,
|
||||
description: update.description,
|
||||
@@ -619,6 +625,14 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
|
||||
return updatedProject;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update project. A project with the slug "${update.slug}" already exists in your organization. Please choose a different slug.`
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutoCapitalization = async ({
|
||||
|
||||
@@ -15,6 +15,8 @@ import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { deepEqualSkipFields } from "@app/lib/fn/object";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -50,6 +52,8 @@ type TSecretSyncServiceFactoryDep = {
|
||||
secretImportDAL: TSecretImportDALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem">;
|
||||
@@ -68,6 +72,8 @@ export const secretSyncServiceFactory = ({
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectDAL,
|
||||
orgDAL,
|
||||
projectBotService,
|
||||
secretSyncQueue,
|
||||
keyStore,
|
||||
@@ -225,6 +231,61 @@ export const secretSyncServiceFactory = ({
|
||||
return secretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
||||
if (baseFieldsMatch) {
|
||||
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
||||
sync.destinationConfig as Record<string, unknown>,
|
||||
destinationConfig
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const hasDuplicate = duplicates.length > 0;
|
||||
return {
|
||||
hasDuplicate,
|
||||
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
const createSecretSync = async (
|
||||
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
|
||||
actor: OrgServiceActor
|
||||
@@ -271,6 +332,30 @@ export const secretSyncServiceFactory = ({
|
||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
|
||||
const organization = await orgDAL.findById(project.orgId);
|
||||
if (organization?.blockDuplicateSecretSyncDestinations) {
|
||||
const duplicateCheck = await checkDuplicateDestination(
|
||||
{
|
||||
destination: params.destination,
|
||||
destinationConfig: params.destinationConfig,
|
||||
projectId
|
||||
},
|
||||
actor
|
||||
);
|
||||
if (duplicateCheck.hasDuplicate) {
|
||||
throw new BadRequestError({
|
||||
message: `A secret sync with this destination already exists${
|
||||
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
|
||||
}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
|
||||
|
||||
// validates permission to connect and app is valid for sync destination
|
||||
@@ -369,6 +454,33 @@ export const secretSyncServiceFactory = ({
|
||||
|
||||
let { folderId } = secretSync;
|
||||
|
||||
if (params.destinationConfig) {
|
||||
const project = await projectDAL.findById(secretSync.projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
const organization = await orgDAL.findById(project.orgId);
|
||||
|
||||
if (organization?.blockDuplicateSecretSyncDestinations) {
|
||||
const duplicateCheck = await checkDuplicateDestination(
|
||||
{
|
||||
destination,
|
||||
destinationConfig: params.destinationConfig,
|
||||
projectId: secretSync.projectId,
|
||||
excludeSyncId: secretSync.id
|
||||
},
|
||||
actor
|
||||
);
|
||||
if (duplicateCheck.hasDuplicate) {
|
||||
throw new BadRequestError({
|
||||
message: `A secret sync with this destination already exists${
|
||||
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
|
||||
}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (params.connectionId) {
|
||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
|
||||
|
||||
@@ -703,61 +815,6 @@ export const secretSyncServiceFactory = ({
|
||||
return updatedSecretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
||||
if (baseFieldsMatch) {
|
||||
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
||||
sync.destinationConfig as Record<string, unknown>,
|
||||
destinationConfig
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const hasDuplicate = duplicates.length > 0;
|
||||
return {
|
||||
hasDuplicate,
|
||||
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretSyncOptions,
|
||||
listSecretSyncsByProjectId,
|
||||
|
||||
@@ -35,7 +35,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
||||
|
||||
const existingTag = await secretTagDAL.findOne({ slug, projectId });
|
||||
if (existingTag) throw new BadRequestError({ message: "Tag already exist" });
|
||||
if (existingTag) throw new BadRequestError({ message: "Tag already exists" });
|
||||
|
||||
const newTag = await secretTagDAL.create({
|
||||
projectId,
|
||||
@@ -53,7 +53,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
|
||||
if (slug) {
|
||||
const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId });
|
||||
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
|
||||
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exists" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
|
||||
@@ -282,7 +282,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
folderId
|
||||
});
|
||||
if (inputSecret.type === SecretType.Shared && doesSecretExist)
|
||||
throw new BadRequestError({ message: "Secret already exist" });
|
||||
throw new BadRequestError({ message: "Secret already exists" });
|
||||
|
||||
// if user creating personal check its shared also exist
|
||||
if (inputSecret.type === SecretType.Personal && !doesSecretExist) {
|
||||
@@ -527,7 +527,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
type: SecretType.Shared,
|
||||
folderId
|
||||
});
|
||||
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" });
|
||||
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exists" });
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
@@ -1674,7 +1674,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
}
|
||||
});
|
||||
if (secrets.length)
|
||||
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
|
||||
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
|
||||
|
||||
@@ -525,7 +525,7 @@ export const fnSecretBlindIndexCheck = async ({
|
||||
);
|
||||
|
||||
if (isNew) {
|
||||
if (secrets.length) throw new BadRequestError({ message: "Secret already exist" });
|
||||
if (secrets.length) throw new BadRequestError({ message: "Secret already exists" });
|
||||
} else {
|
||||
const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map(
|
||||
(el) => blindIndex2KeyName[el.secretBlindIndex as string]
|
||||
@@ -819,7 +819,7 @@ export const createManySecretsRawFnFactory = ({
|
||||
);
|
||||
if (secretsStoredInDB.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret already exist: ${secretsStoredInDB.map((el) => el.key).join(",")}`
|
||||
message: `Secret already exists: ${secretsStoredInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
|
||||
const inputSecrets = secrets.map((secret) => {
|
||||
|
||||
@@ -2751,7 +2751,7 @@ export const secretServiceFactory = ({
|
||||
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
|
||||
|
||||
if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) {
|
||||
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
|
||||
throw new BadRequestError({ message: "One or more tags already exists on the secret" });
|
||||
}
|
||||
|
||||
const combinedTags = new Set([...existingSecretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
|
||||
|
||||
@@ -8,3 +8,9 @@ secrets via HTTPS requests. This can be useful for automating tasks, such as
|
||||
rotating credentials, or for integrating secret management into a larger system.
|
||||
|
||||
With the Public API, you can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
|
||||
|
||||
## API Versioning
|
||||
|
||||
The API is versioned on a per-resource basis. A resource's version is only incremented for breaking changes, so different endpoints may have different version numbers (e.g., `/api/v4/secrets` vs. `/api/v1/secret-syncs`).
|
||||
|
||||
As a best practice, always use the latest available version for each endpoint to ensure access to the most recent features and improvements.
|
||||
|
||||
@@ -26,6 +26,12 @@ Infisical provides two approaches for migrating from HashiCorp Vault.
|
||||
|
||||
This migration approach lets you set up a connection to your Vault instance once, then import specific resources as needed throughout Infisical.
|
||||
|
||||
<Warning>
|
||||
**Organization Admin Access Required:** All in-platform migration features
|
||||
(importing secrets, Kubernetes configurations, and policies from Vault) are
|
||||
only accessible to organization admins.
|
||||
</Warning>
|
||||
|
||||
### Step 1: Set Up Your Vault Connection
|
||||
|
||||
<Steps>
|
||||
@@ -84,6 +90,19 @@ This migration approach lets you set up a connection to your Vault instance once
|
||||
path "auth/+/role/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
# Kubernetes secrets engine - for reading secrets engine configuration and roles
|
||||
path "+/config" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "+/roles" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
|
||||
path "+/roles/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
@@ -160,6 +179,34 @@ The authentication settings (service accounts, TTL, policies, etc.) will be auto
|
||||
must be manually provided in the form after importing the configuration.
|
||||
</Note>
|
||||
|
||||
#### Import Kubernetes Dynamic Secret Configurations
|
||||
|
||||
When creating a Kubernetes dynamic secret, you can import the configuration from a Vault Kubernetes secrets engine role:
|
||||
|
||||
1. Navigate to your project and select an environment
|
||||
2. Click **"+ Add Secret"** dropdown and choose **"Dynamic Secret"**
|
||||
3. Select **Kubernetes** as the provider
|
||||
4. Click **"Load from Vault"** at the top of the form
|
||||
|
||||

|
||||
|
||||
5. Select your Vault namespace, Kubernetes secrets engine mount, and role
|
||||
6. Click **"Load Configuration"**
|
||||
|
||||
The form will be automatically populated with the role's configuration including:
|
||||
|
||||
- Cluster URL and CA certificate
|
||||
- Credential type (Static or Dynamic)
|
||||
- Service account name or Kubernetes role settings
|
||||
- Allowed namespaces
|
||||
- Token TTL values
|
||||
- Token audiences
|
||||
|
||||
<Note>
|
||||
Sensitive values like cluster tokens cannot be retrieved from Vault and must
|
||||
be manually provided in the form after loading the configuration.
|
||||
</Note>
|
||||
|
||||
#### Import and Translate Access Control Policies
|
||||
|
||||
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 424 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
@@ -187,6 +187,65 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
|
||||
<Tab title="Group Access Token">
|
||||
Group access tokens provide access to all projects within a GitLab group, offering group-level control.
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Group Settings">
|
||||
Go to your GitLab group and navigate to Settings > Access Tokens. Click **Add new token** to create a new group access token.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure Token">
|
||||
Fill in the token details:
|
||||
- **Token name**: A descriptive name for the token
|
||||
- **Expiration date**: Set an appropriate expiration date
|
||||
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
For Secret Syncs, the required role depends on your sync destination:
|
||||
- **Project variables**: Requires **Maintainer** role or higher
|
||||
- **Group variables**: Requires **Owner** role
|
||||
|
||||
Your token will require the `api` scope.
|
||||
|
||||

|
||||
|
||||
Click **Create group access token** to create the token.
|
||||
|
||||
<Note>
|
||||
Use the **Owner** role if you need to sync to group-level variables. The **Maintainer** role is sufficient only for project-level variables.
|
||||
</Note>
|
||||
</Tab>
|
||||
<Tab title="Secret Scanning">
|
||||
To set up Secret Scanning, the required permissions depend on the data source level:
|
||||
- **Project-level data source:** Requires **Maintainer** role or higher
|
||||
- **Group-level data source:** Requires **Owner** role
|
||||
|
||||
Your token will require the `api` scope.
|
||||
|
||||

|
||||
|
||||
Click **Create group access token** to create the token.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
Group Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed.
|
||||
</Info>
|
||||
</Step>
|
||||
<Step title="Copy Token">
|
||||
Copy the generated token immediately as it won't be shown again.
|
||||

|
||||
<Warning>
|
||||
Keep your access token secure and do not share it. Anyone with access to this token can access all projects within your GitLab group.
|
||||
</Warning>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Setup GitLab Access Token Connection in Infisical
|
||||
@@ -206,7 +265,9 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
||||

|
||||
|
||||
Click **Connect** to establish the connection.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Connection Created">
|
||||
Your **GitLab Connection** is now available for use.
|
||||

|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Decorator } from "@storybook/react-vite";
|
||||
|
||||
export const DocumentDecorator: Decorator = (Story) => {
|
||||
useEffect(() => {
|
||||
const root = document.getElementsByTagName("html")[0];
|
||||
const root = document.documentElement;
|
||||
|
||||
root.setAttribute("class", "overflow-visible");
|
||||
}, []);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { components, OptionProps } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faBuilding, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { TAvailableAppConnection } from "@app/hooks/api/appConnections";
|
||||
|
||||
export const AppConnectionOption = ({
|
||||
@@ -13,6 +15,8 @@ export const AppConnectionOption = ({
|
||||
}: OptionProps<TAvailableAppConnection>) => {
|
||||
const isCreateOption = props.data.id === "_create";
|
||||
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
@@ -23,15 +27,22 @@ export const AppConnectionOption = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="truncate">{children}</p>
|
||||
<p className="mr-auto truncate">{children}</p>
|
||||
{!props.data.projectId && (
|
||||
<Tooltip content="This connection belongs to your organization.">
|
||||
<div className="mr-auto ml-2">
|
||||
<Badge className="flex h-5 w-min items-center gap-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300 hover:text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBuilding} size="sm" />
|
||||
<Tooltip
|
||||
content={`This connection belongs to your ${isSubOrganization ? "sub-" : ""}organization.`}
|
||||
>
|
||||
{isSubOrganization ? (
|
||||
<Badge variant="sub-org">
|
||||
<SubOrgIcon />
|
||||
Sub-Organization
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="org">
|
||||
<OrgIcon />
|
||||
Organization
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
{isSelected && (
|
||||
|
||||
@@ -4,7 +4,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
EmptyState,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
CertStatus,
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faDownload,
|
||||
faTriangleExclamation,
|
||||
faXmark,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { differenceInSeconds } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { CheckIcon, DownloadIcon, LucideIcon, TriangleAlertIcon } from "lucide-react";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge, TBadgeProps } from "@app/components/v3";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
className?: string;
|
||||
mini?: boolean;
|
||||
};
|
||||
|
||||
export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) => {
|
||||
export const PkiSyncImportStatusBadge = ({ pkiSync, mini }: Props) => {
|
||||
const { importStatus, lastImportMessage, lastImportedAt, destination } = pkiSync;
|
||||
const [hide, setHide] = useState(importStatus === PkiSyncStatus.Succeeded);
|
||||
const destinationName = PKI_SYNC_MAP[destination].name;
|
||||
@@ -50,24 +43,24 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
|
||||
|
||||
if (!importStatus || hide) return null;
|
||||
|
||||
let variant: BadgeProps["variant"];
|
||||
let variant: TBadgeProps["variant"];
|
||||
let label: string;
|
||||
let icon: IconDefinition;
|
||||
let Icon: LucideIcon;
|
||||
let tooltipContent: ReactNode;
|
||||
|
||||
switch (importStatus) {
|
||||
case PkiSyncStatus.Pending:
|
||||
case PkiSyncStatus.Running:
|
||||
variant = "primary";
|
||||
variant = "warning";
|
||||
label = "Importing Certificates...";
|
||||
tooltipContent = `Importing certificates from ${destinationName}. This may take a moment.`;
|
||||
icon = faDownload;
|
||||
Icon = DownloadIcon;
|
||||
|
||||
break;
|
||||
case PkiSyncStatus.Failed:
|
||||
variant = "danger";
|
||||
label = "Failed to Import Certificates";
|
||||
icon = faTriangleExclamation;
|
||||
Icon = TriangleAlertIcon;
|
||||
tooltipContent = (
|
||||
<div className="flex flex-col gap-2 py-1 whitespace-normal">
|
||||
{failureMessage && (
|
||||
@@ -93,20 +86,15 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
|
||||
tooltipContent = "Successfully imported certificates.";
|
||||
variant = "success";
|
||||
label = "Certificates Imported";
|
||||
icon = faCheck;
|
||||
Icon = CheckIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
|
||||
<div>
|
||||
<Badge
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
variant={variant}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
{!mini && <span>{label}</span>}
|
||||
<Badge isSquare={mini} variant={variant}>
|
||||
<Icon />
|
||||
{!mini && label}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
@@ -20,24 +18,12 @@ export const PkiSyncModalHeader = ({ destination, isConfigured }: Props) => {
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
{destinationDetails.name} Certificate Sync
|
||||
<a
|
||||
target="_blank"
|
||||
href={`https://infisical.com/docs/integrations/pki-syncs/${destination}`}
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
<DocumentationLinkBadge
|
||||
href={`https://infisical.com/docs/documentation/platform/pki/certificate-syncs/${destination}`}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">
|
||||
{isConfigured
|
||||
? `Edit ${destinationDetails.name} Certificate Sync`
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faEraser,
|
||||
faTriangleExclamation,
|
||||
faXmark,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { differenceInSeconds } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { AlertTriangleIcon, CheckIcon, EraserIcon, LucideIcon } from "lucide-react";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge, TBadgeProps } from "@app/components/v3";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
className?: string;
|
||||
mini?: boolean;
|
||||
};
|
||||
|
||||
export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) => {
|
||||
export const PkiSyncRemoveStatusBadge = ({ pkiSync, mini }: Props) => {
|
||||
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = pkiSync;
|
||||
const [hide, setHide] = useState(removeStatus === PkiSyncStatus.Succeeded);
|
||||
const destinationName = PKI_SYNC_MAP[destination].name;
|
||||
@@ -50,24 +43,24 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
|
||||
|
||||
if (!removeStatus || hide) return null;
|
||||
|
||||
let variant: BadgeProps["variant"];
|
||||
let variant: TBadgeProps["variant"];
|
||||
let label: string;
|
||||
let icon: IconDefinition;
|
||||
let Icon: LucideIcon;
|
||||
let tooltipContent: ReactNode;
|
||||
|
||||
switch (removeStatus) {
|
||||
case PkiSyncStatus.Pending:
|
||||
case PkiSyncStatus.Running:
|
||||
variant = "primary";
|
||||
variant = "warning";
|
||||
label = "Removing Certificates...";
|
||||
tooltipContent = `Removing certificates from ${destinationName}. This may take a moment.`;
|
||||
icon = faEraser;
|
||||
Icon = EraserIcon;
|
||||
|
||||
break;
|
||||
case PkiSyncStatus.Failed:
|
||||
variant = "danger";
|
||||
label = "Failed to Remove Certificates";
|
||||
icon = faTriangleExclamation;
|
||||
Icon = AlertTriangleIcon;
|
||||
tooltipContent = (
|
||||
<div className="flex flex-col gap-2 py-1 whitespace-normal">
|
||||
{failureMessage && (
|
||||
@@ -93,20 +86,15 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
|
||||
tooltipContent = "Successfully removed certificates.";
|
||||
variant = "success";
|
||||
label = "Certificates Removed";
|
||||
icon = faCheck;
|
||||
Icon = CheckIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
|
||||
<div>
|
||||
<Badge
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
variant={variant}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
{!mini && <span>{label}</span>}
|
||||
<Badge isSquare={mini} variant={variant}>
|
||||
<Icon />
|
||||
{!mini && label}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,7 +69,10 @@ export const PkiSyncSelect = ({ onSelect }: Props) => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseCertificateSyncs
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
? handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true,
|
||||
text: "You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
|
||||
})
|
||||
: onSelect(destination)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
@@ -148,6 +151,7 @@ export const PkiSyncSelect = ({ onSelect }: Props) => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
text="You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
import {
|
||||
faCheck,
|
||||
faExclamationTriangle,
|
||||
faHourglass,
|
||||
faRotate,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
AlertTriangleIcon,
|
||||
CheckIcon,
|
||||
HourglassIcon,
|
||||
LucideIcon,
|
||||
RefreshCwIcon
|
||||
} from "lucide-react";
|
||||
|
||||
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { Badge, TBadgeProps } from "@app/components/v3";
|
||||
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
type Props = {
|
||||
status: PkiSyncStatus;
|
||||
} & Omit<BadgeProps, "children" | "variant">;
|
||||
} & Omit<TBadgeProps, "children" | "variant">;
|
||||
|
||||
export const PkiSyncStatusBadge = ({ status }: Props) => {
|
||||
let variant: BadgeProps["variant"];
|
||||
let variant: TBadgeProps["variant"];
|
||||
let text: string;
|
||||
let icon: IconDefinition;
|
||||
let Icon: LucideIcon;
|
||||
|
||||
switch (status) {
|
||||
case PkiSyncStatus.Failed:
|
||||
variant = "danger";
|
||||
text = "Failed to Sync";
|
||||
icon = faExclamationTriangle;
|
||||
Icon = AlertTriangleIcon;
|
||||
break;
|
||||
case PkiSyncStatus.Succeeded:
|
||||
variant = "success";
|
||||
text = "Synced";
|
||||
icon = faCheck;
|
||||
Icon = CheckIcon;
|
||||
break;
|
||||
case PkiSyncStatus.Pending:
|
||||
variant = "primary";
|
||||
variant = "info";
|
||||
text = "Queued";
|
||||
icon = faHourglass;
|
||||
Icon = HourglassIcon;
|
||||
break;
|
||||
case PkiSyncStatus.Running:
|
||||
default:
|
||||
variant = "primary";
|
||||
variant = "info";
|
||||
text = "Syncing";
|
||||
icon = faRotate;
|
||||
Icon = RefreshCwIcon;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
className={status === PkiSyncStatus.Running ? "animate-spin" : ""}
|
||||
/>
|
||||
<span>{text}</span>
|
||||
<Badge variant={variant}>
|
||||
<Icon className={status === PkiSyncStatus.Running ? "animate-spin" : ""} />
|
||||
{text}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { faPlug, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
type Props = {
|
||||
@@ -29,10 +29,10 @@ const getSyncStatusBadge = (status?: string) => {
|
||||
case "FAILED":
|
||||
return <Badge variant="danger">Failed</Badge>;
|
||||
case "RUNNING":
|
||||
return <Badge variant="primary">Running</Badge>;
|
||||
return <Badge variant="warning">Running</Badge>;
|
||||
case "PENDING":
|
||||
default:
|
||||
return <Badge variant="primary">Pending</Badge>;
|
||||
return <Badge variant="warning">Pending</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ export const PkiSyncTable = ({ pkiSyncs, onEdit, onDelete, onTrigger }: Props) =
|
||||
<Td>{pkiSync.name}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="primary">{pkiSync.destination}</Badge>
|
||||
<Badge variant="warning">{pkiSync.destination}</Badge>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{pkiSync.appConnectionName || "Unknown"}</Td>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { Badge, GenericFieldLabel } from "@app/components/v2";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
|
||||
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms";
|
||||
@@ -8,6 +6,7 @@ import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms
|
||||
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
|
||||
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { ProjectEnv } from "@app/hooks/api/projects/types";
|
||||
import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
@@ -115,23 +114,9 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
|
||||
selectedRotation ? (
|
||||
<SecretRotationV2ModalHeader isConfigured={false} type={selectedRotation} />
|
||||
) : (
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
Add Secret Rotation
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview"
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-rotation/overview" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
@@ -20,24 +18,12 @@ export const SecretRotationV2ModalHeader = ({ type, isConfigured }: Props) => {
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
{destinationDetails.name} Rotation
|
||||
<a
|
||||
target="_blank"
|
||||
<DocumentationLinkBadge
|
||||
href={`https://infisical.com/docs/documentation/platform/secret-rotation/${type}`}
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">
|
||||
{isConfigured
|
||||
? `Edit ${destinationDetails.name} Rotation`
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { faBan, faRotate, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { BanIcon, RefreshCwIcon, XIcon } from "lucide-react";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge/Badge";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
type Props = {
|
||||
secretRotation: TSecretRotationV2;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props) => {
|
||||
export const SecretRotationV2StatusBadge = ({ secretRotation }: Props) => {
|
||||
const { isAutoRotationEnabled, rotationStatus, nextRotationAt, lastRotationMessage } =
|
||||
secretRotation;
|
||||
|
||||
@@ -50,28 +49,18 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Badge
|
||||
variant="danger"
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<Badge variant="danger">
|
||||
<XIcon />
|
||||
Rotation Failed
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAutoRotationEnabled) {
|
||||
return (
|
||||
<Badge
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
<Badge variant="neutral">
|
||||
<BanIcon />
|
||||
Auto-Rotation Disabled
|
||||
</Badge>
|
||||
);
|
||||
@@ -92,20 +81,12 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Badge
|
||||
variant={daysToRotation >= 7 ? "success" : "primary"}
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
<Badge variant={daysToRotation >= 7 ? "info" : "warning"} className="capitalize">
|
||||
<RefreshCwIcon />
|
||||
{daysToRotation < 0
|
||||
? "Rotating"
|
||||
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-5
@@ -1,8 +1,10 @@
|
||||
import { ReactNode } from "react";
|
||||
import { faArrowRight, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { KeyIcon } from "lucide-react";
|
||||
|
||||
import { Badge, FormLabel } from "@app/components/v2";
|
||||
import { FormLabel } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
|
||||
type Props = {
|
||||
items: { name: string; input: ReactNode }[];
|
||||
@@ -32,9 +34,10 @@ export const SecretsMappingTable = ({ items }: Props) => {
|
||||
<tr key={name}>
|
||||
<td className="whitespace-nowrap">
|
||||
<div className="mb-4 flex h-full items-start justify-center">
|
||||
<Badge className="pointer-events-none flex h-[36px] w-full items-center justify-center gap-1.5 border border-mineshaft-600 bg-mineshaft-600 whitespace-nowrap text-bunker-200">
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
<span>{name}</span>
|
||||
{/* TODO(scott): probably shouldn't be a badge */}
|
||||
<Badge variant="neutral" className="h-[36px] w-full justify-center text-xs">
|
||||
<KeyIcon />
|
||||
{name}
|
||||
</Badge>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
TSecretScanningDataSource
|
||||
@@ -108,23 +107,9 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
|
||||
selectedDataSource ? (
|
||||
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
|
||||
) : (
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
Add Data Source
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-scanning/overview" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
@@ -20,24 +18,12 @@ export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Prop
|
||||
className="h-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
{dataSourceDetails.name} Data Source
|
||||
<a
|
||||
target="_blank"
|
||||
<DocumentationLinkBadge
|
||||
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">
|
||||
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
|
||||
</p>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { CheckIcon, RotateCwIcon, XIcon } from "lucide-react";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
status: SecretScanningScanStatus;
|
||||
statusMessage?: string | null;
|
||||
className?: string;
|
||||
|
||||
scannedAt?: string | null;
|
||||
};
|
||||
|
||||
export const SecretScanningScanStatusBadge = ({
|
||||
status,
|
||||
statusMessage,
|
||||
className,
|
||||
|
||||
scannedAt
|
||||
}: Props) => {
|
||||
if (status === SecretScanningScanStatus.Failed) {
|
||||
@@ -53,11 +54,8 @@ export const SecretScanningScanStatusBadge = ({
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Badge
|
||||
variant="danger"
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<Badge variant="danger">
|
||||
<XIcon />
|
||||
Scan Error
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -67,26 +65,17 @@ export const SecretScanningScanStatusBadge = ({
|
||||
|
||||
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
|
||||
return (
|
||||
<Badge
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
variant="primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
|
||||
<span>Scanning</span>
|
||||
<Badge variant="info">
|
||||
<RotateCwIcon className="animate-spin" />
|
||||
Scanning
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="success"
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
<span>Complete</span>
|
||||
<Badge variant="success">
|
||||
<CheckIcon />
|
||||
Complete
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faDownload,
|
||||
faTriangleExclamation,
|
||||
faXmark,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { differenceInSeconds } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { AlertTriangleIcon, CheckIcon, DownloadIcon, LucideIcon } from "lucide-react";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge, TBadgeProps } from "@app/components/v3";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
className?: string;
|
||||
mini?: boolean;
|
||||
};
|
||||
|
||||
export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Props) => {
|
||||
export const SecretSyncImportStatusBadge = ({ secretSync, mini }: Props) => {
|
||||
const { importStatus, lastImportMessage, lastImportedAt, destination } = secretSync;
|
||||
const [hide, setHide] = useState(importStatus === SecretSyncStatus.Succeeded);
|
||||
const destinationName = SECRET_SYNC_MAP[destination].name;
|
||||
@@ -50,24 +43,24 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
|
||||
|
||||
if (!importStatus || hide) return null;
|
||||
|
||||
let variant: BadgeProps["variant"];
|
||||
let variant: TBadgeProps["variant"];
|
||||
let label: string;
|
||||
let icon: IconDefinition;
|
||||
let Icon: LucideIcon;
|
||||
let tooltipContent: ReactNode;
|
||||
|
||||
switch (importStatus) {
|
||||
case SecretSyncStatus.Pending:
|
||||
case SecretSyncStatus.Running:
|
||||
variant = "primary";
|
||||
variant = "warning";
|
||||
label = "Importing Secrets...";
|
||||
tooltipContent = `Importing secrets from ${destinationName}. This may take a moment.`;
|
||||
icon = faDownload;
|
||||
Icon = DownloadIcon;
|
||||
|
||||
break;
|
||||
case SecretSyncStatus.Failed:
|
||||
variant = "danger";
|
||||
label = "Failed to Import Secrets";
|
||||
icon = faTriangleExclamation;
|
||||
Icon = AlertTriangleIcon;
|
||||
tooltipContent = (
|
||||
<div className="flex flex-col gap-2 py-1 whitespace-normal">
|
||||
{failureMessage && (
|
||||
@@ -93,20 +86,15 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
|
||||
tooltipContent = "Successfully imported secrets.";
|
||||
variant = "success";
|
||||
label = "Secrets Imported";
|
||||
icon = faCheck;
|
||||
Icon = CheckIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
|
||||
<div>
|
||||
<Badge
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
variant={variant}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
{!mini && <span>{label}</span>}
|
||||
<Badge isSquare={mini} variant={variant}>
|
||||
<Icon />
|
||||
{!mini && label}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -20,24 +18,12 @@ export const SecretSyncModalHeader = ({ destination, isConfigured }: Props) => {
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 object-contain p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
{destinationDetails.name} Sync
|
||||
<a
|
||||
target="_blank"
|
||||
<DocumentationLinkBadge
|
||||
href={`https://infisical.com/docs/integrations/secret-syncs/${destination}`}
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">
|
||||
{isConfigured
|
||||
? `Edit ${destinationDetails.name} Sync`
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faEraser,
|
||||
faTriangleExclamation,
|
||||
faXmark,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { differenceInSeconds } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { AlertTriangleIcon, CheckIcon, EraserIcon, LucideIcon } from "lucide-react";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { Badge, TBadgeProps } from "@app/components/v3";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
className?: string;
|
||||
mini?: boolean;
|
||||
};
|
||||
|
||||
export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Props) => {
|
||||
export const SecretSyncRemoveStatusBadge = ({ secretSync, mini }: Props) => {
|
||||
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = secretSync;
|
||||
const [hide, setHide] = useState(removeStatus === SecretSyncStatus.Succeeded);
|
||||
const destinationName = SECRET_SYNC_MAP[destination].name;
|
||||
@@ -50,24 +43,24 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
|
||||
|
||||
if (!removeStatus || hide) return null;
|
||||
|
||||
let variant: BadgeProps["variant"];
|
||||
let variant: TBadgeProps["variant"];
|
||||
let label: string;
|
||||
let icon: IconDefinition;
|
||||
let Icon: LucideIcon;
|
||||
let tooltipContent: ReactNode;
|
||||
|
||||
switch (removeStatus) {
|
||||
case SecretSyncStatus.Pending:
|
||||
case SecretSyncStatus.Running:
|
||||
variant = "primary";
|
||||
variant = "warning";
|
||||
label = "Removing Secrets...";
|
||||
tooltipContent = `Removing secrets from ${destinationName}. This may take a moment.`;
|
||||
icon = faEraser;
|
||||
Icon = EraserIcon;
|
||||
|
||||
break;
|
||||
case SecretSyncStatus.Failed:
|
||||
variant = "danger";
|
||||
label = "Failed to Remove Secrets";
|
||||
icon = faTriangleExclamation;
|
||||
Icon = AlertTriangleIcon;
|
||||
tooltipContent = (
|
||||
<div className="flex flex-col gap-2 py-1 whitespace-normal">
|
||||
{failureMessage && (
|
||||
@@ -93,20 +86,15 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
|
||||
tooltipContent = "Successfully removed secrets.";
|
||||
variant = "success";
|
||||
label = "Secrets Removed";
|
||||
icon = faCheck;
|
||||
Icon = CheckIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
|
||||
<div>
|
||||
<Badge
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
variant={variant}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
{!mini && <span>{label}</span>}
|
||||
<Badge isSquare={mini} variant={variant}>
|
||||
<Icon />
|
||||
{!mini && label}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,7 +67,9 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseSecretSyncs
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
? handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true
|
||||
})
|
||||
: onSelect(destination)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
@@ -145,6 +147,7 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
|
||||
)}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use every Secret Sync if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
import {
|
||||
faCheck,
|
||||
faExclamationTriangle,
|
||||
faHourglass,
|
||||
faRotate,
|
||||
IconDefinition
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
AlertTriangleIcon,
|
||||
CheckIcon,
|
||||
HourglassIcon,
|
||||
LucideIcon,
|
||||
RefreshCwIcon
|
||||
} from "lucide-react";
|
||||
|
||||
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { Badge, TBadgeProps } from "@app/components/v3";
|
||||
import { SecretSyncStatus } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
type Props = {
|
||||
status: SecretSyncStatus;
|
||||
} & Omit<BadgeProps, "children" | "variant">;
|
||||
} & Omit<TBadgeProps, "children" | "variant">;
|
||||
|
||||
export const SecretSyncStatusBadge = ({ status }: Props) => {
|
||||
let variant: BadgeProps["variant"];
|
||||
let variant: TBadgeProps["variant"];
|
||||
let text: string;
|
||||
let icon: IconDefinition;
|
||||
let Icon: LucideIcon;
|
||||
|
||||
switch (status) {
|
||||
case SecretSyncStatus.Failed:
|
||||
variant = "danger";
|
||||
text = "Failed to Sync";
|
||||
icon = faExclamationTriangle;
|
||||
Icon = AlertTriangleIcon;
|
||||
break;
|
||||
case SecretSyncStatus.Succeeded:
|
||||
variant = "success";
|
||||
text = "Synced";
|
||||
icon = faCheck;
|
||||
Icon = CheckIcon;
|
||||
break;
|
||||
case SecretSyncStatus.Pending:
|
||||
variant = "primary";
|
||||
variant = "info";
|
||||
text = "Queued";
|
||||
icon = faHourglass;
|
||||
Icon = HourglassIcon;
|
||||
break;
|
||||
case SecretSyncStatus.Running:
|
||||
default:
|
||||
variant = "primary";
|
||||
variant = "info";
|
||||
text = "Syncing";
|
||||
icon = faRotate;
|
||||
Icon = RefreshCwIcon;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""}
|
||||
/>
|
||||
<span>{text}</span>
|
||||
<Badge variant={variant}>
|
||||
<Icon className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""} />
|
||||
{text}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,13 +8,14 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Switch } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useOrganization, useProject } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
SecretSync,
|
||||
SecretSyncInitialSyncBehavior,
|
||||
TSecretSync,
|
||||
useCreateSecretSync,
|
||||
useDuplicateDestinationCheck,
|
||||
useSecretSyncOption
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -48,6 +49,7 @@ export const CreateSecretSyncForm = ({
|
||||
}: Props) => {
|
||||
const createSecretSync = useCreateSecretSync();
|
||||
const { currentProject } = useProject();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
@@ -106,11 +108,20 @@ export const CreateSecretSyncForm = ({
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { handleSubmit, trigger, control } = formMethods;
|
||||
const { handleSubmit, trigger, control, watch } = formMethods;
|
||||
|
||||
const { hasDuplicate } = useDuplicateDestinationCheck({
|
||||
destination,
|
||||
projectId: currentProject?.id || "",
|
||||
enabled: true,
|
||||
destinationConfig: watch("destinationConfig")
|
||||
});
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
||||
const isCreateButtonDisabled =
|
||||
isFinalStep && hasDuplicate && currentOrg?.blockDuplicateSecretSyncDestinations;
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
@@ -245,7 +256,7 @@ export const CreateSecretSyncForm = ({
|
||||
</FormProvider>
|
||||
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
<Button onClick={handleNext} colorSchema="secondary" isDisabled={isCreateButtonDisabled}>
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
{selectedTabIndex > 0 && (
|
||||
|
||||
+12
-3
@@ -6,6 +6,7 @@ type Props = {
|
||||
onConfirm: () => void;
|
||||
isLoading?: boolean;
|
||||
duplicateProjectId?: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const DuplicateDestinationConfirmationModal = ({
|
||||
@@ -13,7 +14,8 @@ export const DuplicateDestinationConfirmationModal = ({
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
isLoading,
|
||||
duplicateProjectId
|
||||
duplicateProjectId,
|
||||
isDisabled
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
@@ -21,7 +23,12 @@ export const DuplicateDestinationConfirmationModal = ({
|
||||
<div className="mb-4 text-sm">
|
||||
<p>
|
||||
Another secret sync in your organization is already configured with the same
|
||||
destination. Proceeding may cause conflicts or overwrite existing data.
|
||||
destination.{" "}
|
||||
<span className={isDisabled ? "text-red-400" : ""}>
|
||||
{isDisabled
|
||||
? "Your organization does not allow duplicate destination configurations."
|
||||
: "Proceeding may cause conflicts or overwrite existing data."}
|
||||
</span>
|
||||
</p>
|
||||
{duplicateProjectId && (
|
||||
<p className="mt-2 text-xs text-mineshaft-400">
|
||||
@@ -31,9 +38,10 @@ export const DuplicateDestinationConfirmationModal = ({
|
||||
</code>
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2">Are you sure you want to continue?</p>
|
||||
{!isDisabled && <p className="mt-2">Are you sure you want to continue?</p>}
|
||||
</div>
|
||||
|
||||
{!isDisabled && (
|
||||
<div className="flex items-center gap-4 pt-4">
|
||||
<ModalClose asChild>
|
||||
<Button
|
||||
@@ -51,6 +59,7 @@ export const DuplicateDestinationConfirmationModal = ({
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretSyncEditFields } from "@app/components/secret-syncs/types";
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
TSecretSync,
|
||||
@@ -30,6 +31,7 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[secretSync.destination];
|
||||
const [showDuplicateConfirmation, setShowDuplicateConfirmation] = useState(false);
|
||||
const [pendingFormData, setPendingFormData] = useState<TSecretSyncForm | null>(null);
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const formMethods = useForm<TSecretSyncForm>({
|
||||
resolver: zodResolver(UpdateSecretSyncFormSchema),
|
||||
@@ -209,6 +211,7 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
||||
onConfirm={handleConfirmDuplicate}
|
||||
isLoading={updateSecretSync.isPending}
|
||||
duplicateProjectId={storedDuplicateProjectId}
|
||||
isDisabled={currentOrg?.blockDuplicateSecretSyncDestinations}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+3
-12
@@ -5,14 +5,8 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import {
|
||||
Badge,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
useGcpConnectionListProjectLocations,
|
||||
@@ -26,10 +20,7 @@ import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => (
|
||||
<div className="flex w-full flex-row items-center gap-1">
|
||||
<span>{displayName}</span>{" "}
|
||||
<Badge className="h-5 leading-5" variant="success">
|
||||
{locationId}
|
||||
</Badge>
|
||||
<span>{displayName}</span> <Badge variant="info">{locationId}</Badge>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
+3
-2
@@ -2,7 +2,8 @@ import { components, OptionProps, SingleValue } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, FilterableSelect } from "@app/components/v2";
|
||||
import { FilterableSelect } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
|
||||
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
|
||||
@@ -10,7 +11,7 @@ const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGI
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p className="truncate">{children}</p>
|
||||
<Badge variant="success" className="mr-auto ml-1 cursor-pointer">
|
||||
<Badge variant="neutral" className="mr-auto ml-1">
|
||||
{props.data.slug}
|
||||
</Badge>
|
||||
{isSelected && (
|
||||
|
||||
+5
-7
@@ -1,10 +1,10 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { EyeIcon } from "lucide-react";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -41,11 +41,9 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
+6
-8
@@ -1,10 +1,10 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { EyeIcon } from "lucide-react";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
@@ -26,7 +26,7 @@ export const AwsSecretsManagerSyncReviewFields = () => {
|
||||
<>
|
||||
<GenericFieldLabel label="Region">
|
||||
{awsRegion?.name}
|
||||
<Badge className="ml-1" variant="success">
|
||||
<Badge className="ml-1" variant="info">
|
||||
{awsRegion?.slug}{" "}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
@@ -73,11 +73,9 @@ export const AwsSecretsManagerSyncOptionsReviewFields = () => {
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>
|
||||
<Badge variant="neutral">
|
||||
<EyeIcon />
|
||||
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
|
||||
+44
-11
@@ -5,8 +5,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useOrganization, useProject } from "@app/context";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@@ -51,6 +51,7 @@ import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
|
||||
export const SecretSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
const { currentProject } = useProject();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
let DestinationFieldsComponent: ReactNode;
|
||||
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||
@@ -193,18 +194,50 @@ export const SecretSyncReviewFields = () => {
|
||||
{isChecking && <span className="text-xs text-mineshaft-400">Checking...</span>}
|
||||
</div>
|
||||
{hasDuplicate && (
|
||||
<div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 px-3 py-2">
|
||||
<div className="flex text-sm text-yellow-100">
|
||||
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" />
|
||||
<div
|
||||
className={`mb-2 flex items-start rounded-md border px-3 py-2 ${
|
||||
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||
? "border-red-600 bg-red-900/20"
|
||||
: "border-yellow-600 bg-yellow-900/20"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex text-sm ${
|
||||
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||
? "text-red-100"
|
||||
: "text-yellow-100"
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faWarning}
|
||||
className={`mt-1 mr-2 ${
|
||||
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||
? "text-red-600"
|
||||
: "text-yellow-600"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<p>
|
||||
Another secret sync in your organization is already configured with the same
|
||||
destination. This may lead to conflicts or unexpected behavior.
|
||||
{currentOrg?.blockDuplicateSecretSyncDestinations
|
||||
? "Another secret sync in your organization is already configured with the same destination. Your organization does not allow duplicate destination configurations."
|
||||
: "Another secret sync in your organization is already configured with the same destination. This may lead to conflicts or unexpected behavior."}
|
||||
</p>
|
||||
{duplicateProjectId && (
|
||||
<p className="mt-1 text-xs text-yellow-200">
|
||||
<p
|
||||
className={`mt-1 text-xs ${
|
||||
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||
? "text-red-200"
|
||||
: "text-yellow-200"
|
||||
}`}
|
||||
>
|
||||
Duplicate found in project ID:{" "}
|
||||
<code className="rounded-sm bg-yellow-800/50 px-1 py-0.5">
|
||||
<code
|
||||
className={`rounded-sm px-1 py-0.5 ${
|
||||
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||
? "bg-red-800/50"
|
||||
: "bg-yellow-800/50"
|
||||
}`}
|
||||
>
|
||||
{duplicateProjectId}
|
||||
</code>
|
||||
</p>
|
||||
@@ -224,7 +257,7 @@ export const SecretSyncReviewFields = () => {
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Auto-Sync">
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "neutral"}>
|
||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
@@ -235,7 +268,7 @@ export const SecretSyncReviewFields = () => {
|
||||
{AdditionalSyncOptionsFieldsComponent}
|
||||
{disableSecretDeletion && (
|
||||
<GenericFieldLabel label="Secret Deletion">
|
||||
<Badge variant="primary">Disabled</Badge>
|
||||
<Badge variant="warning">Disabled</Badge>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { forwardRef } from "react";
|
||||
import { cva, VariantProps } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
const badgeVariants = cva(
|
||||
[
|
||||
"inline-block cursor-default rounded-md bg-yellow/20 px-1.5 py-0.5 text-xs text-yellow opacity-80 hover:opacity-100"
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-yellow/20 text-yellow",
|
||||
danger: "bg-red/20 text-red",
|
||||
success: "bg-green/20 text-green",
|
||||
org: "bg-org-v1/20 text-org-v1 [&_svg]:text-org-v1 flex items-center opacity-100 hover:bg-org-v1/10 [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
|
||||
namespace:
|
||||
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1.5 w-min whitespace-nowrap",
|
||||
project:
|
||||
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 w-min gap-x-1.5 whitespace-nowrap",
|
||||
instance:
|
||||
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1.5 w-min whitespace-nowrap"
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
|
||||
|
||||
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
||||
({ children, className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export { Badge } from "./Badge";
|
||||
@@ -1,11 +1,16 @@
|
||||
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCube, faCubes, faGlobe, faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createElement } from "react";
|
||||
import { ReactNode } from "@tanstack/react-router";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import {
|
||||
Badge,
|
||||
InstanceIcon,
|
||||
OrgIcon,
|
||||
ProjectIcon,
|
||||
SubOrgIcon,
|
||||
TBadgeProps
|
||||
} from "@app/components/v3";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
type Props = {
|
||||
@@ -16,19 +21,19 @@ type Props = {
|
||||
scope: "org" | "namespace" | "instance" | ProjectType | null;
|
||||
};
|
||||
|
||||
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = {
|
||||
org: { label: "Organization", icon: faGlobe },
|
||||
[ProjectType.SecretManager]: { label: "Project", icon: faCube },
|
||||
[ProjectType.CertificateManager]: { label: "Project", icon: faCube },
|
||||
[ProjectType.SSH]: { label: "Project", icon: faCube },
|
||||
[ProjectType.KMS]: { label: "Project", icon: faCube },
|
||||
[ProjectType.PAM]: { label: "Project", icon: faCube },
|
||||
[ProjectType.SecretScanning]: { label: "Project", icon: faCube },
|
||||
namespace: { label: "Sub-Organization", icon: faCubes },
|
||||
instance: { label: "Server", icon: faServer }
|
||||
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
|
||||
org: { label: "Organization", icon: OrgIcon },
|
||||
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
|
||||
namespace: { label: "Sub-Organization", icon: SubOrgIcon },
|
||||
instance: { label: "Server", icon: InstanceIcon }
|
||||
};
|
||||
|
||||
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]> = {
|
||||
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, TBadgeProps["variant"]> = {
|
||||
org: "org",
|
||||
[ProjectType.SecretManager]: "project",
|
||||
[ProjectType.CertificateManager]: "project",
|
||||
@@ -36,8 +41,8 @@ const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]>
|
||||
[ProjectType.KMS]: "project",
|
||||
[ProjectType.PAM]: "project",
|
||||
[ProjectType.SecretScanning]: "project",
|
||||
namespace: "namespace",
|
||||
instance: "instance"
|
||||
namespace: "sub-org",
|
||||
instance: "neutral"
|
||||
};
|
||||
|
||||
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
||||
@@ -47,7 +52,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P
|
||||
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
|
||||
{scope && (
|
||||
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
|
||||
<FontAwesomeIcon icon={SCOPE_NAME[scope].icon} />
|
||||
{createElement(SCOPE_NAME[scope].icon)}
|
||||
{SCOPE_NAME[scope].label}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faCaretDown, faCaretUp, faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Spinner } from "../Spinner";
|
||||
@@ -19,6 +20,7 @@ type Props = {
|
||||
icon?: IconProp;
|
||||
isMulti?: boolean;
|
||||
iconClassName?: string;
|
||||
LucideIcon?: LucideIcon;
|
||||
dropdownContainerStyle?: React.CSSProperties;
|
||||
side?: SelectPrimitive.SelectContentProps["side"];
|
||||
};
|
||||
@@ -39,6 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
iconClassName,
|
||||
dropdownContainerStyle,
|
||||
side,
|
||||
LucideIcon: Lucide,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -65,6 +68,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
>
|
||||
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
|
||||
{Lucide && <Lucide className={twMerge("size-3.5", iconClassName)} />}
|
||||
<div className="flex-1 truncate">
|
||||
<SelectPrimitive.Value placeholder={placeholder} />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
export * from "./AccessRestrictedBanner";
|
||||
export * from "./Accordion";
|
||||
export * from "./Alert";
|
||||
export * from "./Badge";
|
||||
export * from "./Breadcrumb";
|
||||
export * from "./Button";
|
||||
export * from "./Card";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
AsteriskIcon,
|
||||
BanIcon,
|
||||
BoxesIcon,
|
||||
BoxIcon,
|
||||
CheckIcon,
|
||||
ChevronsUpDownIcon,
|
||||
CircleXIcon,
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
GlobeIcon,
|
||||
InfoIcon,
|
||||
RadarIcon,
|
||||
TriangleAlertIcon
|
||||
TriangleAlertIcon,
|
||||
UserIcon
|
||||
} from "lucide-react";
|
||||
|
||||
import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
/**
|
||||
@@ -69,7 +71,29 @@ export const Neutral: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use this variant when indicating neutral or disabled states."
|
||||
story:
|
||||
"Use this variant when indicating neutral or disabled states or when linking to external documents."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
name: "Variant: Ghost",
|
||||
args: {
|
||||
variant: "ghost",
|
||||
children: (
|
||||
<>
|
||||
<UserIcon />
|
||||
User
|
||||
</>
|
||||
)
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use this variant when indicating a configuration or property value. Avoid using this variant as an interactive element as it is not intuitive to interact with."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,8 +133,7 @@ export const Info: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use this variant when indicating informational states or linking to external references."
|
||||
story: "Use this variant when indicating informational states."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +185,7 @@ export const Organization: Story = {
|
||||
variant: "org",
|
||||
children: (
|
||||
<>
|
||||
<GlobeIcon />
|
||||
<OrgIcon />
|
||||
Organization
|
||||
</>
|
||||
)
|
||||
@@ -182,7 +205,7 @@ export const SubOrganization: Story = {
|
||||
variant: "sub-org",
|
||||
children: (
|
||||
<>
|
||||
<BoxesIcon />
|
||||
<SubOrgIcon />
|
||||
Sub-Organization
|
||||
</>
|
||||
)
|
||||
@@ -202,7 +225,7 @@ export const Project: Story = {
|
||||
variant: "project",
|
||||
children: (
|
||||
<>
|
||||
<BoxIcon />
|
||||
<ProjectIcon />
|
||||
Project
|
||||
</>
|
||||
)
|
||||
@@ -283,10 +306,11 @@ export const AsButton: Story = {
|
||||
export const IsTruncatable: Story = {
|
||||
name: "Example: isTruncatable",
|
||||
args: {
|
||||
variant: "org",
|
||||
isTruncatable: true,
|
||||
children: (
|
||||
<>
|
||||
<GlobeIcon />
|
||||
<OrgIcon />
|
||||
<span>Infisical Infrastructure</span>
|
||||
</>
|
||||
)
|
||||
@@ -295,7 +319,7 @@ export const IsTruncatable: Story = {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation."
|
||||
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation. Parent `div` should have a fixed width and `flex` class."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -305,3 +329,48 @@ export const IsTruncatable: Story = {
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const IsSquare: Story = {
|
||||
name: "Example: isSquare",
|
||||
args: {
|
||||
variant: "danger",
|
||||
isSquare: true,
|
||||
children: <AlertTriangleIcon />
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use the `isSquare` prop when displaying a squared badge with 1-2 character text or only an icon."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const IsFullWidth: Story = {
|
||||
name: "Example: isFullWidth",
|
||||
args: {
|
||||
variant: "neutral",
|
||||
isFullWidth: true,
|
||||
|
||||
children: (
|
||||
<>
|
||||
<AsteriskIcon />
|
||||
Secret Value
|
||||
</>
|
||||
)
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use the `isFullWidth` prop to expand the badges width to fill it's parent container."
|
||||
}
|
||||
}
|
||||
},
|
||||
decorators: (Story) => (
|
||||
<div className="w-32">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -6,30 +6,37 @@ import { cn } from "@app/components/v3/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
[
|
||||
"select-none items-center rounded-sm px-1.5 py-0.5 text-xs",
|
||||
"gap-x-1 [a&,button&]:cursor-pointer inline-flex",
|
||||
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3",
|
||||
"select-none items-center align-middle rounded-sm h-4.5 px-1.5 text-xs",
|
||||
"gap-x-1 [a&,button&]:cursor-pointer inline-flex font-normal",
|
||||
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&_svg:not([class*='size-'])]:size-3",
|
||||
"transition duration-200 ease-in-out"
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
isTruncatable: {
|
||||
true: "[&>span,&>p]:truncate min-w-0",
|
||||
false: "w-fit shrink-0 whitespace-nowrap overflow-hidden"
|
||||
false: "w-fit shrink-0 min-w-fit whitespace-nowrap overflow-hidden"
|
||||
},
|
||||
isFullWidth: {
|
||||
true: "w-full justify-center"
|
||||
},
|
||||
isSquare: {
|
||||
true: "w-4.5 justify-center px-0.5"
|
||||
},
|
||||
variant: {
|
||||
neutral: "bg-neutral/30 text-neutral [a&,button&]:hover:bg-neutral/40",
|
||||
success: "bg-success/30 text-success [a&,button&]:hover:bg-success/40",
|
||||
info: "bg-info/30 text-info [a&,button&]:hover:bg-info/40",
|
||||
warning: "bg-warning/30 text-warning [a&,button&]:hover:bg-warning/40",
|
||||
danger: "bg-danger/30 text-danger [a&,button&]:hover:bg-danger/40",
|
||||
project: "bg-project/30 text-project [a&,button&]:hover:bg-project/40",
|
||||
org: "bg-org/30 text-org [a&,button&]:hover:bg-org/40",
|
||||
"sub-org": "bg-sub-org/30 text-sub-org [a&,button&]:hover:bg-sub-org/40"
|
||||
ghost: "text-mineshaft-200 gap-x-2",
|
||||
neutral: "bg-neutral/25 text-neutral [a&,button&]:hover:bg-neutral/35",
|
||||
success: "bg-success/25 text-success [a&,button&]:hover:bg-success/35",
|
||||
info: "bg-info/25 text-info [a&,button&]:hover:bg-info/35",
|
||||
warning: "bg-warning/25 text-warning [a&,button&]:hover:bg-warning/35",
|
||||
danger: "bg-danger/25 text-danger [a&,button&]:hover:bg-danger/35",
|
||||
project: "bg-project/25 text-project [a&,button&]:hover:bg-project/35",
|
||||
org: "bg-org/25 text-org [a&,button&]:hover:bg-org/35",
|
||||
"sub-org": "bg-sub-org/25 text-sub-org [a&,button&]:hover:bg-sub-org/35"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "success"
|
||||
variant: "neutral"
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -37,16 +44,28 @@ const badgeVariants = cva(
|
||||
type TBadgeProps = VariantProps<typeof badgeVariants> &
|
||||
React.ComponentProps<"span"> & {
|
||||
asChild?: boolean;
|
||||
variant: NonNullable<VariantProps<typeof badgeVariants>["variant"]>; // TODO: REMOVE
|
||||
};
|
||||
|
||||
const Badge = forwardRef<HTMLSpanElement, TBadgeProps>(
|
||||
({ className, variant, asChild = false, isTruncatable = false, ...props }, ref): JSX.Element => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
isTruncatable = false,
|
||||
isFullWidth = false,
|
||||
isSquare = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
): JSX.Element => {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant, isTruncatable }), className)}
|
||||
className={cn(badgeVariants({ variant, isTruncatable, isFullWidth, isSquare }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./generic";
|
||||
export * from "./platform";
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
|
||||
import { DocumentationLinkBadge } from "./DocumentationLinkBadge";
|
||||
|
||||
/**
|
||||
* The documentation link badge is a re-usable component to link to Infisical documentation references.
|
||||
*/
|
||||
const meta = {
|
||||
title: "Platform/Documentation Link Badge",
|
||||
component: DocumentationLinkBadge,
|
||||
parameters: {
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
args: { href: "https://infisical.com/docs/documentation/getting-started/introduction" }
|
||||
} satisfies Meta<typeof DocumentationLinkBadge>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
name: "Example: Default"
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { BookOpenIcon } from "lucide-react";
|
||||
|
||||
import { Badge } from "@app/components/v3";
|
||||
|
||||
type TDocumentationLinkBadgeProps = {
|
||||
href: string;
|
||||
};
|
||||
|
||||
export function DocumentationLinkBadge({ href }: TDocumentationLinkBadgeProps) {
|
||||
return (
|
||||
<Badge variant="neutral" asChild>
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
<BookOpenIcon />
|
||||
Documentation
|
||||
</a>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./DocumentationLinkBadge";
|
||||
@@ -0,0 +1,8 @@
|
||||
import { BoxesIcon, BoxIcon, Building2Icon, ServerIcon } from "lucide-react";
|
||||
|
||||
const InstanceIcon = ServerIcon;
|
||||
const OrgIcon = Building2Icon;
|
||||
const SubOrgIcon = BoxesIcon;
|
||||
const ProjectIcon = BoxIcon;
|
||||
|
||||
export { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./DocumentationLinkBadge";
|
||||
export * from "./ScopeIcons";
|
||||
@@ -301,10 +301,6 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/projects/cert-management/$projectId/subscribers",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
||||
),
|
||||
CertificatesPage: setRoute(
|
||||
"/projects/cert-management/$projectId/certificates",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
|
||||
),
|
||||
CertificateAuthoritiesPage: setRoute(
|
||||
"/projects/cert-management/$projectId/certificate-authorities",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { LucideIcon, UserCheckIcon, UserPenIcon } from "lucide-react";
|
||||
|
||||
import { TBadgeProps } from "@app/components/v3";
|
||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||
|
||||
export const policyDetails: Record<
|
||||
PolicyType,
|
||||
{ name: string; className: string; icon: IconDefinition }
|
||||
{ name: string; variant: TBadgeProps["variant"]; Icon: LucideIcon }
|
||||
> = {
|
||||
[PolicyType.AccessPolicy]: {
|
||||
className: "bg-green/20 text-green",
|
||||
variant: "ghost",
|
||||
name: "Access Policy",
|
||||
icon: faArrowRightToBracket
|
||||
Icon: UserCheckIcon
|
||||
},
|
||||
[PolicyType.ChangePolicy]: {
|
||||
className: "bg-yellow/20 text-yellow",
|
||||
variant: "ghost",
|
||||
name: "Change Policy",
|
||||
icon: faEdit
|
||||
Icon: UserPenIcon
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {
|
||||
faBan,
|
||||
faCheck,
|
||||
faMagnifyingGlassMinus,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { AlertTriangleIcon, BanIcon, CheckIcon, LucideIcon, SearchSlashIcon } from "lucide-react";
|
||||
|
||||
import { TBadgeProps } from "@app/components/v3";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
@@ -74,12 +70,28 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
|
||||
}
|
||||
};
|
||||
|
||||
export const SECRET_SCANNING_FINDING_STATUS_ICON_MAP = {
|
||||
[SecretScanningFindingStatus.Resolved]: { icon: faCheck, className: "text-green" },
|
||||
[SecretScanningFindingStatus.Unresolved]: { icon: faWarning, className: "text-yellow" },
|
||||
[SecretScanningFindingStatus.Ignore]: { icon: faBan, className: "text-mineshaft-400" },
|
||||
export const SECRET_SCANNING_FINDING_STATUS_MAP: Record<
|
||||
SecretScanningFindingStatus,
|
||||
{ Icon: LucideIcon; variant: TBadgeProps["variant"]; className: string }
|
||||
> = {
|
||||
[SecretScanningFindingStatus.Resolved]: {
|
||||
Icon: CheckIcon,
|
||||
variant: "success",
|
||||
className: "text-success"
|
||||
},
|
||||
[SecretScanningFindingStatus.Unresolved]: {
|
||||
Icon: AlertTriangleIcon,
|
||||
variant: "warning",
|
||||
className: "text-warning"
|
||||
},
|
||||
[SecretScanningFindingStatus.Ignore]: {
|
||||
Icon: BanIcon,
|
||||
variant: "neutral",
|
||||
className: "text-neutral"
|
||||
},
|
||||
[SecretScanningFindingStatus.FalsePositive]: {
|
||||
icon: faMagnifyingGlassMinus,
|
||||
className: "text-mineshaft-400"
|
||||
Icon: SearchSlashIcon,
|
||||
variant: "neutral",
|
||||
className: "text-neutral"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,5 +10,6 @@ export type TGitLabGroup = {
|
||||
|
||||
export enum GitLabAccessTokenType {
|
||||
Personal = "personal",
|
||||
Project = "project"
|
||||
Project = "project",
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,6 @@ export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCert
|
||||
case CaStatus.DISABLED:
|
||||
return "danger";
|
||||
default:
|
||||
return "primary";
|
||||
return "warning";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
ExternalMigrationProviders,
|
||||
TVaultExternalMigrationConfig,
|
||||
VaultKubernetesAuthRole
|
||||
VaultKubernetesAuthRole,
|
||||
VaultKubernetesRole
|
||||
} from "./types";
|
||||
|
||||
export const externalMigrationQueryKeys = {
|
||||
@@ -31,6 +32,11 @@ export const externalMigrationQueryKeys = {
|
||||
"vault-kubernetes-auth-roles",
|
||||
namespace,
|
||||
mountPath
|
||||
],
|
||||
vaultKubernetesRoles: (namespace?: string, mountPath?: string) => [
|
||||
"vault-kubernetes-roles",
|
||||
namespace,
|
||||
mountPath
|
||||
]
|
||||
};
|
||||
|
||||
@@ -172,3 +178,30 @@ export const useGetVaultKubernetesAuthRoles = (
|
||||
enabled: enabled && !!namespace && !!mountPath
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetVaultKubernetesRoles = (
|
||||
enabled = true,
|
||||
namespace?: string,
|
||||
mountPath?: string
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: externalMigrationQueryKeys.vaultKubernetesRoles(namespace, mountPath),
|
||||
queryFn: async () => {
|
||||
if (!namespace || !mountPath) {
|
||||
throw new Error("Both namespace and mountPath are required");
|
||||
}
|
||||
|
||||
const { data } = await apiRequest.get<{
|
||||
roles: VaultKubernetesRole[];
|
||||
}>("/api/v3/external-migration/vault/kubernetes-roles", {
|
||||
params: {
|
||||
namespace,
|
||||
mountPath
|
||||
}
|
||||
});
|
||||
|
||||
return data.roles;
|
||||
},
|
||||
enabled: enabled && !!namespace && !!mountPath
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,3 +49,24 @@ export type VaultKubernetesAuthRole = {
|
||||
disable_local_ca_jwt?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type VaultKubernetesRole = {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
config: {
|
||||
kubernetes_host: string;
|
||||
kubernetes_ca_cert?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -125,7 +125,8 @@ export const useUpdateOrg = () => {
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
@@ -146,7 +147,8 @@ export const useUpdateOrg = () => {
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ export type Organization = {
|
||||
shareSecretsProductEnabled: boolean;
|
||||
maxSharedSecretLifetime: number;
|
||||
maxSharedSecretViewLimit: number | null;
|
||||
blockDuplicateSecretSyncDestinations: boolean;
|
||||
};
|
||||
|
||||
export type UpdateOrgDTO = {
|
||||
@@ -52,6 +53,7 @@ export type UpdateOrgDTO = {
|
||||
shareSecretsProductEnabled?: boolean;
|
||||
maxSharedSecretViewLimit?: number | null;
|
||||
maxSharedSecretLifetime?: number;
|
||||
blockDuplicateSecretSyncDestinations?: boolean;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
|
||||
@@ -15,6 +15,6 @@ export const getPkiSubscriberStatusBadgeVariant = (status: PkiSubscriberStatus)
|
||||
case PkiSubscriberStatus.DISABLED:
|
||||
return "danger";
|
||||
default:
|
||||
return "primary";
|
||||
return "warning";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,15 +7,12 @@ import {
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faChevronRight,
|
||||
faCubes,
|
||||
faEnvelope,
|
||||
faExclamationTriangle,
|
||||
faGlobe,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faPlus,
|
||||
faServer,
|
||||
faSignOut,
|
||||
faToolbox,
|
||||
faUser,
|
||||
@@ -30,7 +27,6 @@ import { Mfa } from "@app/components/auth/Mfa";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
Badge,
|
||||
BreadcrumbContainer,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -46,6 +42,7 @@ import {
|
||||
TBreadcrumbFormat,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
@@ -271,9 +268,7 @@ export const Navbar = () => {
|
||||
to="/admin"
|
||||
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
|
||||
</div>
|
||||
<InstanceIcon className="size-3.5 text-xs text-bunker-300" />
|
||||
<div className="whitespace-nowrap">Server Console</div>
|
||||
</Link>
|
||||
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
|
||||
@@ -288,6 +283,18 @@ export const Navbar = () => {
|
||||
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
|
||||
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
|
||||
<Badge
|
||||
asChild
|
||||
variant="org"
|
||||
isTruncatable
|
||||
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
||||
className={twMerge(
|
||||
"gap-x-1.5 text-sm",
|
||||
(!isOrgScope || isSubOrganization) &&
|
||||
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organization/projects",
|
||||
@@ -297,15 +304,10 @@ export const Navbar = () => {
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}
|
||||
}}
|
||||
variant="org"
|
||||
className={twMerge(
|
||||
"max-w-full min-w-0 cursor-pointer text-sm",
|
||||
(!isOrgScope || isSubOrganization) &&
|
||||
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGlobe} />
|
||||
<p className="truncate">{currentOrg?.name}</p>
|
||||
<OrgIcon className="size-[12px]" />
|
||||
<span>{currentOrg?.name}</span>
|
||||
</button>
|
||||
</Badge>
|
||||
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
|
||||
{getPlan(subscription)}
|
||||
@@ -443,19 +445,22 @@ export const Navbar = () => {
|
||||
<>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
<DropdownMenu modal={false}>
|
||||
<Link className="overflow-hidden" to="/organization/projects">
|
||||
<Badge
|
||||
variant="namespace"
|
||||
asChild
|
||||
isTruncatable
|
||||
variant="sub-org"
|
||||
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
||||
className={twMerge(
|
||||
"flex max-w-full min-w-0 cursor-pointer text-sm",
|
||||
"gap-x-1.5 text-sm",
|
||||
!isOrgScope &&
|
||||
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
|
||||
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCubes} />
|
||||
<p className="truncate">{currentOrg.subOrganization.name}</p>
|
||||
</Badge>
|
||||
<Link to="/organization/projects">
|
||||
<SubOrgIcon className="size-[12px]" />
|
||||
<span>{currentOrg.subOrganization.name}</span>
|
||||
</Link>
|
||||
</Badge>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
@@ -549,10 +554,10 @@ export const Navbar = () => {
|
||||
)}
|
||||
{user.superAdmin && !location.pathname.startsWith("/admin") && (
|
||||
<Link
|
||||
className="mr-2 rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
className="mr-2 flex items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
to="/admin"
|
||||
>
|
||||
<FontAwesomeIcon icon={faServer} className="mr-2" />
|
||||
<InstanceIcon className="mr-2 inline-block size-3.5" />
|
||||
Server Console
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,10 @@ export const PamLayout = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.pam) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can use PAM if you switch to Infisical's Enterprise plan.",
|
||||
isEnterpriseFeature: true
|
||||
});
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
@@ -111,7 +114,8 @@ export const PamLayout = () => {
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use PAM if you switch to a paid Infisical plan."
|
||||
text={popUp.upgradePlan.data?.description}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faCube,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faStar as faSolidStar
|
||||
@@ -16,7 +15,6 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { NewProjectModal } from "@app/components/projects";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -25,6 +23,7 @@ import {
|
||||
Input,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge, ProjectIcon } from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -120,11 +119,9 @@ export const ProjectSelect = () => {
|
||||
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
|
||||
{currentWorkspace?.name}
|
||||
</p>
|
||||
<Badge variant="project" className="cursor-pointer">
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
<span>
|
||||
<Badge variant="project">
|
||||
<ProjectIcon />
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
</span>
|
||||
</Badge>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject, useProjectPermission } from "@app/context";
|
||||
import {
|
||||
useGetAccessRequestsCount,
|
||||
@@ -88,7 +89,7 @@ export const SecretManagerLayout = () => {
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="primary" className="ml-1.5">
|
||||
<Badge variant="warning" isSquare className="ml-1.5">
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionSub,
|
||||
useProject,
|
||||
@@ -66,7 +67,7 @@ export const SecretScanningLayout = () => {
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Findings
|
||||
{Boolean(unresolvedFindings) && (
|
||||
<Badge variant="primary" className="ml-2 h-min">
|
||||
<Badge isSquare variant="warning" className="ml-2">
|
||||
{unresolvedFindings}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -7,16 +7,15 @@ import {
|
||||
faTrash,
|
||||
faUsers,
|
||||
faUserXmark,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useSubscription, useUser } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
@@ -451,7 +451,7 @@ export const ServerAdminsTable = () => {
|
||||
const email = user.email ?? user.username;
|
||||
return (
|
||||
<li key={user.id}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<p>
|
||||
{user.firstName || user.lastName ? (
|
||||
<>
|
||||
@@ -464,15 +464,10 @@ export const ServerAdminsTable = () => {
|
||||
</p>
|
||||
{userId === user.id && (
|
||||
<Tooltip content="Are you sure you want to remove yourself from this instance?">
|
||||
<div className="inline-block">
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} />
|
||||
<span>Deleting Yourself</span>
|
||||
<Badge variant="danger">
|
||||
<AlertTriangleIcon />
|
||||
Deleting Yourself
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useInvalidateCache } from "@app/hooks/api";
|
||||
@@ -62,11 +62,8 @@ export const CachingPageForm = () => {
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span>
|
||||
{isInvalidating && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
|
||||
<Badge variant="danger">
|
||||
<RefreshCwIcon className="animate-spin" />
|
||||
Invalidating Cache
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user