Compare commits

..

1 Commits

Author SHA1 Message Date
65b6f61b53 dedicated-instance-deploy 2025-02-25 16:21:20 +09:00
39 changed files with 5216 additions and 933 deletions

3026
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -117,6 +117,7 @@
"vitest": "^1.2.2"
},
"dependencies": {
"@aws-sdk/client-cloudformation": "^3.750.0",
"@aws-sdk/client-elasticache": "^3.637.0",
"@aws-sdk/client-iam": "^3.525.0",
"@aws-sdk/client-kms": "^3.609.0",
@ -139,9 +140,9 @@
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/auth-app": "^7.1.5",
"@octokit/plugin-retry": "^7.1.4",
"@octokit/rest": "^21.1.1",
"@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1",
"@opentelemetry/api": "^1.9.0",
@ -161,6 +162,7 @@
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-cdk-lib": "^2.180.0",
"aws-sdk": "^2.1553.0",
"axios": "^1.6.7",
"axios-retry": "^4.0.0",

View File

@ -19,6 +19,7 @@ import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/ser
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { TDedicatedInstanceServiceFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
@ -228,6 +229,7 @@ declare module "fastify" {
secretSync: TSecretSyncServiceFactory;
kmip: TKmipServiceFactory;
kmipOperation: TKmipOperationServiceFactory;
dedicatedInstance: TDedicatedInstanceServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -930,5 +930,10 @@ declare module "knex/types/tables" {
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
[TableName.DedicatedInstances]: KnexOriginal.CompositeTableType<
TDedicatedInstances,
TDedicatedInstancesInsert,
TDedicatedInstancesUpdate
>;
}
}

View File

@ -0,0 +1,56 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.DedicatedInstances);
if (!isTablePresent) {
await knex.schema.createTable(TableName.DedicatedInstances, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.string("instanceName").notNullable();
t.string("subdomain").notNullable().unique();
t.enum("status", ["RUNNING", "UPGRADING", "PROVISIONING", "FAILED"]).notNullable();
t.string("rdsInstanceType").notNullable();
t.string("elasticCacheType").notNullable();
t.integer("elasticContainerMemory").notNullable();
t.integer("elasticContainerCpu").notNullable();
t.string("region").notNullable();
t.string("version").notNullable();
t.integer("backupRetentionDays").defaultTo(7);
t.timestamp("lastBackupTime").nullable();
t.timestamp("lastUpgradeTime").nullable();
t.boolean("publiclyAccessible").defaultTo(false);
t.string("vpcId").nullable();
t.specificType("subnetIds", "text[]").nullable();
t.jsonb("tags").nullable();
t.boolean("multiAz").defaultTo(true);
t.integer("rdsAllocatedStorage").defaultTo(50);
t.integer("rdsBackupRetentionDays").defaultTo(7);
t.integer("redisNumCacheNodes").defaultTo(1);
t.integer("desiredContainerCount").defaultTo(1);
t.string("stackName").nullable();
t.text("rdsInstanceId").nullable();
t.text("redisClusterId").nullable();
t.text("ecsClusterArn").nullable();
t.text("ecsServiceArn").nullable();
t.specificType("securityGroupIds", "text[]").nullable();
t.text("error").nullable();
t.timestamps(true, true, true);
t.foreign("orgId")
.references("id")
.inTable(TableName.Organization)
.onDelete("CASCADE");
t.unique(["orgId", "instanceName"]);
});
}
await createOnUpdateTrigger(knex, TableName.DedicatedInstances);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.DedicatedInstances);
await knex.schema.dropTableIfExists(TableName.DedicatedInstances);
}

View File

@ -0,0 +1,16 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// First drop the existing constraint
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} DROP CONSTRAINT IF EXISTS dedicated_instances_status_check`);
// Add the new constraint with updated enum values
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} ADD CONSTRAINT dedicated_instances_status_check CHECK (status IN ('RUNNING', 'UPGRADING', 'PROVISIONING', 'FAILED'))`);
}
export async function down(knex: Knex): Promise<void> {
// Revert back to original constraint
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} DROP CONSTRAINT IF EXISTS dedicated_instances_status_check`);
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} ADD CONSTRAINT dedicated_instances_status_check CHECK (status IN ('RUNNING', 'UPGRADING', 'PROVISIONING'))`);
}

View File

@ -0,0 +1,34 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const DedicatedInstancesSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
instanceName: z.string(),
status: z.string(),
rdsInstanceType: z.string(),
elasticCacheType: z.string(),
elasticContainerMemory: z.number(),
elasticContainerCpu: z.number(),
region: z.string(),
version: z.string(),
backupRetentionDays: z.number().default(7).nullable().optional(),
lastBackupTime: z.date().nullable().optional(),
lastUpgradeTime: z.date().nullable().optional(),
publiclyAccessible: z.boolean().default(false).nullable().optional(),
vpcId: z.string().nullable().optional(),
subnetIds: z.string().array().nullable().optional(),
tags: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TDedicatedInstances = z.infer<typeof DedicatedInstancesSchema>;
export type TDedicatedInstancesInsert = Omit<z.input<typeof DedicatedInstancesSchema>, TImmutableDBKeys>;
export type TDedicatedInstancesUpdate = Partial<Omit<z.input<typeof DedicatedInstancesSchema>, TImmutableDBKeys>>;

View File

@ -2,6 +2,7 @@ import { z } from "zod";
export enum TableName {
Users = "users",
Organization = "organizations",
SshCertificateAuthority = "ssh_certificate_authorities",
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
SshCertificateTemplate = "ssh_certificate_templates",
@ -29,7 +30,6 @@ export enum TableName {
AuthTokens = "auth_tokens",
AuthTokenSession = "auth_token_sessions",
BackupPrivateKey = "backup_private_key",
Organization = "organizations",
OrgMembership = "org_memberships",
OrgRoles = "org_roles",
OrgBot = "org_bots",
@ -136,7 +136,8 @@ export enum TableName {
KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
KmipClientCertificates = "kmip_client_certificates",
DedicatedInstances = "dedicated_instances"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,141 @@
import { z } from "zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const DedicatedInstanceSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
instanceName: z.string().min(1),
subdomain: z.string().min(1),
status: z.enum(["RUNNING", "UPGRADING", "PROVISIONING", "FAILED"]),
rdsInstanceType: z.string(),
elasticCacheType: z.string(),
elasticContainerMemory: z.number(),
elasticContainerCpu: z.number(),
region: z.string(),
version: z.string(),
backupRetentionDays: z.number(),
lastBackupTime: z.date().nullable(),
lastUpgradeTime: z.date().nullable(),
publiclyAccessible: z.boolean(),
vpcId: z.string().nullable(),
subnetIds: z.array(z.string()).nullable(),
tags: z.record(z.string()).nullable(),
createdAt: z.date(),
updatedAt: z.date()
});
const CreateDedicatedInstanceSchema = z.object({
instanceName: z.string().min(1),
subdomain: z.string().min(1),
provider: z.literal('aws'), // Only allow 'aws' as provider
region: z.string(),
publiclyAccessible: z.boolean().default(false)
});
const DedicatedInstanceDetailsSchema = DedicatedInstanceSchema.extend({
stackStatus: z.string().optional(),
stackStatusReason: z.string().optional(),
error: z.string().nullable(),
events: z.array(
z.object({
timestamp: z.date().optional(),
logicalResourceId: z.string().optional(),
resourceType: z.string().optional(),
resourceStatus: z.string().optional(),
resourceStatusReason: z.string().optional()
})
).optional()
});
export const registerDedicatedInstanceRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/dedicated-instances",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid()
}),
response: {
200: z.object({
instances: DedicatedInstanceSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const instances = await server.services.dedicatedInstance.listInstances({
orgId: req.params.organizationId
});
return { instances };
}
});
server.route({
method: "POST",
url: "/:organizationId/dedicated-instances",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid()
}),
body: CreateDedicatedInstanceSchema
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { organizationId } = req.params;
const { instanceName, subdomain, region, publiclyAccessible, provider} = req.body;
const instance = await server.services.dedicatedInstance.createInstance({
orgId: organizationId,
instanceName,
subdomain,
region,
publiclyAccessible,
provider: provider,
dryRun: false,
});
return instance;
}
});
server.route({
method: "GET",
url: "/:organizationId/dedicated-instances/:instanceId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid(),
instanceId: z.string().uuid()
}),
response: {
200: DedicatedInstanceDetailsSchema
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { organizationId, instanceId } = req.params;
const { instance, stackStatus, stackStatusReason, events } = await server.services.dedicatedInstance.getInstance({
orgId: organizationId,
instanceId
});
return {
...instance,
stackStatus,
stackStatusReason,
events
};
}
});
};

View File

@ -4,6 +4,7 @@ import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-rou
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDedicatedInstanceRouter } from "./dedicated-instance-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
@ -38,6 +39,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerLicenseRouter, { prefix: "/organizations" });
await server.register(registerDedicatedInstanceRouter, { prefix: "/organizations" });
await server.register(
async (projectRouter) => {
await projectRouter.register(registerProjectRoleRouter);

View File

@ -0,0 +1,28 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TDedicatedInstanceDALFactory = ReturnType<typeof dedicatedInstanceDALFactory>;
export const dedicatedInstanceDALFactory = (db: TDbClient) => {
const dedicatedInstanceOrm = ormify(db, TableName.DedicatedInstances);
const findInstancesByOrgId = async (orgId: string, tx?: Knex) => {
try {
const instances = await (tx || db.replicaNode())(TableName.DedicatedInstances)
.where({ orgId })
.select("*");
return instances;
} catch (error) {
throw new DatabaseError({ error, name: "Find instances by org ID" });
}
};
return {
...dedicatedInstanceOrm,
findInstancesByOrgId
};
};

View File

@ -0,0 +1,470 @@
import { ForbiddenError } from "@casl/ability";
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
import { randomBytes } from 'crypto';
import { CloudFormationClient, CreateStackCommand, DescribeStacksCommand, DescribeStackEventsCommand } from "@aws-sdk/client-cloudformation";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TDedicatedInstanceDALFactory } from "./dedicated-instance-dal";
type TDedicatedInstanceServiceFactoryDep = {
dedicatedInstanceDAL: TDedicatedInstanceDALFactory;
permissionService: TPermissionServiceFactory;
};
interface CreateInstanceParams {
orgId: string;
instanceName: string;
subdomain: string;
region: string;
provider: 'aws';
publiclyAccessible: boolean;
clusterSize: 'small' | 'medium' | 'large';
dryRun?: boolean;
}
interface GetInstanceParams {
orgId: string;
instanceId: string;
}
interface StackResource {
resourceType: string;
resourceStatus: string;
resourceStatusReason?: string;
}
interface StackEvent {
timestamp?: Date;
logicalResourceId?: string;
resourceType?: string;
resourceStatus?: string;
resourceStatusReason?: string;
}
interface InstanceDetails {
instance: Awaited<ReturnType<TDedicatedInstanceDALFactory["findById"]>>;
stackStatus?: string;
stackStatusReason?: string;
resources?: StackResource[];
events?: StackEvent[];
}
export type TDedicatedInstanceServiceFactory = ReturnType<typeof dedicatedInstanceServiceFactory>;
const CLUSTER_SIZES = {
small: {
containerCpu: 1024, // 1 vCPU
containerMemory: 2048, // 2GB
rdsInstanceType: 'db.t3.small',
elasticCacheType: 'cache.t3.micro',
desiredContainerCount: 1,
displayName: '1 vCPU, 2GB RAM'
},
medium: {
containerCpu: 2048, // 2 vCPU
containerMemory: 4096, // 4GB
rdsInstanceType: 'db.t3.medium',
elasticCacheType: 'cache.t3.small',
desiredContainerCount: 2,
displayName: '2 vCPU, 4GB RAM'
},
large: {
containerCpu: 4096, // 4 vCPU
containerMemory: 8192, // 8GB
rdsInstanceType: 'db.t3.large',
elasticCacheType: 'cache.t3.medium',
desiredContainerCount: 4,
displayName: '4 vCPU, 8GB RAM'
}
};
export const dedicatedInstanceServiceFactory = ({
dedicatedInstanceDAL,
permissionService
}: TDedicatedInstanceServiceFactoryDep) => {
const listInstances = async ({
orgId
}: {
orgId: string;
}) => {
const instances = await dedicatedInstanceDAL.findInstancesByOrgId(orgId);
return instances;
};
const createInstance = async (params: CreateInstanceParams) => {
const { orgId, instanceName, subdomain, region, publiclyAccessible, dryRun = false, clusterSize = 'small' } = params;
if (params.provider !== 'aws') {
throw new BadRequestError({ message: 'Only AWS provider is supported' });
}
const clusterConfig = CLUSTER_SIZES[clusterSize];
// Configure AWS SDK with environment variables
const awsConfig = {
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
region: region,
};
if (!awsConfig.credentials.accessKeyId || !awsConfig.credentials.secretAccessKey) {
throw new Error('AWS credentials not found in environment variables');
}
const internalTags = {
'managed-by': 'infisical',
'organization-id': orgId,
'instance-name': instanceName
};
// Create the instance record with expanded configuration
const instance = await dedicatedInstanceDAL.create({
orgId,
instanceName,
subdomain,
status: "PROVISIONING",
region,
rdsInstanceType: clusterConfig.rdsInstanceType,
elasticCacheType: clusterConfig.elasticCacheType,
elasticContainerMemory: clusterConfig.containerMemory,
elasticContainerCpu: clusterConfig.containerCpu,
publiclyAccessible,
tags: internalTags,
version: "1.0.0",
multiAz: true,
rdsAllocatedStorage: 50,
rdsBackupRetentionDays: 7,
redisNumCacheNodes: 1,
desiredContainerCount: clusterConfig.desiredContainerCount,
subnetIds: [],
securityGroupIds: []
});
// Generate unique names for resources
const stackName = `infisical-dedicated-${instance.id}`;
const dbPassword = randomBytes(32).toString('hex');
// Create CDK app and stack
const app = new cdk.App();
const stack = new cdk.Stack(app, stackName, {
env: { region },
tags: internalTags,
synthesizer: new cdk.DefaultStackSynthesizer({
generateBootstrapVersionRule: false,
})
});
// Create VPC
const vpc = new ec2.Vpc(stack, `${orgId}-${instanceName}-vpc`, {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 24,
},
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidrMask: 24,
},
],
});
// Create RDS instance
const dbSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-db-sg`, {
vpc,
description: `Security group for ${instanceName} RDS instance`,
});
const db = new rds.DatabaseInstance(stack, `${orgId}-${instanceName}-db`, {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_14,
}),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
securityGroups: [dbSecurityGroup],
credentials: rds.Credentials.fromPassword(
'postgres',
cdk.SecretValue.unsafePlainText(dbPassword)
),
multiAz: true,
allocatedStorage: 50,
backupRetention: cdk.Duration.days(7),
});
// Create Redis cluster
const redisSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-redis-sg`, {
vpc,
description: `Security group for ${instanceName} Redis cluster`,
});
const redisSubnetGroup = new elasticache.CfnSubnetGroup(stack, `${orgId}-${instanceName}-redis-subnet`, {
subnetIds: vpc.privateSubnets.map((subnet: ec2.ISubnet) => subnet.subnetId),
description: `Subnet group for ${instanceName} Redis cluster`,
});
const redis = new elasticache.CfnCacheCluster(stack, `${orgId}-${instanceName}-redis`, {
engine: 'redis',
cacheNodeType: 'cache.t3.micro',
numCacheNodes: 1,
vpcSecurityGroupIds: [redisSecurityGroup.securityGroupId],
cacheSubnetGroupName: redisSubnetGroup.ref,
});
// Create ECS Fargate cluster and service
const cluster = new ecs.Cluster(stack, `${orgId}-${instanceName}-cluster`, { vpc });
// Create task execution role with permissions to read from Parameter Store
const executionRole = new iam.Role(stack, `${orgId}-${instanceName}-execution-role`, {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
executionRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')
);
// Create ECS task definition and service
const taskDefinition = new ecs.FargateTaskDefinition(stack, `${orgId}-${instanceName}-task-def`, {
memoryLimitMiB: clusterConfig.containerMemory,
cpu: clusterConfig.containerCpu,
executionRole,
});
taskDefinition.addContainer('infisical', {
image: ecs.ContainerImage.fromRegistry('infisical/infisical:latest-postgres'),
environment: {
NODE_ENV: 'production',
ENCRYPTION_KEY: randomBytes(16).toString('hex'),
AUTH_SECRET: randomBytes(32).toString('base64'),
DB_CONNECTION_URI: `postgresql://postgres:${dbPassword}@${db.instanceEndpoint.hostname}:5432/postgres?sslmode=no-verify`,
REDIS_URL: `redis://${redis.attrRedisEndpointAddress}:${redis.attrRedisEndpointPort}`,
},
logging: ecs.LogDrivers.awsLogs({ streamPrefix: stackName }),
});
const service = new ecs.FargateService(stack, `${orgId}-${instanceName}-service`, {
cluster,
taskDefinition,
desiredCount: clusterConfig.desiredContainerCount,
assignPublicIp: publiclyAccessible,
vpcSubnets: {
subnetType: publiclyAccessible ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
});
// Create security group for ECS service
const ecsSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-ecs-sg`, {
vpc,
description: `Security group for ${instanceName} ECS service`,
allowAllOutbound: true,
});
// Update service to use the security group
service.connections.addSecurityGroup(ecsSecurityGroup);
// Configure security group rules for RDS access
dbSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(5432),
'Allow ECS tasks to connect to PostgreSQL'
);
// Configure security group rules for Redis access
redisSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(6379),
'Allow ECS tasks to connect to Redis'
);
// Add outputs for resource IDs
new cdk.CfnOutput(stack, 'vpcid', { value: vpc.vpcId });
new cdk.CfnOutput(stack, 'publicsubnet1id', { value: vpc.publicSubnets[0].subnetId });
new cdk.CfnOutput(stack, 'publicsubnet2id', { value: vpc.publicSubnets[1].subnetId });
new cdk.CfnOutput(stack, 'privatesubnet1id', { value: vpc.privateSubnets[0].subnetId });
new cdk.CfnOutput(stack, 'privatesubnet2id', { value: vpc.privateSubnets[1].subnetId });
new cdk.CfnOutput(stack, 'rdsinstanceid', { value: db.instanceIdentifier });
new cdk.CfnOutput(stack, 'redisclusterid', { value: redis.ref });
new cdk.CfnOutput(stack, 'ecsclusterarn', { value: cluster.clusterArn });
new cdk.CfnOutput(stack, 'ecsservicearn', { value: service.serviceArn });
new cdk.CfnOutput(stack, 'dbsecuritygroupid', { value: dbSecurityGroup.securityGroupId });
new cdk.CfnOutput(stack, 'redissecuritygroupid', { value: redisSecurityGroup.securityGroupId });
// After VPC creation, store subnet IDs
const subnetIds = [
...vpc.publicSubnets.map(subnet => subnet.subnetId),
...vpc.privateSubnets.map(subnet => subnet.subnetId)
];
// After security group creation, store security group IDs
const securityGroupIds = [
dbSecurityGroup.securityGroupId,
redisSecurityGroup.securityGroupId
];
// Update instance with all infrastructure details
await dedicatedInstanceDAL.updateById(instance.id, {
stackName,
status: "PROVISIONING",
// Remove the token values and update them after stack creation
rdsInstanceId: null,
redisClusterId: null,
ecsClusterArn: null,
ecsServiceArn: null,
vpcId: null,
subnetIds: null,
securityGroupIds: null
});
// Deploy the stack
const deployment = app.synth();
if (dryRun) {
console.log('Dry run - would create stack with template:', JSON.stringify(deployment.getStackArtifact(stackName).template, null, 2));
return instance;
}
// Deploy the CloudFormation stack
try {
const cfnClient = new CloudFormationClient(awsConfig);
const command = new CreateStackCommand({
StackName: stackName,
TemplateBody: JSON.stringify(deployment.getStackArtifact(stackName).template),
Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
Tags: Object.entries(internalTags).map(([Key, Value]) => ({ Key, Value }))
});
await cfnClient.send(command);
} catch (error) {
await dedicatedInstanceDAL.updateById(instance.id, {
status: "FAILED",
error: error instanceof Error ? error.message : 'Unknown error occurred'
});
throw error;
}
return instance;
};
const getInstance = async ({ orgId, instanceId }: GetInstanceParams): Promise<InstanceDetails> => {
const instance = await dedicatedInstanceDAL.findById(instanceId);
if (!instance) {
throw new NotFoundError({ message: "Instance not found" });
}
if (instance.orgId !== orgId) {
throw new BadRequestError({ message: "Not authorized to access this instance" });
}
// Get CloudFormation stack status
try {
const awsConfig = {
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
region: instance.region,
};
if (!awsConfig.credentials.accessKeyId || !awsConfig.credentials.secretAccessKey) {
throw new Error('AWS credentials not found in environment variables');
}
const cfnClient = new CloudFormationClient(awsConfig);
// Get stack status
const stackResponse = await cfnClient.send(new DescribeStacksCommand({
StackName: instance.stackName
}));
const stack = stackResponse.Stacks?.[0];
if (!stack) {
return { instance };
}
// Get stack events for progress tracking
const eventsResponse = await cfnClient.send(new DescribeStackEventsCommand({
StackName: instance.stackName
}));
const events = eventsResponse.StackEvents?.map(event => ({
timestamp: event.Timestamp,
logicalResourceId: event.LogicalResourceId,
resourceType: event.ResourceType,
resourceStatus: event.ResourceStatus,
resourceStatusReason: event.ResourceStatusReason
})).sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0)) || [];
const stackStatus = stack.StackStatus || '';
let updates: Record<string, any> = {};
// Process outputs when stack is complete
if (stackStatus === 'CREATE_COMPLETE') {
const outputs = stack.Outputs || [];
const getOutput = (key: string) => outputs.find(o => o.OutputKey?.toLowerCase() === key.toLowerCase())?.OutputValue;
updates = {
status: 'RUNNING',
vpcId: getOutput('vpcid'),
subnetIds: [
getOutput('publicsubnet1id'),
getOutput('publicsubnet2id'),
getOutput('privatesubnet1id'),
getOutput('privatesubnet2id')
].filter(Boolean) as string[],
rdsInstanceId: getOutput('rdsinstanceid'),
redisClusterId: getOutput('redisclusterid'),
ecsClusterArn: getOutput('ecsclusterarn'),
ecsServiceArn: getOutput('ecsservicearn'),
securityGroupIds: [
getOutput('dbsecuritygroupid'),
getOutput('redissecuritygroupid')
].filter(Boolean) as string[]
};
} else if (stackStatus.includes('FAILED')) {
updates = {
status: 'FAILED',
error: stack.StackStatusReason
};
}
// Update instance if we have changes
if (Object.keys(updates).length > 0) {
await dedicatedInstanceDAL.updateById(instance.id, updates);
}
return {
instance: {
...instance,
...updates
},
stackStatus: stack.StackStatus,
stackStatusReason: stack.StackStatusReason,
events
};
} catch (error) {
// Log the error but don't throw - we still want to return the instance details
console.error('Error fetching CloudFormation stack status:', error);
}
return { instance };
};
return {
listInstances,
createInstance,
getInstance
};
};

View File

@ -12,7 +12,8 @@ export enum OrgPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
Delete = "delete",
Update = "update"
}
export enum OrgPermissionAppConnectionActions {
@ -50,7 +51,8 @@ export enum OrgPermissionSubjects {
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
Kmip = "kmip"
Kmip = "kmip",
DedicatedInstance = "dedicatedInstance"
}
export type AppConnectionSubjectFields = {
@ -81,7 +83,8 @@ export type OrgPermissionSet =
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgPermissionActions, OrgPermissionSubjects.DedicatedInstance];
const AppConnectionConditionSchema = z
.object({
@ -180,6 +183,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.DedicatedInstance).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
})
]);
@ -271,6 +278,11 @@ const buildAdminPermission = () => {
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
can(OrgPermissionActions.Create, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Read, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Update, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.DedicatedInstance);
return rules;
};
@ -301,6 +313,8 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Read, OrgPermissionSubjects.DedicatedInstance);
return rules;
};

View File

@ -232,6 +232,8 @@ import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal";
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
import { dedicatedInstanceDALFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-dal";
import { dedicatedInstanceServiceFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-service";
import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity";
@ -1457,6 +1459,12 @@ export const registerRoutes = async (
permissionService
});
const dedicatedInstanceDAL = dedicatedInstanceDALFactory(db);
const dedicatedInstanceService = dedicatedInstanceServiceFactory({
dedicatedInstanceDAL,
permissionService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@ -1557,7 +1565,8 @@ export const registerRoutes = async (
appConnection: appConnectionService,
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService
kmipOperation: kmipOperationService,
dedicatedInstance: dedicatedInstanceService
});
const cronJobs: CronJob[] = [];

View File

@ -1,32 +0,0 @@
---
title: "AB Initio"
description: "How to use Infisical secrets in AB Initio."
---
## Prerequisites
- Set up and add envars to [Infisical](https://app.infisical.com).
- Install the [Infisical CLI](https://infisical.com/docs/cli/overview) to your server.
## Setup
<Steps>
<Step title="Authorize Infisical for AB Initio">
Create a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) in Infisical and give it the appropriate read permissions for the desired project and secret paths.
</Step>
<Step title="Add Infisical CLI to your workflow">
Update your AB Initio workflows to use Infisical CLI to inject Infisical secrets as environment variables.
```bash
# Login using the machine identity. Modify this accordingly based on the authentication method used.
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain)
# Fetch secrets from Infisical
infisical export --projectId="<>" --env="prod" > infisical.env
# Inject secrets as environment variables
source infisical.env
```
</Step>
</Steps>

View File

@ -515,8 +515,7 @@
"integrations/frameworks/laravel",
"integrations/frameworks/rails",
"integrations/frameworks/dotnet",
"integrations/platforms/pm2",
"integrations/frameworks/ab-initio"
"integrations/platforms/pm2"
]
}
]

View File

@ -23,7 +23,7 @@
"@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2",
"@octokit/rest": "^21.1.1",
"@peculiar/x509": "^1.12.3",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.3",
@ -1605,16 +1605,16 @@
}
},
"node_modules/@octokit/core": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
"integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz",
"integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^5.0.0",
"@octokit/graphql": "^8.0.0",
"@octokit/request": "^9.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.0.0",
"@octokit/graphql": "^8.1.2",
"@octokit/request": "^9.2.1",
"@octokit/request-error": "^6.1.7",
"@octokit/types": "^13.6.2",
"before-after-hook": "^3.0.2",
"universal-user-agent": "^7.0.0"
},
@ -1623,12 +1623,12 @@
}
},
"node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
"integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.0.0",
"@octokit/types": "^13.6.2",
"universal-user-agent": "^7.0.2"
},
"engines": {
@ -1636,13 +1636,13 @@
}
},
"node_modules/@octokit/graphql": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
"integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.1.tgz",
"integrity": "sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^9.0.0",
"@octokit/types": "^13.0.0",
"@octokit/request": "^9.2.2",
"@octokit/types": "^13.8.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
@ -1650,18 +1650,18 @@
}
},
"node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
"version": "23.0.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "11.3.6",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.6.tgz",
"integrity": "sha512-zcvqqf/+TicbTCa/Z+3w4eBJcAxCFymtc0UAIsR3dEVoNilWld4oXdscQ3laXamTszUZdusw97K8+DrbFiOwjw==",
"version": "11.4.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.2.tgz",
"integrity": "sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.6.2"
"@octokit/types": "^13.7.0"
},
"engines": {
"node": ">= 18"
@ -1683,12 +1683,12 @@
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "13.2.6",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz",
"integrity": "sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==",
"version": "13.3.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.1.tgz",
"integrity": "sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.6.1"
"@octokit/types": "^13.8.0"
},
"engines": {
"node": ">= 18"
@ -1698,14 +1698,15 @@
}
},
"node_modules/@octokit/request": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz",
"integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.1.0",
"@octokit/endpoint": "^10.1.3",
"@octokit/request-error": "^6.1.7",
"@octokit/types": "^13.6.2",
"fast-content-type-parse": "^2.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
@ -1713,39 +1714,39 @@
}
},
"node_modules/@octokit/request-error": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
"integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.0.0"
"@octokit/types": "^13.6.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/rest": {
"version": "21.0.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz",
"integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==",
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
"integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
"license": "MIT",
"dependencies": {
"@octokit/core": "^6.1.2",
"@octokit/plugin-paginate-rest": "^11.0.0",
"@octokit/core": "^6.1.4",
"@octokit/plugin-paginate-rest": "^11.4.2",
"@octokit/plugin-request-log": "^5.3.1",
"@octokit/plugin-rest-endpoint-methods": "^13.0.0"
"@octokit/plugin-rest-endpoint-methods": "^13.3.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/types": {
"version": "13.6.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.2.tgz",
"integrity": "sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==",
"version": "13.8.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
"@octokit/openapi-types": "^23.0.1"
}
},
"node_modules/@peculiar/asn1-cms": {
@ -6905,6 +6906,22 @@
"safe-buffer": "^5.1.1"
}
},
"node_modules/fast-content-type-parse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",

View File

@ -27,7 +27,7 @@
"@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2",
"@octokit/rest": "^21.1.1",
"@peculiar/x509": "^1.12.3",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.3",

View File

@ -20,7 +20,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
sideOffset={-8}
sideOffset={10}
{...props}
ref={forwardedRef}
className={twMerge(

View File

@ -74,6 +74,13 @@ export const OrganizationLayout = () => {
</MenuItem>
)}
</Link>
<Link to="/organization/dedicated-instances">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="moving-block">
Dedicated Instances
</MenuItem>
)}
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (

View File

@ -20,46 +20,44 @@ export const MenuIconButton = <T extends ElementType = "button">({
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div className={!isSelected ? "hover:px-1" : ""}>
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded my-1 p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600 rounded-none",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
</Item>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
);
};

View File

@ -13,7 +13,8 @@ import {
faPlug,
faSignOut,
faUser,
faUsers
faUsers,
faServer
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
@ -84,10 +85,6 @@ export const MinimizedOrgSidebar = () => {
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { subscription } = useSubscription();
const [open, setOpen] = useState(false);
const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false);
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
@ -164,29 +161,23 @@ export const MinimizedOrgSidebar = () => {
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<div className="flex items-center hover:bg-mineshaft-700">
<DropdownMenu open={openOrg} onOpenChange={setOpenOrg} modal>
<DropdownMenuTrigger
onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
asChild
>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-3 pb-5 pt-6 transition-all">
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
<DropdownMenu modal>
<DropdownMenuTrigger asChild>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
align="start"
side="right"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
className="p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "320px" }}
>
<div className="px-0.5 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/5 to-mineshaft-800 p-1 transition-all duration-300">
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
{currentOrg?.name.charAt(0)}
</div>
@ -251,7 +242,7 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-1">
<div className="space-y-1 px-1">
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
@ -261,7 +252,7 @@ export const MinimizedOrgSidebar = () => {
}
icon="sliding-carousel"
>
Secrets
Secret Manager
</MenuIconButton>
)}
</Link>
@ -274,7 +265,7 @@ export const MinimizedOrgSidebar = () => {
}
icon="note"
>
PKI
Cert Manager
</MenuIconButton>
)}
</Link>
@ -306,41 +297,31 @@ export const MinimizedOrgSidebar = () => {
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
Scanner
Secret Scanning
</MenuIconButton>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Share
Secret Sharing
</MenuIconButton>
)}
</Link>
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
asChild
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full">
<MenuIconButton
lottieIconMode="reverse"
icon="settings-cog"
isSelected={isMoreSelected}
>
Admin
Org Controls
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
align="start"
side="right"
className="p-1"
>
<DropdownMenuContent align="start" side="right" className="p-1">
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
@ -352,6 +333,11 @@ export const MinimizedOrgSidebar = () => {
App Connections
</DropdownMenuItem>
</Link>
<Link to="/organization/dedicated-instances">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faServer} />}>
Dedicated Instances
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
@ -384,24 +370,14 @@ export const MinimizedOrgSidebar = () => {
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
<DropdownMenu open={openSupport} onOpenChange={setOpenSupport}>
<DropdownMenuTrigger
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
className="w-full"
>
<DropdownMenu>
<DropdownMenuTrigger className="w-full">
<MenuIconButton>
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
Support
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
align="end"
side="right"
className="p-1"
>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
@ -449,28 +425,17 @@ export const MinimizedOrgSidebar = () => {
</button>
</Tooltip>
)}
<DropdownMenu open={openUser} onOpenChange={setOpenUser}>
<DropdownMenuTrigger
onMouseEnter={() => setOpenUser(true)}
onMouseLeave={() => setOpenUser(false)}
className="w-full"
asChild
>
<DropdownMenu>
<DropdownMenuTrigger className="w-full" asChild>
<div>
<MenuIconButton icon="user">User</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenUser(true)}
onMouseLeave={() => setOpenUser(false)}
side="right"
align="end"
className="p-1"
>
<div className="cursor-default px-1 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
<div className="p-1 pr-3">
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="p-2">
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
@ -518,11 +483,11 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenuItem>
</Link>
)}
<div className="mt-1 border-t border-mineshaft-600 pt-1">
<Link to="/organization/admin">
<DropdownMenuItem>Organization Admin Console</DropdownMenuItem>
</Link>
</div>
<Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances')({
component: DedicatedInstancesPage,
});

View File

@ -0,0 +1,371 @@
import { useState } from "react";
import {
Button,
Card,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TBody,
Th,
THead,
Tr,
Td,
IconButton,
EmptyState,
Badge,
Tooltip,
Spinner
} from "@app/components/v2";
import {
faServer,
faBookOpen,
faArrowUpRightFromSquare,
faHome,
faTerminal,
faKey,
faLock,
faRotateRight,
faExclamationTriangle,
faChevronRight
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { useOrganization } from "@app/context/OrganizationContext";
import { apiRequest } from "@app/config/request";
interface DedicatedInstanceDetails {
id: string;
orgId: string;
instanceName: string;
subdomain: string;
status: "RUNNING" | "UPGRADING" | "PROVISIONING" | "FAILED";
version: string;
versionUpgrades: "Automatic" | "Manual";
clusterId: string;
clusterTier: "Development" | "Production";
clusterSize: string;
highAvailability: boolean;
createdAt: string;
region: string;
provider: string;
publicAccess: boolean;
ipAllowlist: "Enabled" | "Disabled";
}
const fetchInstanceDetails = async (organizationId: string, instanceId: string) => {
const { data } = await apiRequest.get<DedicatedInstanceDetails>(
`/api/v1/organizations/${organizationId}/dedicated-instances/${instanceId}`
);
return data;
};
export const DedicatedInstanceDetailsPage = () => {
const { currentOrg } = useOrganization();
const { instanceId } = useParams({
from: "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId"
});
const [isAPILockModalOpen, setIsAPILockModalOpen] = useState(false);
const [isRevokeTokensModalOpen, setIsRevokeTokensModalOpen] = useState(false);
const { data: instance, isLoading, error } = useQuery({
queryKey: ["dedicatedInstance", instanceId],
queryFn: () => fetchInstanceDetails(currentOrg?.id || "", instanceId || ""),
enabled: Boolean(currentOrg?.id && instanceId)
});
if (isLoading) {
return (
<div className="flex h-full items-center justify-center">
<Spinner size="lg" />
</div>
);
}
if (error || !instance) {
return (
<div className="flex h-full items-center justify-center">
<EmptyState
title="Error loading instance details"
icon={faServer}
>
<p className="text-sm text-bunker-400">
{error instanceof Error ? error.message : "Failed to load instance details"}
</p>
</EmptyState>
</div>
);
}
return (
<div className="flex h-full flex-col space-y-4 p-4">
<div className="mb-6">
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
<Link to="/" className="flex items-center gap-1 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faHome} />
<span>Home</span>
</Link>
<FontAwesomeIcon icon={faChevronRight} className="text-xs" />
<Link to="/organization/dedicated-instances" className="hover:text-mineshaft-200">
Dedicated Instances
</Link>
<FontAwesomeIcon icon={faChevronRight} className="text-xs" />
<span className="text-mineshaft-200">{instance.instanceName}</span>
</div>
<div className="mt-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-semibold text-mineshaft-100">
{instance.instanceName} <span className="text-mineshaft-300">({instance.region})</span>
</h1>
</div>
<div>
<Button
variant="solid"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faArrowUpRightFromSquare} />}
onClick={() => window.open(`https://${instance.subdomain}.infisical.com`, "_blank")}
>
Launch Web UI
</Button>
</div>
</div>
{instance.publicAccess && (
<div className="mt-4 flex items-center gap-2 rounded-lg bg-yellow-500/10 px-4 py-3 text-sm text-yellow-500">
<FontAwesomeIcon icon={faExclamationTriangle} />
<p>
This cluster's network configuration is set to public. Configure an IP Allowlist in the cluster's networking
settings to limit network access to the cluster's public endpoint.
</p>
</div>
)}
</div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="grid grid-cols-2 gap-4">
<Card className="col-span-1 rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster Details</h2>
<div className="space-y-4">
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Status</span>
<Badge variant={instance.status === "RUNNING" ? "success" : "primary"}>
{instance.status}
</Badge>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Vault Version</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.version}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Version upgrades</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.versionUpgrades}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Cluster Size</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.clusterSize}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">High Availability</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.highAvailability ? "Yes" : "No"}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-mineshaft-300">Created</span>
<span className="text-sm font-medium text-mineshaft-100">
{new Date(instance.createdAt).toLocaleDateString()}
</span>
</div>
</div>
</div>
</Card>
<div className="col-span-1 space-y-4">
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Quick actions</h2>
<div className="grid grid-cols-2 gap-4">
<Card className="bg-mineshaft-700 rounded-lg">
<div className="p-4">
<h3 className="mb-2 text-sm font-medium text-mineshaft-100">How to access via</h3>
<div className="flex gap-4">
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faTerminal} />}
>
Command-line (CLI)
</Button>
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faKey} />}
>
API
</Button>
</div>
</div>
</Card>
<Card className="bg-mineshaft-700 rounded-lg">
<div className="p-4">
<h3 className="mb-2 text-sm font-medium text-mineshaft-100">New root token</h3>
<Tooltip content="Generate a root token">
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faKey} />}
>
Generate token
</Button>
</Tooltip>
</div>
</Card>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster URLs</h2>
<p className="mb-4 text-sm text-mineshaft-300">Copy the address into your CLI or browser to access the cluster.</p>
<div className="space-y-2">
<div className="flex items-center justify-between rounded-lg bg-mineshaft-700 p-3">
<div className="flex items-center gap-2">
<Badge variant="primary">Private</Badge>
<code className="text-sm text-mineshaft-300">https://{instance.subdomain}.infisical.com</code>
</div>
<Button variant="outline" colorSchema="secondary" size="sm">
Copy
</Button>
</div>
<div className="flex items-center justify-between rounded-lg bg-mineshaft-700 p-3">
<div className="flex items-center gap-2">
<Badge variant="primary">Public</Badge>
<code className="text-sm text-mineshaft-300">https://{instance.subdomain}.infisical.com</code>
</div>
<Button variant="outline" colorSchema="secondary" size="sm">
Copy
</Button>
</div>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster networking</h2>
<div className="space-y-4">
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Provider/region</span>
<span className="text-sm font-medium text-mineshaft-100">
{instance.provider} ({instance.region})
</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Cluster accessibility</span>
<Badge variant={instance.publicAccess ? "danger" : "success"}>
{instance.publicAccess ? "Public" : "Private"}
</Badge>
</div>
<div className="flex justify-between">
<span className="text-sm text-mineshaft-300">IP allowlist</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.ipAllowlist}</span>
</div>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">In case of emergency</h2>
<div className="space-y-4">
<Button
variant="outline"
colorSchema="danger"
className="w-full"
leftIcon={<FontAwesomeIcon icon={faLock} />}
onClick={() => setIsAPILockModalOpen(true)}
>
API Lock
</Button>
<Button
variant="outline"
colorSchema="danger"
className="w-full"
leftIcon={<FontAwesomeIcon icon={faRotateRight} />}
onClick={() => setIsRevokeTokensModalOpen(true)}
>
Revoke all admin tokens
</Button>
</div>
</div>
</Card>
</div>
</div>
</div>
<Modal isOpen={isAPILockModalOpen} onOpenChange={setIsAPILockModalOpen}>
<ModalContent
title="API Lock"
subTitle="Are you sure you want to lock the API? This will prevent all API access until unlocked."
>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsAPILockModalOpen(false)}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="danger"
onClick={() => {
// Handle API lock
setIsAPILockModalOpen(false);
}}
>
Lock API
</Button>
</div>
</ModalContent>
</Modal>
<Modal isOpen={isRevokeTokensModalOpen} onOpenChange={setIsRevokeTokensModalOpen}>
<ModalContent
title="Revoke Admin Tokens"
subTitle="Are you sure you want to revoke all admin tokens? This action cannot be undone."
>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsRevokeTokensModalOpen(false)}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="danger"
onClick={() => {
// Handle token revocation
setIsRevokeTokensModalOpen(false);
}}
>
Revoke Tokens
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '../DedicatedInstanceDetailsPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage
});

View File

@ -0,0 +1,602 @@
import { useState } from "react";
import {
Button,
Card,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
DropdownMenu,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuContent,
Table,
TableContainer,
TBody,
Th,
THead,
Tr,
Td,
IconButton,
EmptyState,
FormHelperText,
Spinner
} from "@app/components/v2";
import {
faPlus,
faSearch,
faServer,
faEllipsisVertical,
faBookOpen,
faArrowUpRightFromSquare,
faArrowDown,
faArrowUp,
faMagnifyingGlass,
faHome,
faSpinner
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Link, useNavigate } from "@tanstack/react-router";
import { useQuery, useMutation, useQueryClient, useQueries } from "@tanstack/react-query";
import { useOrganization } from "@app/context/OrganizationContext";
import { apiRequest } from "@app/config/request";
interface DedicatedInstance {
id: string;
orgId: string;
instanceName: string;
subdomain: string;
status: "RUNNING" | "UPGRADING" | "PROVISIONING" | "FAILED";
stackStatus?: string;
stackStatusReason?: string;
events?: Array<{
timestamp?: Date;
logicalResourceId?: string;
resourceType?: string;
resourceStatus?: string;
resourceStatusReason?: string;
}>;
rdsInstanceType: string;
elasticCacheType: string;
elasticContainerMemory: number;
elasticContainerCpu: number;
region: string;
version: string;
backupRetentionDays: number;
lastBackupTime: string | null;
lastUpgradeTime: string | null;
publiclyAccessible: boolean;
vpcId: string | null;
subnetIds: string[] | null;
tags: Record<string, string> | null;
createdAt: string;
updatedAt: string;
}
const INSTANCE_SIZES = [
{ value: "small", label: "Small (2 vCPU, 8GB RAM)" },
{ value: "medium", label: "Medium (4 vCPU, 16GB RAM)" },
{ value: "large", label: "Large (8 vCPU, 32GB RAM)" }
];
type CloudProvider = "aws" | "gcp" | "azure";
interface CreateInstancePayload {
instanceName: string;
subdomain: string;
region: string;
provider: "aws";
publiclyAccessible: boolean;
clusterSize: "small" | "medium" | "large";
}
const INITIAL_INSTANCE_STATE: CreateInstancePayload = {
instanceName: "",
subdomain: "",
region: "",
provider: "aws",
publiclyAccessible: false,
clusterSize: "small"
};
const PROVIDERS: Array<{
value: CloudProvider;
label: string;
image: string;
description: string;
}> = [
{
value: "aws",
label: "Amazon Web Services",
image: "/images/integrations/Amazon Web Services.png",
description: "Deploy your instance on AWS infrastructure"
},
{
value: "gcp",
label: "Google Cloud Platform",
image: "/images/integrations/Google Cloud Platform.png",
description: "Deploy your instance on Google Cloud infrastructure"
},
{
value: "azure",
label: "Microsoft Azure",
image: "/images/integrations/Microsoft Azure.png",
description: "Deploy your instance on Azure infrastructure"
}
];
const REGIONS = {
aws: [
{ value: "us-east-1", label: "US East (N. Virginia)" },
{ value: "us-west-2", label: "US West (Oregon)" },
{ value: "eu-west-1", label: "EU West (Ireland)" },
{ value: "ap-southeast-1", label: "Asia Pacific (Singapore)" }
],
gcp: [
{ value: "us-east1", label: "US East (South Carolina)" },
{ value: "us-west1", label: "US West (Oregon)" },
{ value: "europe-west1", label: "Europe West (Belgium)" },
{ value: "asia-southeast1", label: "Asia Southeast (Singapore)" }
],
azure: [
{ value: "eastus", label: "East US (Virginia)" },
{ value: "westus", label: "West US (California)" },
{ value: "westeurope", label: "West Europe (Netherlands)" },
{ value: "southeastasia", label: "Southeast Asia (Singapore)" }
]
};
interface RouteParams {
organizationId: string;
}
const dedicatedInstanceKeys = {
getInstances: (organizationId: string) => ["dedicatedInstances", { organizationId }] as const,
getInstance: (organizationId: string, instanceId: string) => ["dedicatedInstance", { organizationId, instanceId }] as const
};
const getDeploymentStage = (events?: DedicatedInstance['events']) => {
if (!events?.length) return 'Deploying';
return 'Deploying';
};
const getDeploymentProgress = (instance: DedicatedInstance) => {
if (instance.status === 'RUNNING') return { stage: 'Complete', progress: 100 };
if (instance.status === 'FAILED') return { stage: 'Failed', progress: 0 };
const events = instance.events || [];
if (events.length === 0) return { stage: 'Deploying', progress: 0 };
// Count completed events vs total events
const completedEvents = events.filter(event =>
event.resourceStatus?.includes('COMPLETE') ||
event.resourceStatus?.includes('CREATE_COMPLETE')
).length;
const progress = Math.round((completedEvents / events.length) * 100);
return { stage: 'Deploying', progress };
};
const fetchInstances = async (organizationId: string) => {
const { data } = await apiRequest.get<{ instances: DedicatedInstance[] }>(
`/api/v1/organizations/${organizationId}/dedicated-instances`
);
return data;
};
const fetchInstanceDetails = async (organizationId: string, instanceId: string) => {
const { data } = await apiRequest.get<DedicatedInstance>(
`/api/v1/organizations/${organizationId}/dedicated-instances/${instanceId}`
);
return data;
};
const createInstance = async (organizationId: string, data: CreateInstancePayload) => {
const { data: response } = await apiRequest.post<DedicatedInstance>(
`/api/v1/organizations/${organizationId}/dedicated-instances`,
data
);
return response;
};
export const DedicatedInstancesPage = () => {
const { currentOrg } = useOrganization();
const navigate = useNavigate();
const organizationId = currentOrg?.id || "";
const queryClient = useQueryClient();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [newInstance, setNewInstance] = useState(INITIAL_INSTANCE_STATE);
type InstancesResponse = { instances: DedicatedInstance[] };
// Fetch all instances
const { data: instancesData, isLoading, error } = useQuery<InstancesResponse>({
queryKey: dedicatedInstanceKeys.getInstances(organizationId),
queryFn: () => fetchInstances(organizationId),
enabled: Boolean(organizationId),
});
// Fetch details for provisioning instances
const provisioningInstances = instancesData?.instances.filter(
instance => instance.status === "PROVISIONING"
) || [];
const instanceDetailsQueries = useQueries({
queries: provisioningInstances.map(instance => ({
queryKey: dedicatedInstanceKeys.getInstance(organizationId, instance.id),
queryFn: () => fetchInstanceDetails(organizationId, instance.id),
refetchInterval: 2000, // Poll every 2 seconds
}))
});
// Merge instance details with the main instances list
const instances = instancesData?.instances.map(instance => {
if (instance.status === "PROVISIONING") {
const detailsQuery = instanceDetailsQueries.find(
q => q.data?.id === instance.id
);
return detailsQuery?.data || instance;
}
return instance;
}) || [];
// Create instance mutation
const createInstanceMutation = useMutation({
mutationFn: (data: CreateInstancePayload) => createInstance(organizationId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dedicatedInstanceKeys.getInstances(organizationId) });
setIsCreateModalOpen(false);
setNewInstance(INITIAL_INSTANCE_STATE);
}
});
const handleCreateInstance = () => {
createInstanceMutation.mutate(newInstance);
};
const handleModalOpenChange = (open: boolean) => {
setIsCreateModalOpen(open);
if (!open) {
setNewInstance(INITIAL_INSTANCE_STATE);
}
};
// Get available regions based on selected provider
const availableRegions = newInstance.provider ? REGIONS[newInstance.provider] : [];
// Filter instances based on search term
const filteredInstances = instances.filter((instance: DedicatedInstance) =>
instance.instanceName.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="flex h-full flex-col space-y-4 p-4">
<div className="mb-6">
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
<Link to="/" className="flex items-center gap-1 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faHome} />
<span>Home</span>
</Link>
<span>/</span>
<span className="text-mineshaft-200">Dedicated Instances</span>
</div>
<div className="mt-4 flex items-center gap-3">
<h1 className="text-3xl font-semibold text-mineshaft-100">Dedicated Instances</h1>
<a
href="https://infisical.com/docs/dedicated-instances/overview"
target="_blank"
rel="noopener noreferrer"
className="mt-1.5 inline-flex items-center gap-1.5 rounded-md bg-yellow/20 px-2 py-1 text-sm text-yellow opacity-80 hover:opacity-100"
>
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-[10px]" />
</a>
</div>
<p className="mt-2 text-base text-mineshaft-300">
Create and manage dedicated Infisical instances across different regions
</p>
</div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-end mb-4">
<Button
onClick={() => setIsCreateModalOpen(true)}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Create Instance
</Button>
</div>
<div>
<div className="flex gap-2">
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search instances..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/4">Name</Th>
<Th className="w-1/4">Type</Th>
<Th className="w-1/4">Region</Th>
<Th className="w-1/4">Status</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading ? (
<tr>
<td colSpan={5}>
<div className="flex items-center justify-center py-8">
<Spinner size="md" className="text-mineshaft-300" />
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={5}>
<EmptyState
title="Error loading instances"
icon={faServer}
>
<p className="text-sm text-bunker-400">
{error instanceof Error ? error.message : "An error occurred while loading instances"}
</p>
</EmptyState>
</td>
</tr>
) : filteredInstances.length === 0 ? (
<tr>
<td colSpan={5}>
<EmptyState
title={searchTerm ? "No Instances match search..." : "No Instances Found"}
icon={faServer}
>
<p className="text-sm text-bunker-400">
You don't have any dedicated instances yet. Create a new one to get started.
</p>
</EmptyState>
</td>
</tr>
) : (
filteredInstances.map((instance: DedicatedInstance) => (
<Tr
key={instance.id}
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700 cursor-pointer")}
onClick={() => {
navigate({
to: "/organization/dedicated-instances/$instanceId",
params: {
instanceId: instance.id
}
});
}}
>
<Td>
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-bunker-700/50">
<FontAwesomeIcon icon={faServer} className="text-bunker-300" />
</div>
<div>
<p className="text-sm font-medium text-bunker-100">{instance.instanceName}</p>
<p className="text-xs text-bunker-300">Created {new Date(instance.createdAt).toLocaleDateString()}</p>
</div>
</div>
</Td>
<Td>
<p className="text-sm font-medium text-bunker-100">
{`${instance.elasticContainerCpu / 1024} vCPU, ${instance.elasticContainerMemory / 1024}GB RAM`}
</p>
<p className="text-xs text-bunker-300">Instance Type</p>
</Td>
<Td>
<p className="text-sm font-medium text-bunker-100">
{REGIONS.aws.find(r => r.value === instance.region)?.label || instance.region}
</p>
<p className="text-xs text-bunker-300">Region</p>
</Td>
<Td>
{instance.status === "PROVISIONING" ? (
<div className="space-y-1">
<div className="flex items-center">
<span className="text-sm font-medium text-yellow-500">
{getDeploymentProgress(instance).stage}
</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-mineshaft-600">
<div
className="h-full rounded-full bg-yellow-500/50 transition-all duration-500 [animation:pulse_0.7s_cubic-bezier(0.4,0,0.6,1)_infinite]"
style={{ width: `${getDeploymentProgress(instance).progress}%` }}
/>
</div>
</div>
) : (
<div
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
instance.status === "RUNNING"
? "bg-emerald-100/10 text-emerald-500"
: instance.status === "FAILED"
? "bg-red-100/10 text-red-500"
: "bg-yellow-100/10 text-yellow-500"
}`}
>
{instance.status.toLowerCase()}
</div>
)}
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
onClick={(e) => e.stopPropagation()}
>
<FontAwesomeIcon icon={faEllipsisVertical} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(instance.id);
}}>
Copy Instance ID
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Start instance
}}>
Start Instance
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Stop instance
}}>
Stop Instance
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Delete instance
}} className="text-red-500">
Delete Instance
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))
)}
</TBody>
</Table>
</TableContainer>
</div>
</div>
<Modal isOpen={isCreateModalOpen} onOpenChange={handleModalOpenChange}>
<ModalContent title="Create Dedicated Instance" subTitle="Configure your dedicated Infisical instance">
<div className="mt-6 space-y-6">
<div>
<div className="mb-2 text-sm font-medium text-mineshaft-200">Cloud Provider</div>
<div className="grid grid-cols-3 gap-2">
{PROVIDERS.map(({ value, label, image, description }) => (
<button
key={value}
type="button"
onClick={() => {
if (value === "aws") {
setNewInstance({ ...newInstance, provider: value, region: "" });
}
}}
className={twMerge(
"group relative flex h-24 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600",
newInstance.provider === value && "border-primary/50 bg-primary/10"
)}
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-mineshaft-800">
<img
src={image}
alt={`${label} logo`}
className="h-7 w-7 object-contain"
/>
</div>
<div className="mt-2 max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{label}
</div>
</button>
))}
</div>
<div className="mt-1 text-xs text-mineshaft-400">
{PROVIDERS.find(p => p.value === newInstance.provider)?.description || "Select a cloud provider for your instance"}
</div>
</div>
<FormControl label="Instance Name" helperText="Give your instance a unique name to identify it">
<Input
value={newInstance.instanceName}
onChange={(e) => setNewInstance({ ...newInstance, instanceName: e.target.value })}
placeholder="Enter instance name"
/>
</FormControl>
<FormControl label="Subdomain" helperText="Enter the subdomain for your instance">
<div className="flex items-center">
<Input
value={newInstance.subdomain}
onChange={(e) => setNewInstance({ ...newInstance, subdomain: e.target.value })}
placeholder="your-subdomain"
className="rounded-r-none"
/>
<div className="flex h-10 items-center rounded-r-lg border border-l-0 border-mineshaft-600 bg-mineshaft-800 px-3 text-sm text-mineshaft-300">
.infisical.com
</div>
</div>
</FormControl>
<FormControl label="Region" helperText="Select the geographical location where your instance will be deployed">
<Select
value={newInstance.region}
onValueChange={(value) => setNewInstance({ ...newInstance, region: value })}
placeholder="Select region"
isDisabled={!newInstance.provider}
>
{availableRegions.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Cluster Size" helperText="Select the size of your dedicated instance">
<Select
value={newInstance.clusterSize}
onValueChange={(value) => setNewInstance({ ...newInstance, clusterSize: value as "small" | "medium" | "large" })}
placeholder="Select size"
>
<SelectItem value="small">Small (1 vCPU, 2GB RAM)</SelectItem>
<SelectItem value="medium">Medium (2 vCPU, 4GB RAM)</SelectItem>
<SelectItem value="large">Large (4 vCPU, 8GB RAM)</SelectItem>
</Select>
</FormControl>
</div>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsCreateModalOpen(false)}
size="md"
disabled={createInstanceMutation.status === "pending"}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="primary"
onClick={handleCreateInstance}
disabled={!newInstance.instanceName || !newInstance.region || !newInstance.provider || !newInstance.subdomain || createInstanceMutation.status === "pending"}
size="md"
leftIcon={createInstanceMutation.status === "pending" ? <Spinner size="xs" className="text-black" /> : undefined}
>
{createInstanceMutation.status === "pending" ? "Creating..." : "Create Instance"}
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};

View File

@ -0,0 +1,2 @@
export { DedicatedInstancesPage } from './DedicatedInstancesPage';
export { DedicatedInstanceDetailsPage } from './DedicatedInstanceDetailsPage';

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import { DedicatedInstancesPage } from './DedicatedInstancesPage'
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/',
)({
component: DedicatedInstancesPage,
})

View File

@ -120,7 +120,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="API Key" type="text" autoComplete="off" autoCorrect="off" spellCheck="false" />
<Input {...field} placeholder="API Key" type="text" />
</FormControl>
)}
/>
@ -155,7 +155,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
errorText={error?.message}
isOptional
>
<Input {...field} placeholder="Password" type="password" autoComplete="new-password" autoCorrect="off" spellCheck="false" aria-autocomplete="none" data-form-type="other" />
<Input {...field} placeholder="Password" type="password" />
</FormControl>
)}
/>

View File

@ -59,12 +59,7 @@ export const PitDrawer = ({
onClick={() => onSelectSnapshot(id)}
>
<div className="flex w-full justify-between">
<div>
{(() => {
const distance = formatDistance(new Date(createdAt), new Date());
return distance.charAt(0).toUpperCase() + distance.slice(1) + " ago";
})()}
</div>
<div>{formatDistance(new Date(createdAt), new Date())}</div>
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div>
</div>
</Button>
@ -75,7 +70,7 @@ export const PitDrawer = ({
<Button
className="mt-8 px-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
variant="star"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={fetchNextPage}

View File

@ -1,11 +1,11 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
import {
faArrowRotateRight,
faCheckCircle,
faCircle,
faCircleDot,
faClock,
faEyeSlash,
faPlus,
faShare,
faTag,
@ -236,102 +236,44 @@ export const SecretDetailSidebar = ({
}}
isOpen={isOpen}
>
<DrawerContent title={`Secret ${secret?.key}`} className="thin-scrollbar">
<DrawerContent title="Secret">
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
<div className="flex h-full flex-col">
<div className="flex flex-row">
<div className="w-full">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Controller
name="value"
key="secret-value"
control={control}
render={({ field }) => (
<FormControl label="Value">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
)}
</ProjectPermissionCan>
</div>
<div className="ml-1 mt-1.5 flex items-center">
<Button
className="w-full px-2 py-[0.43rem] font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share
</Button>
</div>
</div>
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div className="mb-4 px-4">
<FormControl label="Key">
<Input isDisabled {...register("key")} />
</FormControl>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Controller
name="value"
key="secret-value"
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<div className="flex items-center justify-between">
<span className="w-max text-sm text-mineshaft-300">
Multi-line encoding
<Tooltip
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip>
</span>
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center justify-between"
/>
</div>
)}
</ProjectPermissionCan>
render={({ field }) => (
<FormControl label="Value">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
</div>
<div
className={`mb-4 w-full border-t border-mineshaft-600 ${isOverridden ? "block" : "hidden"}`}
/>
)}
</ProjectPermissionCan>
<div className="mb-2 border-b border-mineshaft-600 pb-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
@ -342,243 +284,194 @@ export const SecretDetailSidebar = ({
})}
>
{(isAllowed) => (
<div className="flex items-center justify-between px-4 pb-4">
<span className="w-max text-sm text-mineshaft-300">
Override with a personal value
<Tooltip
content="Override the secret value with a personal value that does not get shared with other users and machines."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip>
</span>
<Switch
isDisabled={!isAllowed}
id="personal-override"
onCheckedChange={handleOverrideClick}
isChecked={isOverridden}
className="justify-start"
/>
</div>
<Switch
isDisabled={!isAllowed}
id="personal-override"
onCheckedChange={handleOverrideClick}
isChecked={isOverridden}
>
Override with a personal value
</Switch>
)}
</ProjectPermissionCan>
{isOverridden && (
<Controller
name="valueOverride"
control={control}
render={({ field }) => (
<FormControl label="Override Value" className="px-4">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
)}
/>
)}
</div>
<div className="mb-4 mt-2 flex flex-col rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div
className={`flex justify-between px-4 text-mineshaft-100 ${tagFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
>
<div
className={`text-sm text-mineshaft-300 ${tagFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
>
Tags
</div>
<div>
<FormControl>
<div
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
>
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="start" side="right" className="z-[100]">
<DropdownMenuLabel className="pl-2">
Add tags to this secret
</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<div className="p-2">
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</div>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
{isOverridden && (
<Controller
name="valueOverride"
control={control}
render={({ field }) => (
<FormControl label="Value Override">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
</div>
</div>
<div
className={`mb-4 w-full border-t border-mineshaft-600 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`}
)}
/>
<div
className={`flex justify-between px-4 text-mineshaft-100 ${metadataFormFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
>
<div
className={`text-sm text-mineshaft-300 ${metadataFormFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
>
Metadata
</div>
<FormControl>
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className={`${metadataFormFields.fields.length > 0 ? "pt-2" : ""}`}>
<IconButton
ariaLabel="Add Key"
variant="outline_bg"
size="xs"
className="rounded-md"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
)}
<FormControl label="Metadata">
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</FormControl>
))}
<div className="mt-2">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</div>
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 bg-bunker-800 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<FormControl>
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<DropdownMenuItem asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormControl label="Reminder">
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
<div className="flex items-center justify-between px-2">
<div className="ml-1 mt-2 flex items-center justify-between">
<div className="flex items-center space-x-2">
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
<span className="text-sm text-bunker-300">
@ -597,9 +490,9 @@ export const SecretDetailSidebar = ({
</div>
</div>
) : (
<div className="ml-1 flex items-center space-x-2">
<div className="ml-1 mt-2 flex items-center space-x-2">
<Button
className="w-full px-2 py-2 font-normal"
className="w-full px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />}
onClick={() => setCreateReminderFormOpen.on()}
@ -610,147 +503,92 @@ export const SecretDetailSidebar = ({
</div>
)}
</FormControl>
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<div className="mb-2 pl-1">Version History</div>
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, version, id }) => (
<div className="flex flex-row">
<div key={id} className="flex w-full flex-col space-y-1">
<div className="flex items-center">
<div className="w-10">
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
v{version}
</div>
</div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div>
<div className="flex w-full cursor-default">
<div className="relative w-10">
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
</div>
<div className="flex flex-row">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
Value:
</div>
<div className="group break-all pl-1 font-mono">
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
<button
type="button"
className="select-none"
onClick={(e) => {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
{secretValue}
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}
}}
>
<FontAwesomeIcon icon={faEyeSlash} />
</button>
</div>
<span className="group-[.show-value]:hidden">
{secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget.closest(".group")?.classList.add("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.currentTarget
.closest(".group")
?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
</div>
</div>
</div>
<div
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
>
<Tooltip content="Restore Secret Value">
<IconButton
ariaLabel="Restore"
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() => setValue("value", secretValue)}
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<div className="my-2 mb-4 border-b border-mineshaft-600 pb-4">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Multi line encoding
<Tooltip
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div>
<div className="ml-1 flex items-center space-x-4">
<Button
className="w-full px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share Secret
</Button>
</div>
<div className="dark mb-4 mt-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
</div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{secretValue}</div>
</div>
</div>
))}
</div>
</div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2 mt-4">
<div className="mb-2">
Access List
<Tooltip
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</div>
{isPending && (
@ -770,22 +608,14 @@ export const SecretDetailSidebar = ({
</Button>
)}
{!isPending && secretAccessList && (
<div className="mb-4 flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
<div className="flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretAccessList.users.length > 0 && (
<div className="pb-3">
<div className="mb-2 font-bold">Users</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.users.map((user) => (
<div className="rounded-md bg-bunker-500">
<Tooltip
content={user.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
className="z-[100]"
>
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip content={user.allowedActions.join(", ")} className="z-[100]">
<Link
to={
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
@ -794,7 +624,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id,
membershipId: user.membershipId
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 text-sm hover:text-primary"
>
{user.name}
</Link>
@ -809,14 +639,9 @@ export const SecretDetailSidebar = ({
<div className="mb-2 font-bold">Identities</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.identities.map((identity) => (
<div className="rounded-md bg-bunker-500">
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip
content={identity.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
content={identity.allowedActions.join(", ")}
className="z-[100]"
>
<Link
@ -827,7 +652,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id,
identityId: identity.id
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 text-sm hover:text-primary"
>
{identity.name}
</Link>
@ -842,14 +667,9 @@ export const SecretDetailSidebar = ({
<div className="mb-2 font-bold">Groups</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.groups.map((group) => (
<div className="rounded-md bg-bunker-500">
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip
content={group.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
content={group.allowedActions.join(", ")}
className="z-[100]"
>
<Link
@ -857,7 +677,7 @@ export const SecretDetailSidebar = ({
params={{
groupId: group.id
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 text-sm hover:text-primary"
>
{group.name}
</Link>
@ -871,7 +691,7 @@ export const SecretDetailSidebar = ({
)}
</div>
<div className="flex flex-col space-y-4">
<div className="mb-4 flex items-center space-x-4">
<div className="mb-2 flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
@ -885,7 +705,6 @@ export const SecretDetailSidebar = ({
<Button
isFullWidth
type="submit"
variant="outline_bg"
isDisabled={isSubmitting || !isDirty || !isAllowed}
isLoading={isSubmitting}
>
@ -903,17 +722,9 @@ export const SecretDetailSidebar = ({
})}
>
{(isAllowed) => (
<IconButton
colorSchema="danger"
ariaLabel="Delete Secret"
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
isDisabled={!isAllowed}
onClick={onDeleteSecret}
>
<Tooltip content="Delete Secret">
<FontAwesomeIcon icon={faTrash} />
</Tooltip>
</IconButton>
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
Delete
</Button>
)}
</ProjectPermissionCan>
</div>

View File

@ -59,7 +59,9 @@ import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/org
import { Route as organizationKmsOverviewPageRouteImport } from './pages/organization/KmsOverviewPage/route'
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport } from './pages/organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route'
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
import { Route as organizationDedicatedInstancesPageRouteImport } from './pages/organization/DedicatedInstancesPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
@ -209,6 +211,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections',
@ -454,6 +460,16 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport.update(
{
id: '/dedicated-instances',
path: '/dedicated-instances',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any,
)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({
id: '/app-connections',
@ -617,6 +633,16 @@ const organizationGroupDetailsByIDPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute =
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport.update(
{
id: '/$instanceId',
path: '/$instanceId',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute,
} as any,
)
const organizationCertManagerOverviewPageRouteRoute =
organizationCertManagerOverviewPageRouteImport.update({
id: '/cert-manager/overview',
@ -625,6 +651,14 @@ const organizationCertManagerOverviewPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationDedicatedInstancesPageRouteRoute =
organizationDedicatedInstancesPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute,
} as any)
const organizationAppConnectionsAppConnectionsPageRouteRoute =
organizationAppConnectionsAppConnectionsPageRouteImport.update({
id: '/',
@ -1886,6 +1920,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances'
path: '/dedicated-instances'
fullPath: '/organization/dedicated-instances'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
path: '/secret-manager/$projectId'
@ -1907,6 +1948,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/'
path: '/'
fullPath: '/organization/dedicated-instances/'
preLoaderRoute: typeof organizationDedicatedInstancesPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
path: '/cert-manager/overview'
@ -1914,6 +1962,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationCertManagerOverviewPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId'
path: '/$instanceId'
fullPath: '/organization/dedicated-instances/$instanceId'
preLoaderRoute: typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
path: '/groups/$groupId'
@ -2900,6 +2955,24 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren {
organizationDedicatedInstancesPageRouteRoute: typeof organizationDedicatedInstancesPageRouteRoute
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute: typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren =
{
organizationDedicatedInstancesPageRouteRoute:
organizationDedicatedInstancesPageRouteRoute,
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute:
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
@ -2909,6 +2982,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
@ -2933,6 +3007,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren,
organizationCertManagerOverviewPageRouteRoute:
organizationCertManagerOverviewPageRouteRoute,
organizationGroupDetailsByIDPageRouteRoute:
@ -3608,10 +3684,13 @@ export interface FileRoutesByFullPath {
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/dedicated-instances': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/dedicated-instances/': typeof organizationDedicatedInstancesPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -3780,7 +3859,9 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/dedicated-instances': typeof organizationDedicatedInstancesPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -3956,10 +4037,13 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/': typeof organizationDedicatedInstancesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -4136,10 +4220,13 @@ export interface FileRouteTypes {
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/organization/app-connections'
| '/organization/dedicated-instances'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections/'
| '/organization/dedicated-instances/'
| '/organization/cert-manager/overview'
| '/organization/dedicated-instances/$instanceId'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
| '/organization/kms/overview'
@ -4307,7 +4394,9 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections'
| '/organization/dedicated-instances'
| '/organization/cert-manager/overview'
| '/organization/dedicated-instances/$instanceId'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
| '/organization/kms/overview'
@ -4481,10 +4570,13 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/'
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
| '/_authenticate/_inject-org-details/_org-layout/organization/kms/overview'
@ -4848,6 +4940,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances",
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
@ -4919,6 +5012,14 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/",
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId"
]
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@ -4937,10 +5038,18 @@ export const routeTree = rootRoute
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/": {
"filePath": "organization/DedicatedInstancesPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances"
},
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
"filePath": "organization/CertManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId": {
"filePath": "organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances"
},
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId": {
"filePath": "organization/GroupDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"

View File

@ -16,6 +16,10 @@ const organizationRoutes = route("/organization", [
route("/admin", "organization/AdminPage/route.tsx"),
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
route("/billing", "organization/BillingPage/route.tsx"),
route("/dedicated-instances", [
index("organization/DedicatedInstancesPage/route.tsx"),
route("/$instanceId", "organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route.tsx")
]),
route("/secret-sharing", "organization/SecretSharingPage/route.tsx"),
route("/settings", "organization/SettingsPage/route.tsx"),
route("/secret-scanning", "organization/SecretScanningPage/route.tsx"),

View File

@ -0,0 +1,27 @@
import { Router, Route as RootRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage, DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
// ... existing routes ...
// Add dedicated instances routes
const dedicatedInstancesRoute = new RootRoute({
path: '/organization/:organizationId/dedicated-instances',
component: DedicatedInstancesPage
});
const dedicatedInstanceDetailsRoute = new RootRoute({
path: '/organization/:organizationId/dedicated-instances/:instanceId',
component: DedicatedInstanceDetailsPage
});
// Add to routeTree
routeTree.addChildren([dedicatedInstancesRoute, dedicatedInstanceDetailsRoute]);
// Create and export router
export const router = new Router({ routeTree });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances')({
component: DedicatedInstancesPage,
});

View File

@ -0,0 +1,12 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage, DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
// List route
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances')({
component: DedicatedInstancesPage
});
// Details route
export const InstanceRoute = createFileRoute('/organization/$organizationId/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage
});