Update UI to take environment variables from user #6

Merged
nabarun merged 6 commits from iv-env-ui into main 2024-10-21 11:05:36 +00:00
13 changed files with 446 additions and 361 deletions

View File

@ -33,7 +33,7 @@ export class Database {
private dataSource: DataSource; private dataSource: DataSource;
private projectDomain: string; private projectDomain: string;
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) { constructor({ dbPath }: DatabaseConfig, { projectDomain }: MiscConfig) {
this.dataSource = new DataSource({ this.dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',
database: dbPath, database: dbPath,
@ -45,7 +45,7 @@ export class Database {
this.projectDomain = projectDomain; this.projectDomain = projectDomain;
} }
async init (): Promise<void> { async init(): Promise<void> {
await this.dataSource.initialize(); await this.dataSource.initialize();
log('database initialized'); log('database initialized');
@ -58,21 +58,21 @@ export class Database {
} }
} }
async getUser (options: FindOneOptions<User>): Promise<User | null> { async getUser(options: FindOneOptions<User>): Promise<User | null> {
const userRepository = this.dataSource.getRepository(User); const userRepository = this.dataSource.getRepository(User);
const user = await userRepository.findOne(options); const user = await userRepository.findOne(options);
return user; return user;
} }
async addUser (data: DeepPartial<User>): Promise<User> { async addUser(data: DeepPartial<User>): Promise<User> {
const userRepository = this.dataSource.getRepository(User); const userRepository = this.dataSource.getRepository(User);
const user = await userRepository.save(data); const user = await userRepository.save(data);
return user; return user;
} }
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> { async updateUser(user: User, data: DeepPartial<User>): Promise<boolean> {
const userRepository = this.dataSource.getRepository(User); const userRepository = this.dataSource.getRepository(User);
const updateResult = await userRepository.update({ id: user.id }, data); const updateResult = await userRepository.update({ id: user.id }, data);
assert(updateResult.affected); assert(updateResult.affected);
@ -80,7 +80,7 @@ export class Database {
return updateResult.affected > 0; return updateResult.affected > 0;
} }
async getOrganizations ( async getOrganizations(
options: FindManyOptions<Organization> options: FindManyOptions<Organization>
): Promise<Organization[]> { ): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
@ -89,7 +89,7 @@ export class Database {
return organizations; return organizations;
} }
async getOrganization ( async getOrganization(
options: FindOneOptions<Organization> options: FindOneOptions<Organization>
): Promise<Organization | null> { ): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
@ -98,7 +98,7 @@ export class Database {
return organization; return organization;
} }
async getOrganizationsByUserId (userId: string): Promise<Organization[]> { async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
const userOrgs = await organizationRepository.find({ const userOrgs = await organizationRepository.find({
@ -114,21 +114,21 @@ export class Database {
return userOrgs; return userOrgs;
} }
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> { async addUserOrganization(data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization); const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
const newUserOrganization = await userOrganizationRepository.save(data); const newUserOrganization = await userOrganizationRepository.save(data);
return newUserOrganization; return newUserOrganization;
} }
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> { async getProjects(options: FindManyOptions<Project>): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository.find(options); const projects = await projectRepository.find(options);
return projects; return projects;
} }
async getProjectById (projectId: string): Promise<Project | null> { async getProjectById(projectId: string): Promise<Project | null> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const project = await projectRepository const project = await projectRepository
@ -150,7 +150,20 @@ export class Database {
return project; return project;
} }
async getProjectsInOrganization ( async allProjectsWithoutDeployments(): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository
.createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployment', 'deployment.deletedAt IS NULL') // Join only non-soft-deleted deployments
.where('deployment.id IS NULL') // Get projects where no deployments are present
.andWhere('project.auctionId IS NOT NULL') // Ensure auctionId is not null
.getMany();
return projects;
}
async getProjectsInOrganization(
userId: string, userId: string,
organizationSlug: string organizationSlug: string
): Promise<Project[]> { ): Promise<Project[]> {
@ -181,7 +194,7 @@ export class Database {
/** /**
* Get deployments with specified filter * Get deployments with specified filter
*/ */
async getDeployments ( async getDeployments(
options: FindManyOptions<Deployment> options: FindManyOptions<Deployment>
): Promise<Deployment[]> { ): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
@ -190,7 +203,7 @@ export class Database {
return deployments; return deployments;
} }
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> { async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
return this.getDeployments({ return this.getDeployments({
relations: { relations: {
project: true, project: true,
@ -208,7 +221,7 @@ export class Database {
}); });
} }
async getDeployment ( async getDeployment(
options: FindOneOptions<Deployment> options: FindOneOptions<Deployment>
): Promise<Deployment | null> { ): Promise<Deployment | null> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
@ -217,14 +230,14 @@ export class Database {
return deployment; return deployment;
} }
async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> { async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find(options); const domains = await domainRepository.find(options);
return domains; return domains;
} }
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> { async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const id = nanoid(); const id = nanoid();
@ -238,7 +251,7 @@ export class Database {
return deployment; return deployment;
} }
async getProjectMembersByProjectId ( async getProjectMembersByProjectId(
projectId: string projectId: string
): Promise<ProjectMember[]> { ): Promise<ProjectMember[]> {
const projectMemberRepository = const projectMemberRepository =
@ -259,7 +272,7 @@ export class Database {
return projectMembers; return projectMembers;
} }
async getEnvironmentVariablesByProjectId ( async getEnvironmentVariablesByProjectId(
projectId: string, projectId: string,
filter?: FindOptionsWhere<EnvironmentVariable> filter?: FindOptionsWhere<EnvironmentVariable>
): Promise<EnvironmentVariable[]> { ): Promise<EnvironmentVariable[]> {
@ -278,7 +291,7 @@ export class Database {
return environmentVariables; return environmentVariables;
} }
async removeProjectMemberById (projectMemberId: string): Promise<boolean> { async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
const projectMemberRepository = const projectMemberRepository =
this.dataSource.getRepository(ProjectMember); this.dataSource.getRepository(ProjectMember);
@ -293,7 +306,7 @@ export class Database {
} }
} }
async updateProjectMemberById ( async updateProjectMemberById(
projectMemberId: string, projectMemberId: string,
data: DeepPartial<ProjectMember> data: DeepPartial<ProjectMember>
): Promise<boolean> { ): Promise<boolean> {
@ -307,7 +320,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProjectMember ( async addProjectMember(
data: DeepPartial<ProjectMember> data: DeepPartial<ProjectMember>
): Promise<ProjectMember> { ): Promise<ProjectMember> {
const projectMemberRepository = const projectMemberRepository =
@ -317,7 +330,7 @@ export class Database {
return newProjectMember; return newProjectMember;
} }
async addEnvironmentVariables ( async addEnvironmentVariables(
data: DeepPartial<EnvironmentVariable>[] data: DeepPartial<EnvironmentVariable>[]
): Promise<EnvironmentVariable[]> { ): Promise<EnvironmentVariable[]> {
const environmentVariableRepository = const environmentVariableRepository =
@ -328,7 +341,7 @@ export class Database {
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable ( async updateEnvironmentVariable(
environmentVariableId: string, environmentVariableId: string,
data: DeepPartial<EnvironmentVariable> data: DeepPartial<EnvironmentVariable>
): Promise<boolean> { ): Promise<boolean> {
@ -342,7 +355,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteEnvironmentVariable ( async deleteEnvironmentVariable(
environmentVariableId: string environmentVariableId: string
): Promise<boolean> { ): Promise<boolean> {
const environmentVariableRepository = const environmentVariableRepository =
@ -358,7 +371,7 @@ export class Database {
} }
} }
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> { async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = const projectMemberRepository =
this.dataSource.getRepository(ProjectMember); this.dataSource.getRepository(ProjectMember);
@ -381,7 +394,7 @@ export class Database {
return projectMemberWithProject[0]; return projectMemberWithProject[0];
} }
async getProjectsBySearchText ( async getProjectsBySearchText(
userId: string, userId: string,
searchText: string searchText: string
): Promise<Project[]> { ): Promise<Project[]> {
@ -403,14 +416,14 @@ export class Database {
return projects; return projects;
} }
async updateDeploymentById ( async updateDeploymentById(
deploymentId: string, deploymentId: string,
data: DeepPartial<Deployment> data: DeepPartial<Deployment>
): Promise<boolean> { ): Promise<boolean> {
return this.updateDeployment({ id: deploymentId }, data); return this.updateDeployment({ id: deploymentId }, data);
} }
async updateDeployment ( async updateDeployment(
criteria: FindOptionsWhere<Deployment>, criteria: FindOptionsWhere<Deployment>,
data: DeepPartial<Deployment> data: DeepPartial<Deployment>
): Promise<boolean> { ): Promise<boolean> {
@ -420,7 +433,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async updateDeploymentsByProjectIds ( async updateDeploymentsByProjectIds(
projectIds: string[], projectIds: string[],
data: DeepPartial<Deployment> data: DeepPartial<Deployment>
): Promise<boolean> { ): Promise<boolean> {
@ -436,7 +449,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteDeploymentById (deploymentId: string): Promise<boolean> { async deleteDeploymentById(deploymentId: string): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOneOrFail({ const deployment = await deploymentRepository.findOneOrFail({
where: { where: {
@ -449,7 +462,7 @@ export class Database {
return Boolean(deleteResult); return Boolean(deleteResult);
} }
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> { async addProject(user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
// TODO: Check if organization exists // TODO: Check if organization exists
@ -468,7 +481,7 @@ export class Database {
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
async updateProjectById ( async updateProjectById(
projectId: string, projectId: string,
data: DeepPartial<Project> data: DeepPartial<Project>
): Promise<boolean> { ): Promise<boolean> {
@ -481,7 +494,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteProjectById (projectId: string): Promise<boolean> { async deleteProjectById(projectId: string): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const project = await projectRepository.findOneOrFail({ const project = await projectRepository.findOneOrFail({
where: { where: {
@ -497,7 +510,7 @@ export class Database {
return Boolean(deleteResult); return Boolean(deleteResult);
} }
async deleteDomainById (domainId: string): Promise<boolean> { async deleteDomainById(domainId: string): Promise<boolean> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const deleteResult = await domainRepository.softDelete({ id: domainId }); const deleteResult = await domainRepository.softDelete({ id: domainId });
@ -509,21 +522,21 @@ export class Database {
} }
} }
async addDomain (data: DeepPartial<Domain>): Promise<Domain> { async addDomain(data: DeepPartial<Domain>): Promise<Domain> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const newDomain = await domainRepository.save(data); const newDomain = await domainRepository.save(data);
return newDomain; return newDomain;
} }
async getDomain (options: FindOneOptions<Domain>): Promise<Domain | null> { async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const domain = await domainRepository.findOne(options); const domain = await domainRepository.findOne(options);
return domain; return domain;
} }
async updateDomainById ( async updateDomainById(
domainId: string, domainId: string,
data: DeepPartial<Domain> data: DeepPartial<Domain>
): Promise<boolean> { ): Promise<boolean> {
@ -533,7 +546,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async getDomainsByProjectId ( async getDomainsByProjectId(
projectId: string, projectId: string,
filter?: FindOptionsWhere<Domain> filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> { ): Promise<Domain[]> {

View File

@ -416,14 +416,8 @@ export class Registry {
}; };
} }
async getCompletedAuctionIds(auctionIds: (string | null | undefined)[]): Promise<string[] | null> { async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
const validAuctionIds = auctionIds.filter((id): id is string => id !== null && id !== undefined); const auctions = await this.registry.getAuctionsByIds(auctionIds);
if (!validAuctionIds.length) {
return null;
}
const auctions = await this.registry.getAuctionsByIds(validAuctionIds);
const completedAuctions = auctions const completedAuctions = auctions
.filter((auction: { id: string, status: string }) => auction.status === 'completed') .filter((auction: { id: string, status: string }) => auction.status === 'completed')

View File

@ -6,7 +6,7 @@ import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { AddProjectFromTemplateInput, AuctionParams } from './types'; import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types';
const log = debug('snowball:resolver'); const log = debug('snowball:resolver');
@ -211,8 +211,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
organizationSlug, organizationSlug,
data, data,
lrn, lrn,
auctionParams auctionParams,
}: { organizationSlug: string; data: AddProjectFromTemplateInput; lrn: string; auctionParams: AuctionParams }, environmentVariables
}: {
organizationSlug: string;
data: AddProjectFromTemplateInput;
lrn: string;
auctionParams: AuctionParams,
environmentVariables: EnvironmentVariables[];
},
context: any, context: any,
) => { ) => {
try { try {
@ -221,7 +228,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
organizationSlug, organizationSlug,
data, data,
lrn, lrn,
auctionParams auctionParams,
environmentVariables
); );
} catch (err) { } catch (err) {
log(err); log(err);
@ -235,12 +243,26 @@ export const createResolvers = async (service: Service): Promise<any> => {
organizationSlug, organizationSlug,
data, data,
lrn, lrn,
auctionParams auctionParams,
}: { organizationSlug: string; data: DeepPartial<Project>; lrn: string; auctionParams: AuctionParams }, environmentVariables
}: {
organizationSlug: string;
data: DeepPartial<Project>;
lrn: string;
auctionParams: AuctionParams,
environmentVariables: EnvironmentVariables[];
},
context: any, context: any,
) => { ) => {
try { try {
return await service.addProject(context.user, organizationSlug, data, lrn, auctionParams); return await service.addProject(
context.user,
organizationSlug,
data,
lrn,
auctionParams,
environmentVariables
);
} catch (err) { } catch (err) {
log(err); log(err);
throw err; throw err;

View File

@ -271,12 +271,14 @@ type Mutation {
data: AddProjectFromTemplateInput data: AddProjectFromTemplateInput
lrn: String lrn: String
auctionParams: AuctionParams auctionParams: AuctionParams
environmentVariables: [AddEnvironmentVariableInput!]
): Project! ): Project!
addProject( addProject(
organizationSlug: String! organizationSlug: String!
data: AddProjectInput! data: AddProjectInput!
lrn: String lrn: String
auctionParams: AuctionParams auctionParams: AuctionParams
environmentVariables: [AddEnvironmentVariableInput!]
): Project! ): Project!
updateProject(projectId: String!, data: UpdateProjectInput): Boolean! updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
redeployToProd(deploymentId: String!): Boolean! redeployToProd(deploymentId: String!): Boolean!

View File

@ -20,6 +20,7 @@ import {
AppDeploymentRecord, AppDeploymentRecord,
AppDeploymentRemovalRecord, AppDeploymentRemovalRecord,
AuctionParams, AuctionParams,
EnvironmentVariables,
GitPushEventPayload, GitPushEventPayload,
} from './types'; } from './types';
import { Role } from './entity/UserOrganization'; import { Role } from './entity/UserOrganization';
@ -167,7 +168,7 @@ export class Service {
async updateDeploymentsWithRecordData( async updateDeploymentsWithRecordData(
records: AppDeploymentRecord[], records: AppDeploymentRecord[],
): Promise<void> { ): Promise<void> {
// get and update deployments to be updated using request id // Fetch the deployments to be updated using deployment requestId
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: records.map((record) => ({ where: records.map((record) => ({
applicationDeploymentRequestId: record.attributes.request, applicationDeploymentRequestId: record.attributes.request,
@ -220,10 +221,10 @@ export class Service {
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
// if iscurrent is true for this deployment then update the old ones // Get deployments that are in production environment
const prodDeployments = Object.values(recordToDeploymentsMap).filter(deployment => deployment.isCurrent); const prodDeployments = Object.values(recordToDeploymentsMap).filter(deployment => deployment.isCurrent);
// Get deployment IDs of deployments that are in production environment // Set the isCurrent state to false for the old deployments
for (const deployment of prodDeployments) { for (const deployment of prodDeployments) {
const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId); const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId);
const oldDeployments = projectDeployments const oldDeployments = projectDeployments
@ -236,15 +237,6 @@ export class Service {
} }
} }
// Get old deployments for ApplicationDeploymentRecords
// flter out deps with is current false
// loop over these deps
// get the project
// get all the deployemnts in that proj with the same deployer lrn (query filter not above updated dep)
// set is current to false
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
} }
@ -298,44 +290,30 @@ export class Service {
* Calls the createDeploymentFromAuction method for deployments with completed auctions * Calls the createDeploymentFromAuction method for deployments with completed auctions
*/ */
async checkAuctionStatus(): Promise<void> { async checkAuctionStatus(): Promise<void> {
const allProjects = await this.db.getProjects({ const projects = await this.db.allProjectsWithoutDeployments();
where: {
auctionId: Not(IsNull()),
},
relations: ['deployments'],
withDeleted: true,
});
// Should only check on the first deployment const validAuctionIds = projects.map((project) => project.auctionId!)
const projects = allProjects.filter(project => { .filter((id): id is string => Boolean(id));
if (project.deletedAt !== null) return false; const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds);
return project.deployments.length === 0; const projectsToBedeployed = projects.filter((project) =>
}); completedAuctionIds.includes(project.auctionId!)
);
const auctionIds = projects.map((project) => project.auctionId); for (const project of projectsToBedeployed) {
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(auctionIds); const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!);
if (completedAuctionIds) { if (!deployerLrns) {
const projectsToBedeployed = projects.filter((project) => log(`No winning deployer for auction ${project!.auctionId}`);
completedAuctionIds.includes(project.auctionId!) } else {
); // Update project with deployer LRNs
await this.db.updateProjectById(project.id!, {
deployerLrns
});
for (const project of projectsToBedeployed) { for (const deployer of deployerLrns) {
const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!); log(`Creating deployment for deployer LRN ${deployer}`);
await this.createDeploymentFromAuction(project, deployer);
if (!deployerLrns) {
log(`No winning deployer for auction ${project!.auctionId}`);
} else {
// Update project with deployer LRNs
await this.db.updateProjectById(project.id!, {
deployerLrns
});
for (const deployer of deployerLrns) {
log(`Creating deployment for deployer LRN ${deployer}`);
await this.createDeploymentFromAuction(project, deployer);
}
} }
} }
} }
@ -785,7 +763,8 @@ export class Service {
organizationSlug: string, organizationSlug: string,
data: AddProjectFromTemplateInput, data: AddProjectFromTemplateInput,
lrn?: string, lrn?: string,
auctionParams?: AuctionParams auctionParams?: AuctionParams,
environmentVariables?: EnvironmentVariables[],
): Promise<Project | undefined> { ): Promise<Project | undefined> {
try { try {
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
@ -816,7 +795,7 @@ export class Service {
repository: gitRepo.data.full_name, repository: gitRepo.data.full_name,
// TODO: Set selected template // TODO: Set selected template
template: 'webapp', template: 'webapp',
}, lrn, auctionParams); }, lrn, auctionParams, environmentVariables);
if (!project || !project.id) { if (!project || !project.id) {
throw new Error('Failed to create project from template'); throw new Error('Failed to create project from template');
@ -834,7 +813,8 @@ export class Service {
organizationSlug: string, organizationSlug: string,
data: DeepPartial<Project>, data: DeepPartial<Project>,
lrn?: string, lrn?: string,
auctionParams?: AuctionParams auctionParams?: AuctionParams,
environmentVariables?: EnvironmentVariables[],
): Promise<Project | undefined> { ): Promise<Project | undefined> {
const organization = await this.db.getOrganization({ const organization = await this.db.getOrganization({
where: { where: {
@ -846,7 +826,10 @@ export class Service {
} }
const project = await this.db.addProject(user, organization.id, data); const project = await this.db.addProject(user, organization.id, data);
log(`Project created ${project.id}`);
if (environmentVariables) {
await this.addEnvironmentVariables(project.id, environmentVariables);
}
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
const [owner, repo] = project.repository.split('/'); const [owner, repo] = project.repository.split('/');

View File

@ -76,3 +76,9 @@ export interface AuctionParams {
maxPrice: string, maxPrice: string,
numProviders: number, numProviders: number,
} }
export interface EnvironmentVariables {
environments: string[],
key: string,
value: string,
}

View File

@ -1,8 +1,9 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { FormProvider, FieldValues } from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
import { AuctionParams } from 'gql-client'; import { AddEnvironmentVariableInput, AuctionParams } from 'gql-client';
import { import {
ArrowRightCircleFilledIcon, ArrowRightCircleFilledIcon,
@ -14,15 +15,20 @@ import { Select, SelectOption } from 'components/shared/Select';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types';
type ConfigureFormValues = { type ConfigureDeploymentFormValues = {
option: string; option: string;
lrn?: string; lrn?: string;
numProviders?: number; numProviders?: number;
maxPrice?: string; maxPrice?: string;
}; };
type ConfigureFormValues = ConfigureDeploymentFormValues & EnvironmentVariablesFormValues;
const Configure = () => { const Configure = () => {
const [isLoading, setIsLoading] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId'); const templateId = searchParams.get('templateId');
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
@ -40,96 +46,147 @@ const Configure = () => {
const { toast, dismiss } = useToast(); const { toast, dismiss } = useToast();
const client = useGQLClient(); const client = useGQLClient();
const [isLoading, setIsLoading] = useState(false); const methods = useForm<ConfigureFormValues>({
const { handleSubmit, control, watch } = useForm<ConfigureFormValues>({
defaultValues: { option: 'LRN' }, defaultValues: { option: 'LRN' },
}); });
const selectedOption = watch('option'); const selectedOption = methods.watch('option');
const isTabletView = useMediaQuery('(min-width: 720px)'); // md: const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
const buttonSize = isTabletView ? { size: 'lg' as const } : {}; const buttonSize = isTabletView ? { size: 'lg' as const } : {};
const onSubmit: SubmitHandler<ConfigureFormValues> = useCallback( const createProject = async (data: FieldValues, envVariables: AddEnvironmentVariableInput[]): Promise<string> => {
async (data) => { setIsLoading(true);
setIsLoading(true); let projectId: string | null = null;
try { try {
let lrn: string | undefined; let lrn: string | undefined;
let auctionParams: AuctionParams | undefined; let auctionParams: AuctionParams | undefined;
if (data.option === 'LRN') { if (data.option === 'LRN') {
lrn = data.lrn; lrn = data.lrn;
} else if (data.option === 'Auction') { } else if (data.option === 'Auction') {
auctionParams = { auctionParams = {
numProviders: Number(data.numProviders!), numProviders: Number(data.numProviders!),
maxPrice: (data.maxPrice!).toString(), maxPrice: (data.maxPrice!).toString(),
}; };
} }
if (templateId) { if (templateId) {
// Template-based project creation const projectData: any = {
const projectData: any = { templateOwner,
templateOwner, templateRepo,
templateRepo, owner,
owner, name,
name, isPrivate,
isPrivate, };
};
const { addProjectFromTemplate } = await client.addProjectFromTemplate( const { addProjectFromTemplate } = await client.addProjectFromTemplate(
orgSlug!, orgSlug!,
projectData, projectData,
lrn, lrn,
auctionParams auctionParams,
); envVariables
);
data.option === 'Auction' projectId = addProjectFromTemplate.id;
? navigate( } else {
`/${orgSlug}/projects/create/success/${addProjectFromTemplate.id}?isAuction=true`, const { addProject } = await client.addProject(
) orgSlug!,
: navigate( {
`/${orgSlug}/projects/create/template/deploy?projectId=${addProjectFromTemplate.id}&templateId=${templateId}` name: fullName!,
); prodBranch: defaultBranch!,
} else { repository: fullName!,
const { addProject } = await client.addProject( template: 'webapp',
orgSlug!, },
{ lrn,
name: fullName!, auctionParams,
prodBranch: defaultBranch!, envVariables
repository: fullName!, );
template: 'webapp',
},
lrn,
auctionParams
);
data.option === 'Auction' projectId = addProject.id;
? navigate( }
`/${orgSlug}/projects/create/success/${addProject.id}?isAuction=true` } catch (error) {
) console.error('Error creating project:', error);
: navigate( toast({
`/${orgSlug}/projects/create/deploy?projectId=${addProject.id}` id: 'error-creating-project',
); title: 'Error creating project',
} variant: 'error',
} catch (error) { onDismiss: dismiss,
console.error('Error creating project:', error); });
} finally {
setIsLoading(false);
}
if (projectId) {
return projectId;
} else {
throw new Error('Project creation failed');
}
};
const handleFormSubmit = useCallback(
async (createFormData: FieldValues) => {
const environmentVariables = createFormData.variables.map((variable: any) => {
return {
key: variable.key,
value: variable.value,
environments: Object.entries(createFormData.environment)
.filter(([, value]) => value === true)
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
};
});
const projectId = await createProject(createFormData, environmentVariables);
const { environmentVariables: isEnvironmentVariablesAdded } =
await client.getEnvironmentVariables(projectId);
if (isEnvironmentVariablesAdded.length > 0) {
toast({ toast({
id: 'error-creating-project', id:
title: 'Error creating project', createFormData.variables.length > 1
? 'env_variable_added'
: 'env_variables_added',
title:
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
variant: 'success',
onDismiss: dismiss,
});
} else {
toast({
id: 'env_variables_not_added',
title: 'Environment variables not added',
variant: 'error', variant: 'error',
onDismiss: dismiss, onDismiss: dismiss,
}); });
} finally { }
setIsLoading(false); if (templateId) {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
)
: navigate(
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`
);
} else {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`
)
: navigate(
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`
);
} }
}, },
[client, isPrivate, templateId, navigate, dismiss, toast] [client, createProject, dismiss, toast]
); );
return ( return (
<div className="space-y-7"> <div className="space-y-7 px-4 py-6">
<div className="flex justify-between"> <div className="flex justify-between mb-6">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Heading as="h4" className="md:text-lg font-medium"> <Heading as="h4" className="md:text-lg font-medium">
Configure deployment Configure deployment
@ -142,99 +199,111 @@ const Configure = () => {
</div> </div>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)}> <div className="flex flex-col gap-6 lg:gap-8 w-full">
<div className="flex flex-col gap-4 lg:gap-7 w-full"> <FormProvider {...methods}>
<div className="flex flex-col justify-start gap-3"> <form onSubmit={methods.handleSubmit(handleFormSubmit)}>
<Controller <div className="flex flex-col justify-start gap-4 mb-6">
name="option"
control={control}
render={({ field: { value, onChange } }) => (
<Select
label="Configuration Options"
value={
{
value: value || 'LRN',
label: value === 'Auction' ? 'Create Auction' : 'Deployer LRN',
} as SelectOption
}
onChange={(value) => onChange((value as SelectOption).value)}
options={[
{ value: 'LRN', label: 'Deployer LRN' },
{ value: 'Auction', label: 'Create Auction' },
]}
/>
)}
/>
</div>
{selectedOption === 'LRN' && (
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
The app will be deployed by the configured deployer
</Heading>
<span className="text-sm text-elements-high-em">
Enter LRN for deployer
</span>
<Controller <Controller
name="lrn" name="option"
control={control} control={methods.control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Input value={value} onChange={onChange} /> <Select
label="Configuration Options"
value={
{
value: value || 'LRN',
label: value === 'Auction' ? 'Create Auction' : 'Deployer LRN',
} as SelectOption
}
onChange={(value) => onChange((value as SelectOption).value)}
options={[
{ value: 'LRN', label: 'Deployer LRN' },
{ value: 'Auction', label: 'Create Auction' },
]}
/>
)} )}
/> />
</div> </div>
)}
{selectedOption === 'Auction' && ( {selectedOption === 'LRN' && (
<> <div className="flex flex-col justify-start gap-4 mb-6">
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em"> <Heading as="h5" className="text-sm font-sans text-elements-low-em">
Set the number of deployers and maximum price for each deployment The app will be deployed by the configured deployer
</Heading> </Heading>
<span className="text-sm text-elements-high-em"> <span className="text-sm text-elements-high-em">
Number of Deployers Enter LRN for deployer
</span> </span>
<Controller <Controller
name="numProviders" name="lrn"
control={control} control={methods.control}
rules={{ required: true }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} /> <Input value={value} onChange={onChange} />
)} )}
/> />
</div> </div>
<div className="flex flex-col justify-start gap-3"> )}
<span className="text-sm text-elements-high-em">
Maximum Price (alnt)
</span>
<Controller
name="maxPrice"
control={control}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
</>
)}
<div> {selectedOption === 'Auction' && (
<Button <>
{...buttonSize} <div className="flex flex-col justify-start gap-4 mb-6">
type="submit" <Heading as="h5" className="text-sm font-sans text-elements-low-em">
disabled={isLoading} Set the number of deployers and maximum price for each deployment
rightIcon={ </Heading>
isLoading ? ( <span className="text-sm text-elements-high-em">
<LoadingIcon className="animate-spin" /> Number of Deployers
) : ( </span>
<ArrowRightCircleFilledIcon /> <Controller
) name="numProviders"
} control={methods.control}
> rules={{ required: true }}
{isLoading ? 'Deploying repo' : 'Deploy repo'} render={({ field: { value, onChange } }) => (
</Button> <Input type="number" value={value} onChange={onChange} />
</div> )}
</div> />
</form> </div>
<div className="flex flex-col justify-start gap-4 mb-6">
<span className="text-sm text-elements-high-em">
Maximum Price (alnt)
</span>
<Controller
name="maxPrice"
control={methods.control}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
</>
)}
<Heading as="h4" className="md:text-lg font-medium mb-3">
Environment Variables
</Heading>
<div className="p-4 bg-slate-100 rounded-lg mb-6">
<EnvironmentVariablesForm />
</div>
<div>
<Button
{...buttonSize}
type="submit"
disabled={isLoading}
rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
>
{isLoading ? 'Deploying repo' : 'Deploy repo'}
</Button>
</div>
</form>
</FormProvider>
</div>
</div> </div>
); );
}; };

View File

@ -64,9 +64,9 @@ export const RepositoryList = () => {
// Check if selected account is an organization // Check if selected account is an organization
if (selectedAccount.value === gitUser.login) { if (selectedAccount.value === gitUser.login) {
query = query + ` user:${selectedAccount}`; query = query + ` user:${selectedAccount.value}`;
} else { } else {
query = query + ` org:${selectedAccount}`; query = query + ` org:${selectedAccount.value}`;
} }
const result = await octokit.rest.search.repos({ const result = await octokit.rest.search.repos({

View File

@ -163,6 +163,7 @@ const CreateRepo = () => {
<Controller <Controller
name="repoName" name="repoName"
control={control} control={control}
rules={{ required: true }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Input value={value} onChange={onChange} /> <Input value={value} onChange={onChange} />
)} )}
@ -172,7 +173,7 @@ const CreateRepo = () => {
<Controller <Controller
name="isPrivate" name="isPrivate"
control={control} control={control}
render={({}) => ( render={({ }) => (
<Checkbox label="Make this repo private" disabled={true} /> <Checkbox label="Make this repo private" disabled={true} />
)} )}
/> />

View File

@ -1,22 +1,20 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Collapse, Checkbox } from '@snowballtools/material-tailwind-react-fork'; import { Collapse } from '@snowballtools/material-tailwind-react-fork';
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables'; import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { EnvironmentVariablesFormValues } from '../../../../../types'; import { EnvironmentVariablesFormValues } from '../../../../../types';
import HorizontalLine from 'components/HorizontalLine'; import HorizontalLine from 'components/HorizontalLine';
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
// import { Checkbox } from 'components/shared/Checkbox';
import { PlusIcon } from 'components/shared/CustomIcon'; import { PlusIcon } from 'components/shared/CustomIcon';
import { InlineNotification } from 'components/shared/InlineNotification';
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer'; import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { Environment, EnvironmentVariable } from 'gql-client'; import { Environment, EnvironmentVariable } from 'gql-client';
import EnvironmentVariablesForm from './EnvironmentVariablesForm';
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
import { Button } from 'components/shared';
export const EnvironmentVariablesTabPanel = () => { export const EnvironmentVariablesTabPanel = () => {
const { id } = useParams(); const { id } = useParams();
@ -27,13 +25,9 @@ export const EnvironmentVariablesTabPanel = () => {
EnvironmentVariable[] EnvironmentVariable[]
>([]); >([]);
const { const [createNewVariable, setCreateNewVariable] = useState(false);
handleSubmit,
register, const methods = useForm<EnvironmentVariablesFormValues>({
control,
reset,
formState: { isSubmitSuccessful, errors },
} = useForm<EnvironmentVariablesFormValues>({
defaultValues: { defaultValues: {
variables: [{ key: '', value: '' }], variables: [{ key: '', value: '' }],
environment: { environment: {
@ -43,21 +37,6 @@ export const EnvironmentVariablesTabPanel = () => {
}, },
}, },
}); });
const [createNewVariable, setCreateNewVariable] = useState(false);
const { fields, append, remove } = useFieldArray({
name: 'variables',
control,
rules: {
required: 'Add at least 1 environment variables',
},
});
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
}, [isSubmitSuccessful, reset, id]);
const getEnvironmentVariables = useCallback( const getEnvironmentVariables = useCallback(
(environment: Environment) => { (environment: Environment) => {
@ -68,21 +47,6 @@ export const EnvironmentVariablesTabPanel = () => {
[environmentVariables, id], [environmentVariables, id],
); );
const isFieldEmpty = useMemo(() => {
if (errors.variables) {
return fields.some((_, index) => {
if (
errors.variables![index]?.value?.type === 'required' ||
errors.variables![index]?.key?.type === 'required'
) {
return true;
}
});
}
return false;
}, [fields, errors.variables, id]);
const fetchEnvironmentVariables = useCallback( const fetchEnvironmentVariables = useCallback(
async (id: string | undefined) => { async (id: string | undefined) => {
if (id) { if (id) {
@ -99,8 +63,8 @@ export const EnvironmentVariablesTabPanel = () => {
}, [id]); }, [id]);
const createEnvironmentVariablesHandler = useCallback( const createEnvironmentVariablesHandler = useCallback(
async (createFormData: EnvironmentVariablesFormValues) => { async (createFormData: FieldValues) => {
const environmentVariables = createFormData.variables.map((variable) => { const environmentVariables = createFormData.variables.map((variable: any) => {
return { return {
key: variable.key, key: variable.key,
value: variable.value, value: variable.value,
@ -114,7 +78,7 @@ export const EnvironmentVariablesTabPanel = () => {
await client.addEnvironmentVariables(id!, environmentVariables); await client.addEnvironmentVariables(id!, environmentVariables);
if (isEnvironmentVariablesAdded) { if (isEnvironmentVariablesAdded) {
reset(); methods.reset();
setCreateNewVariable((cur) => !cur); setCreateNewVariable((cur) => !cur);
fetchEnvironmentVariables(id); fetchEnvironmentVariables(id);
@ -159,59 +123,10 @@ export const EnvironmentVariablesTabPanel = () => {
</div> </div>
</Heading> </Heading>
<Collapse open={createNewVariable}> <Collapse open={createNewVariable}>
<div className="p-4 bg-slate-100"> <FormProvider {...methods}>
<form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}> <form onSubmit={methods.handleSubmit((data) => createEnvironmentVariablesHandler(data))}>
{fields.map((field, index) => { <div className="p-4 bg-slate-100">
return ( <EnvironmentVariablesForm />
<AddEnvironmentVariableRow
key={field.id}
index={index}
register={register}
onDelete={() => remove(index)}
isDeleteDisabled={fields.length === 1}
/>
);
})}
<div className="flex gap-1 p-2">
<Button
size="md"
onClick={() =>
append({
key: '',
value: '',
})
}
>
+ Add variable
</Button>
{/* TODO: Implement import environment varible functionality */}
<Button size="md" disabled>
Import .env
</Button>
</div>
{isFieldEmpty && (
<InlineNotification
title="Please ensure no fields are empty before saving."
variant="danger"
size="md"
/>
)}
<div className="flex gap-2 p-2">
<Checkbox
label="Production"
{...register(`environment.production`)}
color="blue"
/>
<Checkbox
label="Preview"
{...register(`environment.preview`)}
color="blue"
/>
<Checkbox
label="Development"
{...register(`environment.development`)}
color="blue"
/>
</div> </div>
<div className="p-2"> <div className="p-2">
<Button size="md" type="submit"> <Button size="md" type="submit">
@ -219,7 +134,7 @@ export const EnvironmentVariablesTabPanel = () => {
</Button> </Button>
</div> </div>
</form> </form>
</div> </FormProvider>
</Collapse> </Collapse>
</div> </div>
<div className="p-2"> <div className="p-2">

View File

@ -0,0 +1,76 @@
import { useEffect, useMemo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
// TODO: Use custom checkbox component
import { Checkbox } from '@snowballtools/material-tailwind-react-fork';
import { Button } from 'components/shared/Button';
import { InlineNotification } from 'components/shared/InlineNotification';
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
import { EnvironmentVariablesFormValues } from 'types/types';
const EnvironmentVariablesForm = () => {
const {
register,
control,
reset,
formState: { isSubmitSuccessful, errors },
} = useFormContext<EnvironmentVariablesFormValues>();
const { fields, append, remove } = useFieldArray({
name: 'variables',
control,
});
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
}, [isSubmitSuccessful, reset]);
const isFieldEmpty = useMemo(() => {
if (errors.variables) {
return fields.some((_, index) => {
if (
errors.variables![index]?.value?.type === 'required' ||
errors.variables![index]?.key?.type === 'required'
) {
return true;
}
});
}
return false;
}, [fields, errors.variables]);
return (
<>
{fields.map((field, index) => (
<AddEnvironmentVariableRow
key={field.id}
index={index}
register={register}
onDelete={() => remove(index)}
isDeleteDisabled={fields.length === 0}
/>
))}
<div className="flex gap-1 p-2">
<Button size="md" onClick={() => append({ key: '', value: '' })}>
+ Add variable
</Button>
</div>
{isFieldEmpty && (
<InlineNotification
title="Please ensure no fields are empty before saving."
variant="danger"
/>
)}
<div className="flex gap-2 p-2">
<Checkbox label="Production" {...register('environment.production')} />
<Checkbox label="Preview" {...register('environment.preview')} />
<Checkbox label="Development" {...register('environment.development')} />
</div>
</>
);
};
export default EnvironmentVariablesForm;

View File

@ -233,6 +233,7 @@ export class GQLClient {
data: types.AddProjectFromTemplateInput, data: types.AddProjectFromTemplateInput,
lrn?: string, lrn?: string,
auctionParams?: types.AuctionParams, auctionParams?: types.AuctionParams,
environmentVariables?: types.AddEnvironmentVariableInput[]
): Promise<types.AddProjectFromTemplateResponse> { ): Promise<types.AddProjectFromTemplateResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addProjectFromTemplate, mutation: mutations.addProjectFromTemplate,
@ -240,7 +241,8 @@ export class GQLClient {
organizationSlug, organizationSlug,
data, data,
lrn, lrn,
auctionParams auctionParams,
environmentVariables
}, },
}); });
@ -252,6 +254,7 @@ export class GQLClient {
data: types.AddProjectInput, data: types.AddProjectInput,
lrn?: string, lrn?: string,
auctionParams?: types.AuctionParams, auctionParams?: types.AuctionParams,
environmentVariables?: types.AddEnvironmentVariableInput[]
): Promise<types.AddProjectResponse> { ): Promise<types.AddProjectResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addProject, mutation: mutations.addProject,
@ -259,7 +262,8 @@ export class GQLClient {
organizationSlug, organizationSlug,
data, data,
lrn, lrn,
auctionParams auctionParams,
environmentVariables
}, },
}); });

View File

@ -49,16 +49,16 @@ export const updateDeploymentToProd = gql`
`; `;
export const addProjectFromTemplate = gql` export const addProjectFromTemplate = gql`
mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput, $lrn: String, $auctionParams: AuctionParams) { mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput, $lrn: String, $auctionParams: AuctionParams, $environmentVariables: [AddEnvironmentVariableInput!]) {
addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams) { addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams, environmentVariables: $environmentVariables) {
id id
} }
} }
`; `;
export const addProject = gql` export const addProject = gql`
mutation ($organizationSlug: String!, $data: AddProjectInput!, $lrn: String, $auctionParams: AuctionParams) { mutation ($organizationSlug: String!, $data: AddProjectInput!, $lrn: String, $auctionParams: AuctionParams, $environmentVariables: [AddEnvironmentVariableInput!]) {
addProject(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams) { addProject(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams, environmentVariables: $environmentVariables) {
id id
} }
} }