Merge branch 'andrehadianto/design-system-components' into andrehadianto/T-4869-toast

This commit is contained in:
Andre Hadianto 2024-02-22 23:42:47 +08:00 committed by GitHub
commit 42bf1534fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1905 additions and 508 deletions

26
build-webapp.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
PKG_DIR="./packages/frontend"
OUTPUT_DIR="${PKG_DIR}/build"
DEST_DIR=${1:-/data}
if [[ -d "$DEST_DIR" ]]; then
echo "${DEST_DIR} already exists." 1>&2
exit 1
fi
cat > $PKG_DIR/.env <<EOF
REACT_APP_GQL_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_gql_url'
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
EOF
yarn || exit 1
yarn build || exit 1
if [[ ! -d "$OUTPUT_DIR" ]]; then
echo "Missing output directory: $OUTPUT_DIR" 1>&2
exit 1
fi
mv "$OUTPUT_DIR" "$DEST_DIR"

View File

@ -25,6 +25,9 @@
"allowArgumentsExplicitlyTypedAsAny": true "allowArgumentsExplicitlyTypedAsAny": true
} }
], ],
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] "@typescript-eslint/no-unused-vars": [
"error",
{ "ignoreRestSiblings": true }
]
} }
} }

View File

@ -13,7 +13,7 @@ export interface GitHubConfig {
oAuth: { oAuth: {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
} };
} }
export interface RegistryConfig { export interface RegistryConfig {
@ -27,7 +27,7 @@ export interface RegistryConfig {
amount: string; amount: string;
denom: string; denom: string;
gas: string; gas: string;
} };
} }
export interface Config { export interface Config {

View File

@ -1,8 +1,14 @@
export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml'; import process from 'process';
export const DEFAULT_CONFIG_FILE_PATH =
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
export const DEFAULT_GQL_PATH = '/graphql'; export const DEFAULT_GQL_PATH = '/graphql';
// Note: temporary hardcoded user, later to be derived from auth token // Note: temporary hardcoded user, later to be derived from auth token
export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0'; export const USER_ID =
process.env.SNOWBALL_BACKEND_USER_ID ||
'59f4355d-9549-4aac-9b54-eeefceeabef0';
export const PROJECT_DOMAIN = 'snowball.xyz'; export const PROJECT_DOMAIN =
process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz';

View File

@ -1,4 +1,10 @@
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm'; import {
DataSource,
DeepPartial,
FindManyOptions,
FindOneOptions,
FindOptionsWhere
} from 'typeorm';
import path from 'path'; import path from 'path';
import debug from 'debug'; import debug from 'debug';
import assert from 'assert'; import assert from 'assert';
@ -14,6 +20,12 @@ import { ProjectMember } from './entity/ProjectMember';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { PROJECT_DOMAIN } from './constants'; import { PROJECT_DOMAIN } from './constants';
import { getEntities, loadAndSaveData } from './utils';
import { UserOrganization } from './entity/UserOrganization';
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
const USER_DATA_PATH = '../test/fixtures/users.json';
const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json';
const log = debug('snowball:database'); const log = debug('snowball:database');
@ -36,6 +48,40 @@ export class Database {
async init (): Promise<void> { async init (): Promise<void> {
await this.dataSource.initialize(); await this.dataSource.initialize();
log('database initialized'); log('database initialized');
const organizations = await this.getOrganizations({});
if (!organizations.length) {
const orgEntities = await getEntities(
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
);
const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [
orgEntities[0]
]);
// TODO: Remove user once authenticated
const userEntities = await getEntities(
path.resolve(__dirname, USER_DATA_PATH)
);
const savedUsers = await loadAndSaveData(User, this.dataSource, [
userEntities[0]
]);
const userOrganizationRelations = {
member: savedUsers,
organization: savedOrgs
};
const userOrgEntities = await getEntities(
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
);
await loadAndSaveData(
UserOrganization,
this.dataSource,
[userOrgEntities[0]],
userOrganizationRelations
);
}
} }
async getUser (options: FindOneOptions<User>): Promise<User | null> { async getUser (options: FindOneOptions<User>): Promise<User | null> {
@ -60,7 +106,18 @@ export class Database {
return updateResult.affected > 0; return updateResult.affected > 0;
} }
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> { async getOrganizations (
options: FindManyOptions<Organization>
): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organizations = await organizationRepository.find(options);
return organizations;
}
async getOrganization (
options: FindOneOptions<Organization>
): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options); const organization = await organizationRepository.findOne(options);
@ -95,7 +152,11 @@ export class Database {
const project = await projectRepository const project = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') .leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.createdBy', 'user')
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.owner', 'owner')
@ -108,19 +169,29 @@ export class Database {
return project; return project;
} }
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> { async getProjectsInOrganization (
userId: string,
organizationSlug: string
): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository const projects = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') .leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.projectMembers', 'projectMembers')
.leftJoin('project.organization', 'organization') .leftJoin('project.organization', 'organization')
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', { .where(
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
{
userId, userId,
organizationSlug organizationSlug
}) }
)
.getMany(); .getMany();
return projects; return projects;
@ -129,7 +200,9 @@ export class Database {
/** /**
* Get deployments with specified filter * Get deployments with specified filter
*/ */
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> { async getDeployments (
options: FindManyOptions<Deployment>
): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find(options); const deployments = await deploymentRepository.find(options);
@ -154,7 +227,9 @@ export class Database {
}); });
} }
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> { async getDeployment (
options: FindOneOptions<Deployment>
): Promise<Deployment | null> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOne(options); const deployment = await deploymentRepository.findOne(options);
@ -182,8 +257,11 @@ export class Database {
return deployment; return deployment;
} }
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> { async getProjectMembersByProjectId (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); projectId: string
): Promise<ProjectMember[]> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMembers = await projectMemberRepository.find({ const projectMembers = await projectMemberRepository.find({
relations: { relations: {
@ -200,8 +278,12 @@ export class Database {
return projectMembers; return projectMembers;
} }
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> { async getEnvironmentVariablesByProjectId (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); projectId: string,
filter?: FindOptionsWhere<EnvironmentVariable>
): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const environmentVariables = await environmentVariableRepository.find({ const environmentVariables = await environmentVariableRepository.find({
where: { where: {
@ -216,9 +298,12 @@ export class Database {
} }
async removeProjectMemberById (projectMemberId: string): Promise<boolean> { async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const deleteResult = await projectMemberRepository.delete({ id: projectMemberId }); const deleteResult = await projectMemberRepository.delete({
id: projectMemberId
});
if (deleteResult.affected) { if (deleteResult.affected) {
return deleteResult.affected > 0; return deleteResult.affected > 0;
@ -227,37 +312,63 @@ export class Database {
} }
} }
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> { async updateProjectMemberById (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); projectMemberId: string,
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data); data: DeepPartial<ProjectMember>
): Promise<boolean> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const updateResult = await projectMemberRepository.update(
{ id: projectMemberId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> { async addProjectMember (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); data: DeepPartial<ProjectMember>
): Promise<ProjectMember> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const newProjectMember = await projectMemberRepository.save(data); const newProjectMember = await projectMemberRepository.save(data);
return newProjectMember; return newProjectMember;
} }
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> { async addEnvironmentVariables (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); data: DeepPartial<EnvironmentVariable>[]
const savedEnvironmentVariables = await environmentVariableRepository.save(data); ): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const savedEnvironmentVariables =
await environmentVariableRepository.save(data);
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> { async updateEnvironmentVariable (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); environmentVariableId: string,
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data); data: DeepPartial<EnvironmentVariable>
): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const updateResult = await environmentVariableRepository.update(
{ id: environmentVariableId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> { async deleteEnvironmentVariable (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); environmentVariableId: string
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId }); ): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const deleteResult = await environmentVariableRepository.delete({
id: environmentVariableId
});
if (deleteResult.affected) { if (deleteResult.affected) {
return deleteResult.affected > 0; return deleteResult.affected > 0;
@ -267,7 +378,8 @@ export class Database {
} }
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> { async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMemberWithProject = await projectMemberRepository.find({ const projectMemberWithProject = await projectMemberRepository.find({
relations: { relations: {
@ -279,8 +391,7 @@ export class Database {
where: { where: {
id: projectMemberId id: projectMemberId
} }
} });
);
if (projectMemberWithProject.length === 0) { if (projectMemberWithProject.length === 0) {
throw new Error('Member does not exist'); throw new Error('Member does not exist');
@ -289,34 +400,49 @@ export class Database {
return projectMemberWithProject[0]; return projectMemberWithProject[0];
} }
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> { async getProjectsBySearchText (
userId: string,
searchText: string
): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository const projects = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.organization', 'organization')
.leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.projectMembers', 'projectMembers')
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', { .where(
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
{
userId, userId,
searchText: `%${searchText}%` searchText: `%${searchText}%`
}) }
)
.getMany(); .getMany();
return projects; return projects;
} }
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> { async updateDeploymentById (
deploymentId: string,
data: DeepPartial<Deployment>
): Promise<boolean> {
return this.updateDeployment({ id: deploymentId }, data); return this.updateDeployment({ id: deploymentId }, data);
} }
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> { async updateDeployment (
criteria: FindOptionsWhere<Deployment>,
data: DeepPartial<Deployment>
): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository.update(criteria, data); const updateResult = await deploymentRepository.update(criteria, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> { async updateDeploymentsByProjectIds (
projectIds: string[],
data: DeepPartial<Deployment>
): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository const updateResult = await deploymentRepository
@ -329,7 +455,11 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> { async addProject (
userId: string,
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
@ -352,9 +482,15 @@ export class Database {
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> { async updateProjectById (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const updateResult = await projectRepository.update({ id: projectId }, data); const updateResult = await projectRepository.update(
{ id: projectId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
@ -401,14 +537,20 @@ export class Database {
return domain; return domain;
} }
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> { async updateDomainById (
domainId: string,
data: DeepPartial<Domain>
): Promise<boolean> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const updateResult = await domainRepository.update({ id: domainId }, data); const updateResult = await domainRepository.update({ id: domainId }, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> { async getDomainsByProjectId (
projectId: string,
filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find({ const domains = await domainRepository.find({

View File

@ -26,18 +26,27 @@ export enum DeploymentStatus {
Error = 'Error', Error = 'Error',
} }
export interface ApplicationDeploymentRequest {
type: string;
version: string;
name: string;
application: string;
config: string;
meta: string;
}
export interface ApplicationRecord { export interface ApplicationRecord {
type: string; type: string;
version:string version: string;
name: string name: string;
description?: string description?: string;
homepage?: string homepage?: string;
license?: string license?: string;
author?: string author?: string;
repository?: string[], repository?: string[];
app_version?: string app_version?: string;
repository_ref: string repository_ref: string;
app_type: string app_type: string;
} }
@Entity() @Entity()
@ -78,6 +87,12 @@ export class Deployment {
@Column('simple-json') @Column('simple-json')
applicationRecordData!: ApplicationRecord; applicationRecordData!: ApplicationRecord;
@Column('varchar')
applicationDeploymentRequestId!: string;
@Column('simple-json')
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
applicationDeploymentRecordId!: string | null; applicationDeploymentRecordId!: string | null;

View File

@ -27,8 +27,12 @@ export class Organization {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, { @OneToMany(
() => UserOrganization,
(userOrganization) => userOrganization.organization,
{
cascade: ['soft-remove'] cascade: ['soft-remove']
}) }
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }

View File

@ -15,15 +15,6 @@ import { Organization } from './Organization';
import { ProjectMember } from './ProjectMember'; import { ProjectMember } from './ProjectMember';
import { Deployment } from './Deployment'; import { Deployment } from './Deployment';
export interface ApplicationDeploymentRequest {
type: string
version: string
name: string
application: string
config: string,
meta: string
}
@Entity() @Entity()
export class Project { export class Project {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@ -52,12 +43,6 @@ export class Project {
@Column('varchar', { length: 255, default: 'main' }) @Column('varchar', { length: 255, default: 'main' })
prodBranch!: string; prodBranch!: string;
@Column('varchar', { nullable: true })
applicationDeploymentRequestId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
@Column('text', { default: '' }) @Column('text', { default: '' })
description!: string; description!: string;
@ -91,7 +76,7 @@ export class Project {
@OneToMany(() => Deployment, (deployment) => deployment.project) @OneToMany(() => Deployment, (deployment) => deployment.project)
deployments!: Deployment[]; deployments!: Deployment[];
@OneToMany(() => ProjectMember, projectMember => projectMember.project, { @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove'] cascade: ['soft-remove']
}) })
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];

View File

@ -15,7 +15,7 @@ import { User } from './User';
export enum Permission { export enum Permission {
View = 'View', View = 'View',
Edit = 'Edit' Edit = 'Edit',
} }
@Entity() @Entity()

View File

@ -34,13 +34,17 @@ export class User {
@CreateDateColumn() @CreateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => ProjectMember, projectMember => projectMember.project, { @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove'] cascade: ['soft-remove']
}) })
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, { @OneToMany(
() => UserOrganization,
(UserOrganization) => UserOrganization.member,
{
cascade: ['soft-remove'] cascade: ['soft-remove']
}) }
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }

View File

@ -19,7 +19,9 @@ const OAUTH_CLIENT_TYPE = 'oauth-app';
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
// TODO: get config path using cli // TODO: get config path using cli
const { server, database, gitHub, registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { server, database, gitHub, registryConfig } = await getConfig<Config>(
DEFAULT_CONFIG_FILE_PATH
);
const app = new OAuthApp({ const app = new OAuthApp({
clientType: OAUTH_CLIENT_TYPE, clientType: OAUTH_CLIENT_TYPE,
@ -31,9 +33,16 @@ export const main = async (): Promise<void> => {
await db.init(); await db.init();
const registry = new Registry(registryConfig); const registry = new Registry(registryConfig);
const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry); const service = new Service(
{ gitHubConfig: gitHub, registryConfig },
db,
app,
registry
);
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); const typeDefs = fs
.readFileSync(path.join(__dirname, 'schema.gql'))
.toString();
const resolvers = await createResolvers(service); const resolvers = await createResolvers(service);
await createAndStartServer(server, typeDefs, resolvers, service); await createAndStartServer(server, typeDefs, resolvers, service);

View File

@ -6,8 +6,11 @@ import { DateTime } from 'luxon';
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk'; import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { ApplicationDeploymentRequest } from './entity/Project'; import {
import { ApplicationRecord, Deployment } from './entity/Deployment'; ApplicationRecord,
Deployment,
ApplicationDeploymentRequest
} from './entity/Deployment';
import { AppDeploymentRecord, PackageJSON } from './types'; import { AppDeploymentRecord, PackageJSON } from './types';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
@ -21,35 +24,54 @@ export class Registry {
private registry: LaconicRegistry; private registry: LaconicRegistry;
private registryConfig: RegistryConfig; private registryConfig: RegistryConfig;
constructor (registryConfig : RegistryConfig) { constructor (registryConfig: RegistryConfig) {
this.registryConfig = registryConfig; this.registryConfig = registryConfig;
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId); this.registry = new LaconicRegistry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
} }
async createApplicationRecord ({ async createApplicationRecord ({
appName,
packageJSON, packageJSON,
commitHash, commitHash,
appType, appType,
repoUrl repoUrl
}: { }: {
packageJSON: PackageJSON appName: string;
commitHash: string, packageJSON: PackageJSON;
appType: string, commitHash: string;
repoUrl: string appType: string;
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> { repoUrl: string;
assert(packageJSON.name, "name field doesn't exist in package.json"); }): Promise<{
applicationRecordId: string;
applicationRecordData: ApplicationRecord;
}> {
// Use laconic-sdk to publish record // Use laconic-sdk to publish record
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
// Fetch previous records // Fetch previous records
const records = await this.registry.queryRecords({ const records = await this.registry.queryRecords(
{
type: APP_RECORD_TYPE, type: APP_RECORD_TYPE,
name: packageJSON.name name: packageJSON.name
}, true); },
true
);
// Get next version of record // Get next version of record
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId); const bondRecords = records.filter(
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); (record: any) => record.bondId === this.registryConfig.bondId
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch'); );
const [latestBondRecord] = bondRecords.sort(
(a: any, b: any) =>
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
);
const nextVersion = semverInc(
latestBondRecord?.attributes.version ?? '0.0.0',
'patch'
);
assert(nextVersion, 'Application record version not valid'); assert(nextVersion, 'Application record version not valid');
@ -60,11 +82,16 @@ export class Registry {
repository_ref: commitHash, repository_ref: commitHash,
repository: [repoUrl], repository: [repoUrl],
app_type: appType, app_type: appType,
name: packageJSON.name, name: appName,
...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }), ...(packageJSON.license && { license: packageJSON.license }),
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }), ...(packageJSON.author && {
author:
typeof packageJSON.author === 'object'
? JSON.stringify(packageJSON.author)
: packageJSON.author
}),
...(packageJSON.version && { app_version: packageJSON.version }) ...(packageJSON.version && { app_version: packageJSON.version })
}; };
@ -81,26 +108,45 @@ export class Registry {
log('Application record data:', applicationRecord); log('Application record data:', applicationRecord);
// TODO: Discuss computation of CRN // TODO: Discuss computation of CRN
const crn = this.getCrn(packageJSON.name); const crn = this.getCrn(packageJSON.name, appName);
log(`Setting name: ${crn} for record ID: ${result.data.id}`); log(`Setting name: ${crn} for record ID: ${result.data.id}`);
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee); await this.registry.setName(
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee); { cid: result.data.id, crn },
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee); this.registryConfig.privateKey,
this.registryConfig.fee
);
await this.registry.setName(
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
this.registryConfig.privateKey,
this.registryConfig.fee
);
await this.registry.setName(
{
cid: result.data.id,
crn: `${crn}@${applicationRecord.repository_ref}`
},
this.registryConfig.privateKey,
this.registryConfig.fee
);
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord }; return {
applicationRecordId: result.data.id,
applicationRecordData: applicationRecord
};
} }
async createApplicationDeploymentRequest (data: { async createApplicationDeploymentRequest (data: {
appName: string, appName: string;
commitHash: string, packageJsonName: string;
repository: string, commitHash: string;
environmentVariables: { [key: string]: string } repository: string;
environmentVariables: { [key: string]: string };
}): Promise<{ }): Promise<{
applicationDeploymentRequestId: string, applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> { }> {
const crn = this.getCrn(data.appName); const crn = this.getCrn(data.packageJsonName, data.appName);
const records = await this.registry.resolveNames([crn]); const records = await this.registry.resolveNames([crn]);
const applicationRecord = records[0]; const applicationRecord = records[0];
@ -124,7 +170,9 @@ export class Registry {
env: data.environmentVariables env: data.environmentVariables
}), }),
meta: JSON.stringify({ meta: JSON.stringify({
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`, note: `Added by Snowball @ ${DateTime.utc().toFormat(
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
)}`,
repository: data.repository, repository: data.repository,
repository_ref: data.commitHash repository_ref: data.commitHash
}) })
@ -142,31 +190,40 @@ export class Registry {
log(`Application deployment request record published: ${result.data.id}`); log(`Application deployment request record published: ${result.data.id}`);
log('Application deployment request data:', applicationDeploymentRequest); log('Application deployment request data:', applicationDeploymentRequest);
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest }; return {
applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest
};
} }
/** /**
* Fetch ApplicationDeploymentRecords for deployments * Fetch ApplicationDeploymentRecords for deployments
*/ */
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> { async getDeploymentRecords (
deployments: Deployment[]
): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute // TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
const records = await this.registry.queryRecords({ const records = await this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE type: APP_DEPLOYMENT_RECORD_TYPE
}, true); },
true
);
// Filter records with ApplicationRecord ids // Filter records with ApplicationRecord ids
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application)); return records.filter((record: AppDeploymentRecord) =>
deployments.some(
(deployment) =>
deployment.applicationRecordId === record.attributes.application
)
);
} }
getCrn (packageJsonName: string): string { getCrn (packageJsonName: string, appName: string): string {
const [arg1, arg2] = packageJsonName.split('/'); const [arg1] = packageJsonName.split('/');
if (arg2) {
const authority = arg1.replace('@', ''); const authority = arg1.replace('@', '');
return `crn://${authority}/applications/${arg2}`;
}
return `crn://${arg1}/applications/${arg1}`; return `crn://${authority}/applications/${appName}`;
} }
} }

View File

@ -17,7 +17,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getUser(context.userId); return service.getUser(context.userId);
}, },
organizations: async (_:any, __: any, context: any) => { organizations: async (_: any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.userId); return service.getOrganizationsByUserId(context.userId);
}, },
@ -25,15 +25,25 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectById(projectId); return service.getProjectById(projectId);
}, },
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => { projectsInOrganization: async (
return service.getProjectsInOrganization(context.userId, organizationSlug); _: any,
{ organizationSlug }: { organizationSlug: string },
context: any
) => {
return service.getProjectsInOrganization(
context.userId,
organizationSlug
);
}, },
deployments: async (_: any, { projectId }: { projectId: string }) => { deployments: async (_: any, { projectId }: { projectId: string }) => {
return service.getDeploymentsByProjectId(projectId); return service.getDeploymentsByProjectId(projectId);
}, },
environmentVariables: async (_: any, { projectId }: { projectId: string }) => { environmentVariables: async (
_: any,
{ projectId }: { projectId: string }
) => {
return service.getEnvironmentVariablesByProjectId(projectId); return service.getEnvironmentVariablesByProjectId(projectId);
}, },
@ -41,32 +51,55 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId); return service.getProjectMembersByProjectId(projectId);
}, },
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => { searchProjects: async (
_: any,
{ searchText }: { searchText: string },
context: any
) => {
return service.searchProjects(context.userId, searchText); return service.searchProjects(context.userId, searchText);
}, },
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => { domains: async (
_: any,
{
projectId,
filter
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
) => {
return service.getDomainsByProjectId(projectId, filter); return service.getDomainsByProjectId(projectId, filter);
} }
}, },
// TODO: Return error in GQL response // TODO: Return error in GQL response
Mutation: { Mutation: {
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => { removeProjectMember: async (
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any
) => {
try { try {
return await service.removeProjectMember(context.userId, projectMemberId); return await service.removeProjectMember(
context.userId,
projectMemberId
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateProjectMember: async (_: any, { projectMemberId, data }: { updateProjectMember: async (
projectMemberId: string, _: any,
{
projectMemberId,
data
}: {
projectMemberId: string;
data: { data: {
permissions: Permission[] permissions: Permission[];
};
} }
}) => { ) => {
try { try {
return await service.updateProjectMember(projectMemberId, data); return await service.updateProjectMember(projectMemberId, data);
} catch (err) { } catch (err) {
@ -75,13 +108,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addProjectMember: async (_: any, { projectId, data }: { addProjectMember: async (
projectId: string, _: any,
{
projectId,
data
}: {
projectId: string;
data: { data: {
email: string, email: string;
permissions: Permission[] permissions: Permission[];
};
} }
}) => { ) => {
try { try {
return Boolean(await service.addProjectMember(projectId, data)); return Boolean(await service.addProjectMember(projectId, data));
} catch (err) { } catch (err) {
@ -90,25 +129,51 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => { addEnvironmentVariables: async (
_: any,
{
projectId,
data
}: {
projectId: string;
data: { environments: string[]; key: string; value: string }[];
}
) => {
try { try {
return Boolean(await service.addEnvironmentVariables(projectId, data)); return Boolean(
await service.addEnvironmentVariables(projectId, data)
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => { updateEnvironmentVariable: async (
_: any,
{
environmentVariableId,
data
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
}
) => {
try { try {
return await service.updateEnvironmentVariable(environmentVariableId, data); return await service.updateEnvironmentVariable(
environmentVariableId,
data
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => { removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string }
) => {
try { try {
return await service.removeEnvironmentVariable(environmentVariableId); return await service.removeEnvironmentVariable(environmentVariableId);
} catch (err) { } catch (err) {
@ -117,25 +182,45 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { updateDeploymentToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
) => {
try { try {
return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId)); return Boolean(
await service.updateDeploymentToProd(context.userId, deploymentId)
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => { addProject: async (
_: any,
{
organizationSlug,
data
}: { organizationSlug: string; data: DeepPartial<Project> },
context: any
) => {
try { try {
return await service.addProject(context.userId, organizationSlug, data); return await service.addProject(
context.userId,
organizationSlug,
data
);
} catch (err) { } catch (err) {
log(err); log(err);
throw err; throw err;
} }
}, },
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => { updateProject: async (
_: any,
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
) => {
try { try {
return await service.updateProject(projectId, data); return await service.updateProject(projectId, data);
} catch (err) { } catch (err) {
@ -144,9 +229,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { redeployToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
) => {
try { try {
return Boolean(await service.redeployToProd(context.userId, deploymentId)); return Boolean(
await service.redeployToProd(context.userId, deploymentId)
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -157,7 +248,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
try { try {
return await service.deleteProject(projectId); return await service.deleteProject(projectId);
} catch (err) { } catch (err) {
log(err); return false; log(err);
return false;
} }
}, },
@ -170,7 +262,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => { rollbackDeployment: async (
_: any,
{
projectId,
deploymentId
}: { deploymentId: string; projectId: string }
) => {
try { try {
return await service.rollbackDeployment(projectId, deploymentId); return await service.rollbackDeployment(projectId, deploymentId);
} catch (err) { } catch (err) {
@ -179,7 +277,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => { addDomain: async (
_: any,
{ projectId, data }: { projectId: string; data: { name: string } }
) => {
try { try {
return Boolean(await service.addDomain(projectId, data)); return Boolean(await service.addDomain(projectId, data));
} catch (err) { } catch (err) {
@ -188,7 +289,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => { updateDomain: async (
_: any,
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
) => {
try { try {
return await service.updateDomain(domainId, data); return await service.updateDomain(domainId, data);
} catch (err) { } catch (err) {
@ -197,7 +301,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => { authenticateGitHub: async (
_: any,
{ code }: { code: string },
context: any
) => {
try { try {
return await service.authenticateGitHub(code, context.userId); return await service.authenticateGitHub(code, context.userId);
} catch (err) { } catch (err) {
@ -208,7 +316,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
unauthenticateGitHub: async (_: any, __: object, context: any) => { unauthenticateGitHub: async (_: any, __: object, context: any) => {
try { try {
return service.unauthenticateGitHub(context.userId, { gitHubToken: null }); return service.unauthenticateGitHub(context.userId, {
gitHubToken: null
});
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;

View File

@ -188,10 +188,19 @@ type Query {
type Mutation { type Mutation {
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean! addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean! updateProjectMember(
projectMemberId: String!
data: UpdateProjectMemberInput
): Boolean!
removeProjectMember(projectMemberId: String!): Boolean! removeProjectMember(projectMemberId: String!): Boolean!
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean! addEnvironmentVariables(
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean! projectId: String!
data: [AddEnvironmentVariableInput!]
): Boolean!
updateEnvironmentVariable(
environmentVariableId: String!
data: UpdateEnvironmentVariableInput!
): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(organizationSlug: String!, data: AddProjectInput): Project! addProject(organizationSlug: String!, data: AddProjectInput): Project!

View File

@ -15,15 +15,15 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User'; import { User } from './entity/User';
import { Registry } from './registry'; import { Registry } from './registry';
import { GitHubConfig, RegistryConfig } from './config'; import { GitHubConfig, RegistryConfig } from './config';
import { AppDeploymentRecord, GitPushEventPayload } from './types'; import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
const log = debug('snowball:service'); const log = debug('snowball:service');
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository'; const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
interface Config { interface Config {
gitHubConfig: GitHubConfig gitHubConfig: GitHubConfig;
registryConfig: RegistryConfig registryConfig: RegistryConfig;
} }
export class Service { export class Service {
@ -71,7 +71,9 @@ export class Service {
}); });
if (deployments.length) { if (deployments.length) {
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`); log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
);
// Fetch ApplicationDeploymentRecord for deployments // Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments); const records = await this.registry.getDeploymentRecords(deployments);
@ -91,10 +93,12 @@ export class Service {
/** /**
* Update deployments with ApplicationDeploymentRecord data * Update deployments with ApplicationDeploymentRecord data
*/ */
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> { async updateDeploymentsWithRecordData (
records: AppDeploymentRecord[]
): Promise<void> {
// Get deployments for ApplicationDeploymentRecords // Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: records.map(record => ({ where: records.map((record) => ({
applicationRecordId: record.attributes.application applicationRecordId: record.attributes.application
})), })),
order: { order: {
@ -103,38 +107,46 @@ export class Service {
}); });
// Get project IDs of deployments that are in production environment // Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => { const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) { if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId); acc.add(deployment.projectId);
} }
return acc; return acc;
}, new Set<string>()); },
new Set<string>()
);
// Set old deployments isCurrent to false // Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false }); await this.db.updateDeploymentsByProjectIds(
Array.from(productionDeploymentProjectIds),
{ isCurrent: false }
);
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => { const recordToDeploymentsMap = deployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationRecordId] = deployment; acc[deployment.applicationRecordId] = deployment;
return acc; return acc;
}, {}); },
{}
);
// Update deployment data for ApplicationDeploymentRecords // Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => { const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.application]; const deployment = recordToDeploymentsMap[record.attributes.application];
await this.db.updateDeploymentById( await this.db.updateDeploymentById(deployment.id, {
deployment.id,
{
applicationDeploymentRecordId: record.id, applicationDeploymentRecordId: record.id,
applicationDeploymentRecordData: record.attributes, applicationDeploymentRecordData: record.attributes,
url: record.attributes.url, url: record.attributes.url,
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production isCurrent: deployment.environment === Environment.Production
} });
);
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`); log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
);
}); });
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
@ -150,7 +162,10 @@ export class Service {
async getOctokit (userId: string): Promise<Octokit> { async getOctokit (userId: string): Promise<Octokit> {
const user = await this.db.getUser({ where: { id: userId } }); const user = await this.db.getUser({ where: { id: userId } });
assert(user && user.gitHubToken, 'User needs to be authenticated with GitHub token'); assert(
user && user.gitHubToken,
'User needs to be authenticated with GitHub token'
);
return new Octokit({ auth: user.gitHubToken }); return new Octokit({ auth: user.gitHubToken });
} }
@ -165,8 +180,14 @@ export class Service {
return dbProject; return dbProject;
} }
async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> { async getProjectsInOrganization (
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug); userId: string,
organizationSlug: string
): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(
userId,
organizationSlug
);
return dbProjects; return dbProjects;
} }
@ -175,35 +196,52 @@ export class Service {
return dbDeployments; return dbDeployments;
} }
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> { async getEnvironmentVariablesByProjectId (
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId); projectId: string
): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables =
await this.db.getEnvironmentVariablesByProjectId(projectId);
return dbEnvironmentVariables; return dbEnvironmentVariables;
} }
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> { async getProjectMembersByProjectId (
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId); projectId: string
): Promise<ProjectMember[]> {
const dbProjectMembers =
await this.db.getProjectMembersByProjectId(projectId);
return dbProjectMembers; return dbProjectMembers;
} }
async searchProjects (userId: string, searchText: string): Promise<Project[]> { async searchProjects (userId: string, searchText: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsBySearchText(userId, searchText); const dbProjects = await this.db.getProjectsBySearchText(
userId,
searchText
);
return dbProjects; return dbProjects;
} }
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> { async getDomainsByProjectId (
projectId: string,
filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> {
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter); const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
return dbDomains; return dbDomains;
} }
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> { async updateProjectMember (
projectMemberId: string,
data: { permissions: Permission[] }
): Promise<boolean> {
return this.db.updateProjectMemberById(projectMemberId, data); return this.db.updateProjectMemberById(projectMemberId, data);
} }
async addProjectMember (projectId: string, async addProjectMember (
projectId: string,
data: { data: {
email: string, email: string;
permissions: Permission[] permissions: Permission[];
}): Promise<ProjectMember> { }
): Promise<ProjectMember> {
// TODO: Send invitation // TODO: Send invitation
let user = await this.db.getUser({ let user = await this.db.getUser({
where: { where: {
@ -231,7 +269,10 @@ export class Service {
return newProjectMember; return newProjectMember;
} }
async removeProjectMember (userId: string, projectMemberId: string): Promise<boolean> { async removeProjectMember (
userId: string,
projectMemberId: string
): Promise<boolean> {
const member = await this.db.getProjectMemberById(projectMemberId); const member = await this.db.getProjectMemberById(projectMemberId);
if (String(member.member.id) === userId) { if (String(member.member.id) === userId) {
@ -248,33 +289,48 @@ export class Service {
} }
} }
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> { async addEnvironmentVariables (
const formattedEnvironmentVariables = data.map((environmentVariable) => { projectId: string,
data: { environments: string[]; key: string; value: string }[]
): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data
.map((environmentVariable) => {
return environmentVariable.environments.map((environment) => { return environmentVariable.environments.map((environment) => {
return ({ return {
key: environmentVariable.key, key: environmentVariable.key,
value: environmentVariable.value, value: environmentVariable.value,
environment: environment as Environment, environment: environment as Environment,
project: Object.assign(new Project(), { project: Object.assign(new Project(), {
id: projectId id: projectId
}) })
};
}); });
}); })
}).flat(); .flat();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables); const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
formattedEnvironmentVariables
);
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> { async updateEnvironmentVariable (
environmentVariableId: string,
data: DeepPartial<EnvironmentVariable>
): Promise<boolean> {
return this.db.updateEnvironmentVariable(environmentVariableId, data); return this.db.updateEnvironmentVariable(environmentVariableId, data);
} }
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> { async removeEnvironmentVariable (
environmentVariableId: string
): Promise<boolean> {
return this.db.deleteEnvironmentVariable(environmentVariableId); return this.db.deleteEnvironmentVariable(environmentVariableId);
} }
async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> { async updateDeploymentToProd (
userId: string,
deploymentId: string
): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({ const oldDeployment = await this.db.getDeployment({
where: { id: deploymentId }, where: { id: deploymentId },
relations: { relations: {
@ -286,13 +342,14 @@ export class Service {
throw new Error('Deployment does not exist'); throw new Error('Deployment does not exist');
} }
const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch }); const prodBranchDomains = await this.db.getDomainsByProjectId(
oldDeployment.project.id,
{ branch: oldDeployment.project.prodBranch }
);
const octokit = await this.getOctokit(userId); const octokit = await this.getOctokit(userId);
const newDeployment = await this.createDeployment(userId, const newDeployment = await this.createDeployment(userId, octokit, {
octokit,
{
project: oldDeployment.project, project: oldDeployment.project,
branch: oldDeployment.branch, branch: oldDeployment.branch,
environment: Environment.Production, environment: Environment.Production,
@ -311,7 +368,9 @@ export class Service {
recordData: { repoUrl?: string } = {} recordData: { repoUrl?: string } = {}
): Promise<Deployment> { ): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found'); assert(data.project?.repository, 'Project repository not found');
log(`Creating deployment in project ${data.project.name} from branch ${data.branch}`); log(
`Creating deployment in project ${data.project.name} from branch ${data.branch}`
);
const [owner, repo] = data.project.repository.split('/'); const [owner, repo] = data.project.repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({ const { data: packageJSONData } = await octokit.rest.repos.getContent({
@ -326,28 +385,61 @@ export class Service {
} }
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file'); assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
const packageJSON = JSON.parse(atob(packageJSONData.content)); const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
assert(packageJSON.name, "name field doesn't exist in package.json");
if (!recordData.repoUrl) { if (!recordData.repoUrl) {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); const { data: repoDetails } = await octokit.rest.repos.get({
owner,
repo
});
recordData.repoUrl = repoDetails.html_url; recordData.repoUrl = repoDetails.html_url;
} }
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record) // TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({ const { applicationRecordId, applicationRecordData } =
await this.registry.createApplicationRecord({
appName: repo,
packageJSON, packageJSON,
appType: data.project!.template!, appType: data.project!.template!,
commitHash: data.commitHash!, commitHash: data.commitHash!,
repoUrl: recordData.repoUrl repoUrl: recordData.repoUrl
}); });
const environmentVariables =
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
environment: Environment.Production
});
const environmentVariablesObj = environmentVariables.reduce(
(acc, env) => {
acc[env.key] = env.value;
return acc;
},
{} as { [key: string]: string }
);
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.registry.createApplicationDeploymentRequest({
appName: repo,
packageJsonName: packageJSON.name,
commitHash: data.commitHash!,
repository: recordData.repoUrl,
environmentVariables: environmentVariablesObj
});
// Update previous deployment with prod branch domain // Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain // TODO: Fix unique constraint error for domain
await this.db.updateDeployment({ await this.db.updateDeployment(
{
domainId: data.domain?.id domainId: data.domain?.id
}, { },
{
domain: null domain: null
}); }
);
const newDeployment = await this.db.addDeployment({ const newDeployment = await this.db.addDeployment({
project: data.project, project: data.project,
@ -358,17 +450,25 @@ export class Service {
status: DeploymentStatus.Building, status: DeploymentStatus.Building,
applicationRecordId, applicationRecordId,
applicationRecordData, applicationRecordData,
applicationDeploymentRequestId,
applicationDeploymentRequestData,
domain: data.domain, domain: data.domain,
createdBy: Object.assign(new User(), { createdBy: Object.assign(new User(), {
id: userId id: userId
}) })
}); });
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`); log(
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`
);
return newDeployment; return newDeployment;
} }
async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> { async addProject (
userId: string,
organizationSlug: string,
data: DeepPartial<Project>
): Promise<Project | undefined> {
const organization = await this.db.getOrganization({ const organization = await this.db.getOrganization({
where: { where: {
slug: organizationSlug slug: organizationSlug
@ -383,7 +483,9 @@ export class Service {
const octokit = await this.getOctokit(userId); const octokit = await this.getOctokit(userId);
const [owner, repo] = project.repository.split('/'); const [owner, repo] = project.repository.split('/');
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({ const {
data: [latestCommit]
} = await octokit.rest.repos.listCommits({
owner, owner,
repo, repo,
sha: project.prodBranch, sha: project.prodBranch,
@ -393,7 +495,8 @@ export class Service {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
// Create deployment with prod branch and latest commit // Create deployment with prod branch and latest commit
const newDeployment = await this.createDeployment(userId, await this.createDeployment(
userId,
octokit, octokit,
{ {
project, project,
@ -408,27 +511,6 @@ export class Service {
} }
); );
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(project.id, { environment: Environment.Production });
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
acc[env.key] = env.value;
return acc;
}, {} as { [key: string]: string });
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
{
appName: newDeployment.applicationRecordData.name!,
commitHash: latestCommit.sha,
repository: repoDetails.html_url,
environmentVariables: environmentVariablesObj
});
await this.db.updateProjectById(project.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData
});
await this.createRepoHook(octokit, project); await this.createRepoHook(octokit, project);
return project; return project;
@ -441,7 +523,10 @@ export class Service {
owner, owner,
repo, repo,
config: { config: {
url: new URL('api/github/webhook', this.config.gitHubConfig.webhookUrl).href, url: new URL(
'api/github/webhook',
this.config.gitHubConfig.webhookUrl
).href,
content_type: 'json' content_type: 'json'
}, },
events: ['push'] events: ['push']
@ -449,9 +534,13 @@ export class Service {
} catch (err) { } catch (err) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
if ( if (
!(err instanceof RequestError && !(
err instanceof RequestError &&
err.status === 422 && err.status === 422 &&
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR)) (err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
)
)
) { ) {
throw err; throw err;
} }
@ -463,7 +552,9 @@ export class Service {
async handleGitHubPush (data: GitPushEventPayload): Promise<void> { async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
const { repository, ref, head_commit: headCommit } = data; const { repository, ref, head_commit: headCommit } = data;
log(`Handling GitHub push event from repository: ${repository.full_name}`); log(`Handling GitHub push event from repository: ${repository.full_name}`);
const projects = await this.db.getProjects({ where: { repository: repository.full_name } }); const projects = await this.db.getProjects({
where: { repository: repository.full_name }
});
if (!projects.length) { if (!projects.length) {
log(`No projects found for repository ${repository.full_name}`); log(`No projects found for repository ${repository.full_name}`);
@ -475,15 +566,18 @@ export class Service {
for await (const project of projects) { for await (const project of projects) {
const octokit = await this.getOctokit(project.ownerId); const octokit = await this.getOctokit(project.ownerId);
const [domain] = await this.db.getDomainsByProjectId(project.id, { branch }); const [domain] = await this.db.getDomainsByProjectId(project.id, {
branch
});
// Create deployment with branch and latest commit in GitHub data // Create deployment with branch and latest commit in GitHub data
await this.createDeployment(project.ownerId, await this.createDeployment(project.ownerId, octokit, {
octokit,
{
project, project,
branch, branch,
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview, environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain, domain,
commitHash: headCommit.id, commitHash: headCommit.id,
commitMessage: headCommit.message commitMessage: headCommit.message
@ -491,7 +585,10 @@ export class Service {
} }
} }
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> { async updateProject (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
return this.db.updateProjectById(projectId, data); return this.db.updateProjectById(projectId, data);
} }
@ -508,13 +605,18 @@ export class Service {
}); });
if (domainsRedirectedFrom.length > 0) { if (domainsRedirectedFrom.length > 0) {
throw new Error('Cannot delete domain since it has redirects from other domains'); throw new Error(
'Cannot delete domain since it has redirects from other domains'
);
} }
return this.db.deleteDomainById(domainId); return this.db.deleteDomainById(domainId);
} }
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment> { async redeployToProd (
userId: string,
deploymentId: string
): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({ const oldDeployment = await this.db.getDeployment({
relations: { relations: {
project: true, project: true,
@ -532,9 +634,7 @@ export class Service {
const octokit = await this.getOctokit(userId); const octokit = await this.getOctokit(userId);
const newDeployment = await this.createDeployment(userId, const newDeployment = await this.createDeployment(userId, octokit, {
octokit,
{
project: oldDeployment.project, project: oldDeployment.project,
// TODO: Put isCurrent field in project // TODO: Put isCurrent field in project
branch: oldDeployment.branch, branch: oldDeployment.branch,
@ -547,7 +647,10 @@ export class Service {
return newDeployment; return newDeployment;
} }
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> { async rollbackDeployment (
projectId: string,
deploymentId: string
): Promise<boolean> {
// TODO: Implement transactions // TODO: Implement transactions
const oldCurrentDeployment = await this.db.getDeployment({ const oldCurrentDeployment = await this.db.getDeployment({
relations: { relations: {
@ -565,16 +668,25 @@ export class Service {
throw new Error('Current deployment doesnot exist'); throw new Error('Current deployment doesnot exist');
} }
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null }); const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
oldCurrentDeployment.id,
{ isCurrent: false, domain: null }
);
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain }); const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
deploymentId,
{ isCurrent: true, domain: oldCurrentDeployment?.domain }
);
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate; return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
} }
async addDomain (projectId: string, data: { name: string }): Promise<{ async addDomain (
primaryDomain: Domain, projectId: string,
redirectedDomain: Domain data: { name: string }
): Promise<{
primaryDomain: Domain;
redirectedDomain: Domain;
}> { }> {
const currentProject = await this.db.getProjectById(projectId); const currentProject = await this.db.getProjectById(projectId);
@ -599,12 +711,20 @@ export class Service {
redirectTo: savedPrimaryDomain redirectTo: savedPrimaryDomain
}; };
const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails); const savedRedirectedDomain = await this.db.addDomain(
redirectedDomainDetails
);
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain }; return {
primaryDomain: savedPrimaryDomain,
redirectedDomain: savedRedirectedDomain
};
} }
async updateDomain (domainId: string, data: DeepPartial<Domain>): Promise<boolean> { async updateDomain (
domainId: string,
data: DeepPartial<Domain>
): Promise<boolean> {
const domain = await this.db.getDomain({ const domain = await this.db.getDomain({
where: { where: {
id: domainId id: domainId
@ -645,7 +765,9 @@ export class Service {
} }
if (redirectedDomain.redirectToId) { if (redirectedDomain.redirectToId) {
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'); throw new Error(
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
);
} }
newDomain.redirectTo = redirectedDomain; newDomain.redirectTo = redirectedDomain;
@ -656,8 +778,13 @@ export class Service {
return updateResult; return updateResult;
} }
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> { async authenticateGitHub (
const { authentication: { token } } = await this.oauthApp.createToken({ code: string,
userId: string
): Promise<{ token: string }> {
const {
authentication: { token }
} = await this.oauthApp.createToken({
code code
}); });
@ -666,7 +793,10 @@ export class Service {
return { token }; return { token };
} }
async unauthenticateGitHub (userId: string, data: DeepPartial<User>): Promise<boolean> { async unauthenticateGitHub (
userId: string,
data: DeepPartial<User>
): Promise<boolean> {
return this.db.updateUser(userId, data); return this.db.updateUser(userId, data);
} }
} }

View File

@ -1,6 +1,6 @@
export interface PackageJSON { export interface PackageJSON {
name?: string; name: string;
version?: string; version: string;
author?: string; author?: string;
description?: string; description?: string;
homepage?: string; homepage?: string;
@ -47,5 +47,5 @@ interface RegistryRecord {
} }
export interface AppDeploymentRecord extends RegistryRecord { export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes attributes: AppDeploymentRecordAttributes;
} }

View File

@ -2,6 +2,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import toml from 'toml'; import toml from 'toml';
import debug from 'debug'; import debug from 'debug';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
const log = debug('snowball:utils'); const log = debug('snowball:utils');
@ -19,3 +20,49 @@ export const getConfig = async <ConfigType>(
return config; return config;
}; };
export const checkFileExists = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
log(err);
return false;
}
};
export const getEntities = async (filePath: string): Promise<any> => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData);
return entities;
};
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
entityType: EntityTarget<Entity>,
dataSource: DataSource,
entities: any,
relations?: any | undefined
): Promise<Entity[]> => {
const entityRepository = dataSource.getRepository(entityType);
const savedEntity: Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
if (relations) {
for (const field in relations) {
const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity);
}
return savedEntity;
};

View File

@ -18,4 +18,4 @@ const main = async () => {
deleteFile(config.database.dbPath); deleteFile(config.database.dbPath);
}; };
main().catch(err => log(err)); main().catch((err) => log(err));

View File

@ -1,14 +1,16 @@
[ [
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex":0, "domainIndex": 0,
"createdByIndex": 0, "createdByIndex": 0,
"id":"ffhae3zq", "id": "ffhae3zq",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main", "branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -16,14 +18,16 @@
}, },
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex":1, "domainIndex": 1,
"createdByIndex": 0, "createdByIndex": 0,
"id":"vehagei8", "id": "vehagei8",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -31,14 +35,16 @@
}, },
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex":2, "domainIndex": 2,
"createdByIndex": 0, "createdByIndex": 0,
"id":"qmgekyte", "id": "qmgekyte",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -48,12 +54,14 @@
"projectIndex": 0, "projectIndex": 0,
"domainIndex": null, "domainIndex": null,
"createdByIndex": 0, "createdByIndex": 0,
"id":"f8wsyim6", "id": "f8wsyim6",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", "applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "prod", "branch": "prod",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -61,14 +69,16 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex":3, "domainIndex": 3,
"createdByIndex": 1, "createdByIndex": 1,
"id":"eO8cckxk", "id": "eO8cckxk",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main", "branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -76,14 +86,16 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex":4, "domainIndex": 4,
"createdByIndex": 1, "createdByIndex": 1,
"id":"yaq0t5yw", "id": "yaq0t5yw",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -91,14 +103,16 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex":5, "domainIndex": 5,
"createdByIndex": 1, "createdByIndex": 1,
"id":"hwwr6sbx", "id": "hwwr6sbx",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", "applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -106,14 +120,16 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex":9, "domainIndex": 9,
"createdByIndex": 2, "createdByIndex": 2,
"id":"ndxje48a", "id": "ndxje48a",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main", "branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -121,14 +137,16 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex":7, "domainIndex": 7,
"createdByIndex": 2, "createdByIndex": 2,
"id":"gtgpgvei", "id": "gtgpgvei",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -136,14 +154,16 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex":8, "domainIndex": 8,
"createdByIndex": 2, "createdByIndex": 2,
"id":"b4bpthjr", "id": "b4bpthjr",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -153,12 +173,14 @@
"projectIndex": 3, "projectIndex": 3,
"domainIndex": 6, "domainIndex": 6,
"createdByIndex": 2, "createdByIndex": 2,
"id":"b4bpthjr", "id": "b4bpthjr",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",

View File

@ -2,77 +2,55 @@
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 0, "projectIndex": 0,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 0, "projectIndex": 0,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 1, "projectIndex": 1,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 2, "projectIndex": 2,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 2, "projectIndex": 2,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 3, "projectIndex": 3,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 3, "projectIndex": 3,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 4, "projectIndex": 4,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 4, "projectIndex": 4,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
} }
] ]

View File

@ -10,8 +10,6 @@
"framework": "test", "framework": "test",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject.snowball.xyz" "subDomain": "testProject.snowball.xyz"
}, },
{ {
@ -25,8 +23,6 @@
"framework": "test-2", "framework": "test-2",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject-2.snowball.xyz" "subDomain": "testProject-2.snowball.xyz"
}, },
{ {
@ -40,8 +36,6 @@
"framework": "test-3", "framework": "test-3",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools.snowball.xyz" "subDomain": "iglootools.snowball.xyz"
}, },
{ {
@ -55,8 +49,6 @@
"framework": "test-4", "framework": "test-4",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools-2.snowball.xyz" "subDomain": "iglootools-2.snowball.xyz"
}, },
{ {
@ -70,8 +62,6 @@
"framework": "test-5", "framework": "test-5",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "snowball-2.snowball.xyz" "subDomain": "snowball-2.snowball.xyz"
} }
] ]

View File

@ -1,5 +1,4 @@
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; import { DataSource } from 'typeorm';
import * as fs from 'fs/promises';
import debug from 'debug'; import debug from 'debug';
import path from 'path'; import path from 'path';
@ -11,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
import { Domain } from '../src/entity/Domain'; import { Domain } from '../src/entity/Domain';
import { ProjectMember } from '../src/entity/ProjectMember'; import { ProjectMember } from '../src/entity/ProjectMember';
import { Deployment } from '../src/entity/Deployment'; import { Deployment } from '../src/entity/Deployment';
import { getConfig } from '../src/utils'; import {
checkFileExists,
getConfig,
getEntities,
loadAndSaveData
} from '../src/utils';
import { Config } from '../src/config'; import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
@ -27,43 +31,35 @@ const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json'; const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'; const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData);
const entityRepository = dataSource.getRepository(entityType);
const savedEntity:Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
if (relations) {
for (const field in relations) {
const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity);
}
return savedEntity;
};
const generateTestData = async (dataSource: DataSource) => { const generateTestData = async (dataSource: DataSource) => {
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH)); const userEntities = await getEntities(
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH)); path.resolve(__dirname, USER_DATA_PATH)
);
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
const orgEntities = await getEntities(
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
);
const savedOrgs = await loadAndSaveData(
Organization,
dataSource,
orgEntities
);
const projectRelations = { const projectRelations = {
owner: savedUsers, owner: savedUsers,
organization: savedOrgs organization: savedOrgs
}; };
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations); const projectEntities = await getEntities(
path.resolve(__dirname, PROJECT_DATA_PATH)
);
const savedProjects = await loadAndSaveData(
Project,
dataSource,
projectEntities,
projectRelations
);
const domainRepository = dataSource.getRepository(Domain); const domainRepository = dataSource.getRepository(Domain);
@ -71,14 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects project: savedProjects
}; };
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations); const primaryDomainsEntities = await getEntities(
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
);
const savedPrimaryDomains = await loadAndSaveData(
Domain,
dataSource,
primaryDomainsEntities,
domainPrimaryRelations
);
const domainRedirectedRelations = { const domainRedirectedRelations = {
project: savedProjects, project: savedProjects,
redirectTo: savedPrimaryDomains redirectTo: savedPrimaryDomains
}; };
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations); const redirectDomainsEntities = await getEntities(
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
);
await loadAndSaveData(
Domain,
dataSource,
redirectDomainsEntities,
domainRedirectedRelations
);
const savedDomains = await domainRepository.find(); const savedDomains = await domainRepository.find();
@ -87,14 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs organization: savedOrgs
}; };
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations); const userOrganizationsEntities = await getEntities(
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
);
await loadAndSaveData(
UserOrganization,
dataSource,
userOrganizationsEntities,
userOrganizationRelations
);
const projectMemberRelations = { const projectMemberRelations = {
member: savedUsers, member: savedUsers,
project: savedProjects project: savedProjects
}; };
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations); const projectMembersEntities = await getEntities(
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
);
await loadAndSaveData(
ProjectMember,
dataSource,
projectMembersEntities,
projectMemberRelations
);
const deploymentRelations = { const deploymentRelations = {
project: savedProjects, project: savedProjects,
@ -102,23 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers createdBy: savedUsers
}; };
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations); const deploymentsEntities = await getEntities(
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
);
await loadAndSaveData(
Deployment,
dataSource,
deploymentsEntities,
deploymentRelations
);
const environmentVariableRelations = { const environmentVariableRelations = {
project: savedProjects project: savedProjects
}; };
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations); const environmentVariablesEntities = await getEntities(
}; path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
);
const checkFileExists = async (filePath: string) => { await loadAndSaveData(
try { EnvironmentVariable,
await fs.access(filePath, fs.constants.F_OK); dataSource,
return true; environmentVariablesEntities,
} catch (err) { environmentVariableRelations
log(err); );
return false;
}
}; };
const main = async () => { const main = async () => {

View File

@ -17,16 +17,32 @@ const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
async function main () { async function main () {
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId); const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
const bondId = await registry.getNextBondId(registryConfig.privateKey); const bondId = await registry.getNextBondId(registryConfig.privateKey);
log('bondId:', bondId); log('bondId:', bondId);
await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee); await registry.createBond(
{ denom: DENOM, amount: BOND_AMOUNT },
registryConfig.privateKey,
registryConfig.fee
);
for await (const name of AUTHORITY_NAMES) { for await (const name of AUTHORITY_NAMES) {
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee); await registry.reserveAuthority(
{ name },
registryConfig.privateKey,
registryConfig.fee
);
log('Reserved authority name:', name); log('Reserved authority name:', name);
await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee); await registry.setAuthorityBond(
{ name, bondId },
registryConfig.privateKey,
registryConfig.fee
);
log(`Bond ${bondId} set for authority ${name}`); log(`Bond ${bondId} set for authority ${name}`);
} }
} }

View File

@ -12,9 +12,15 @@ import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records'); const log = debug('snowball:publish-deploy-records');
async function main () { async function main () {
const { registryConfig, database } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig, database } = await getConfig<Config>(
DEFAULT_CONFIG_FILE_PATH
);
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId); const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
const dataSource = new DataSource({ const dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',
@ -53,7 +59,7 @@ async function main () {
so: '66fcfa49a1664d4cb4ce4f72c1c0e151' so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
}), }),
request: deployment.project.applicationDeploymentRequestId, request: deployment.applicationDeploymentRequestId,
url url
}; };

View File

@ -7,8 +7,11 @@
"@material-tailwind/react": "^2.1.7", "@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",

View File

@ -98,19 +98,29 @@ const Button = forwardRef<
href, href,
...baseLinkProps, ...baseLinkProps,
}; };
return <a {...externalLinkProps}>{_children}</a>; return (
// @ts-expect-error - ref
<a ref={ref} {...externalLinkProps}>
{_children}
</a>
);
} }
// Internal link // Internal link
return ( return (
<Link {...baseLinkProps} to={href}> // @ts-expect-error - ref
<Link ref={ref} {...baseLinkProps} to={href}>
{_children} {_children}
</Link> </Link>
); );
} else { } else {
const { ...buttonProps } = _props; const { ...buttonProps } = _props;
return (
// @ts-expect-error - as prop is not a valid prop for button elements // @ts-expect-error - as prop is not a valid prop for button elements
return <button {...buttonProps}>{_children}</button>; <button ref={ref} {...buttonProps}>
{_children}
</button>
);
} }
}, },
[], [],
@ -161,8 +171,6 @@ const Button = forwardRef<
return ( return (
<Component <Component
{...props} {...props}
// @ts-expect-error - ref is not a valid prop for button elements
ref={ref}
className={buttonTheme({ ...styleProps, class: className })} className={buttonTheme({ ...styleProps, class: className })}
> >
{cloneIcon(leftIcon, { ...iconSize })} {cloneIcon(leftIcon, { ...iconSize })}

View File

@ -50,6 +50,7 @@ export const inputTheme = tv(
'outline-offset-0', 'outline-offset-0',
'outline-border-danger', 'outline-border-danger',
'shadow-none', 'shadow-none',
'focus:outline-border-danger',
], ],
helperText: 'text-elements-danger', helperText: 'text-elements-danger',
}, },

View File

@ -0,0 +1,54 @@
import { VariantProps, tv } from 'tailwind-variants';
export const radioTheme = tv({
slots: {
root: ['flex', 'gap-3', 'flex-wrap'],
wrapper: ['flex', 'items-center', 'gap-2', 'group'],
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
radio: [
'w-5',
'h-5',
'rounded-full',
'border',
'group',
'border-border-interactive/10',
'shadow-button',
'group-hover:border-border-interactive/[0.14]',
'focus-ring',
// Checked
'data-[state=checked]:bg-controls-primary',
'data-[state=checked]:group-hover:bg-controls-primary-hovered',
],
indicator: [
'flex',
'items-center',
'justify-center',
'relative',
'w-full',
'h-full',
'after:content-[""]',
'after:block',
'after:w-2.5',
'after:h-2.5',
'after:rounded-full',
'after:bg-transparent',
'after:group-hover:bg-controls-disabled',
'after:group-focus-visible:bg-controls-disabled',
// Checked
'after:data-[state=checked]:bg-elements-on-primary',
'after:data-[state=checked]:group-hover:bg-elements-on-primary',
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
],
},
variants: {
orientation: {
vertical: { root: ['flex-col'] },
horizontal: { root: ['flex-row'] },
},
},
defaultVariants: {
orientation: 'vertical',
},
});
export type RadioTheme = VariantProps<typeof radioTheme>;

View File

@ -0,0 +1,63 @@
import React from 'react';
import {
Root as RadixRoot,
RadioGroupProps,
} from '@radix-ui/react-radio-group';
import { RadioTheme, radioTheme } from './Radio.theme';
import { RadioItem, RadioItemProps } from './RadioItem';
export interface RadioOption extends RadioItemProps {
/**
* The label of the radio option.
*/
label: string;
/**
* The value of the radio option.
*/
value: string;
}
export interface RadioProps extends RadioGroupProps, RadioTheme {
/**
* The options of the radio.
* @default []
* @example
* ```tsx
* const options = [
* {
* label: 'Label 1',
* value: '1',
* },
* {
* label: 'Label 2',
* value: '2',
* },
* {
* label: 'Label 3',
* value: '3',
* },
* ];
* ```
*/
options: RadioOption[];
}
/**
* The Radio component is used to select one option from a list of options.
*/
export const Radio = ({
className,
options,
orientation,
...props
}: RadioProps) => {
const { root } = radioTheme({ orientation });
return (
<RadixRoot {...props} className={root({ className })}>
{options.map((option) => (
<RadioItem key={option.value} {...option} />
))}
</RadixRoot>
);
};

View File

@ -0,0 +1,74 @@
import React, { ComponentPropsWithoutRef } from 'react';
import {
Item as RadixRadio,
Indicator as RadixIndicator,
RadioGroupItemProps,
RadioGroupIndicatorProps,
} from '@radix-ui/react-radio-group';
import { radioTheme } from './Radio.theme';
export interface RadioItemProps extends RadioGroupItemProps {
/**
* The wrapper props of the radio item.
* You can use this prop to customize the wrapper props.
*/
wrapperProps?: ComponentPropsWithoutRef<'div'>;
/**
* The label props of the radio item.
* You can use this prop to customize the label props.
*/
labelProps?: ComponentPropsWithoutRef<'label'>;
/**
* The indicator props of the radio item.
* You can use this prop to customize the indicator props.
*/
indicatorProps?: RadioGroupIndicatorProps;
/**
* The id of the radio item.
*/
id?: string;
/**
* The label of the radio item.
*/
label?: string;
}
/**
* The RadioItem component is used to render a radio item.
*/
export const RadioItem = ({
className,
wrapperProps,
labelProps,
indicatorProps,
label,
id,
...props
}: RadioItemProps) => {
const { wrapper, label: labelClass, radio, indicator } = radioTheme();
// Generate a unique id for the radio item from the label if the id is not provided
const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
const componentId = id ?? kebabCaseLabel;
return (
<div className={wrapper({ className: wrapperProps?.className })}>
<RadixRadio {...props} className={radio({ className })} id={componentId}>
<RadixIndicator
forceMount
{...indicatorProps}
className={indicator({ className: indicatorProps?.className })}
/>
</RadixRadio>
{label && (
<label
{...labelProps}
className={labelClass({ className: labelProps?.className })}
htmlFor={componentId}
>
{label}
</label>
)}
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from './Radio';
export * from './RadioItem';

View File

@ -0,0 +1,84 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const switchTheme = tv({
slots: {
wrapper: ['flex', 'items-start', 'gap-4', 'w-[375px]'],
switch: [
'h-6',
'w-12',
'rounded-full',
'transition-all',
'duration-500',
'relative',
'cursor-default',
'shadow-inset',
'focus-ring',
'outline-none',
],
thumb: [
'block',
'h-4',
'w-4',
'translate-x-1',
'transition-transform',
'duration-100',
'will-change-transform',
'rounded-full',
'shadow-button',
'data-[state=checked]:translate-x-7',
'bg-controls-elevated',
],
label: [
'flex',
'flex-1',
'flex-col',
'px-1',
'gap-1',
'text-sm',
'text-elements-high-em',
'tracking-[-0.006em]',
],
description: ['text-xs', 'text-elements-low-em'],
},
variants: {
checked: {
true: {
switch: [
'bg-controls-primary',
'hover:bg-controls-primary-hovered',
'focus-visible:bg-controls-primary-hovered',
],
},
false: {
switch: [
'bg-controls-inset',
'hover:bg-controls-inset-hovered',
'focus-visible:bg-controls-inset-hovered',
],
},
},
disabled: {
true: {
switch: ['bg-controls-disabled', 'cursor-not-allowed'],
thumb: ['bg-elements-on-disabled'],
},
},
fullWidth: {
true: {
wrapper: ['w-full', 'justify-between'],
},
},
},
compoundVariants: [
{
checked: true,
disabled: true,
class: {
switch: ['bg-controls-disabled-active'],
thumb: ['bg-snowball-900'],
},
},
],
});
export type SwitchVariants = VariantProps<typeof switchTheme>;

View File

@ -0,0 +1,85 @@
import React, { type ComponentPropsWithoutRef } from 'react';
import { type SwitchProps as SwitchRadixProps } from '@radix-ui/react-switch';
import * as SwitchRadix from '@radix-ui/react-switch';
import { switchTheme, type SwitchVariants } from './Switch.theme';
interface SwitchProps
extends Omit<SwitchRadixProps, 'checked'>,
SwitchVariants {
/**
* The label of the switch.
*/
label?: string;
/**
* The description of the switch.
*/
description?: string;
/**
* Custom wrapper props for the switch.
*/
wrapperProps?: ComponentPropsWithoutRef<'div'>;
/**
* Function that is called when the checked state of the switch changes.
* @param checked The new checked state of the switch.
*/
onCheckedChange?(checked: boolean): void;
}
/**
* A switch is a component used for toggling between two states.
*/
export const Switch = ({
className,
checked,
label,
description,
disabled,
name,
wrapperProps,
fullWidth,
...props
}: SwitchProps) => {
const {
wrapper,
switch: switchClass,
thumb,
label: labelClass,
description: descriptionClass,
} = switchTheme({
checked,
disabled,
fullWidth,
});
const switchComponent = (
<SwitchRadix.Root
{...props}
checked={checked}
disabled={disabled}
className={switchClass({ className })}
>
<SwitchRadix.Thumb className={thumb()} />
</SwitchRadix.Root>
);
// If a label is provided, wrap the switch in a label element.
if (label) {
return (
<div
{...wrapperProps}
className={wrapper({ className: wrapperProps?.className })}
>
<label className={labelClass()} htmlFor={name}>
{label}
{description && (
<span className={descriptionClass()}>{description}</span>
)}
</label>
{switchComponent}
</div>
);
}
return switchComponent;
};

View File

@ -0,0 +1 @@
export * from './Switch';

View File

@ -9,6 +9,8 @@ export const tabsTheme = tv({
// Horizontal default // Horizontal default
'px-1', 'px-1',
'pb-5', 'pb-5',
'cursor-default',
'select-none',
'text-elements-low-em', 'text-elements-low-em',
'border-b-2', 'border-b-2',
'border-transparent', 'border-transparent',

View File

@ -0,0 +1,14 @@
import { tv } from 'tailwind-variants';
export const tooltipTheme = tv({
slots: {
content: [
'z-tooltip',
'rounded-md',
'bg-surface-high-contrast',
'p-2',
'text-elements-on-high-contrast',
],
arrow: ['fill-surface-high-contrast'],
},
});

View File

@ -0,0 +1,47 @@
import React from 'react';
import type {
TooltipContentProps,
TooltipTriggerProps,
} from '@radix-ui/react-tooltip';
import { ReactNode, useState } from 'react';
import { TooltipBase, type TooltipBaseProps } from './TooltipBase';
export interface TooltipProps extends TooltipBaseProps {
triggerProps?: TooltipTriggerProps;
contentProps?: TooltipContentProps;
content?: ReactNode;
}
// https://github.com/radix-ui/primitives/issues/955#issuecomment-1798201143
// Wrap on top of Tooltip base to make tooltip open on mobile via click
export const Tooltip = ({
children,
triggerProps,
contentProps,
content,
...props
}: TooltipProps) => {
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
return (
<TooltipBase
open={isTooltipVisible}
onOpenChange={setIsTooltipVisible}
{...props}
>
<TooltipBase.Trigger
asChild
onBlur={() => setIsTooltipVisible(false)}
onClick={() => setIsTooltipVisible((prevOpen) => !prevOpen)}
onFocus={() => setTimeout(() => setIsTooltipVisible(true), 0)}
{...triggerProps}
>
{triggerProps?.children ?? children}
</TooltipBase.Trigger>
<TooltipBase.Content {...contentProps}>
{content ?? contentProps?.children ?? 'Coming soon'}
</TooltipBase.Content>
</TooltipBase>
);
};

View File

@ -0,0 +1,38 @@
import React from 'react';
import {
Provider,
TooltipProps as RadixTooltipProps,
Root,
type TooltipProviderProps,
} from '@radix-ui/react-tooltip';
import { useState, type PropsWithChildren } from 'react';
import { TooltipContent } from './TooltipContent';
import { TooltipTrigger } from './TooltipTrigger';
export interface TooltipBaseProps extends RadixTooltipProps {
providerProps?: TooltipProviderProps;
}
export const TooltipBase = ({
children,
providerProps,
...props
}: PropsWithChildren<TooltipBaseProps>) => {
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
return (
<Provider {...providerProps}>
<Root
open={isTooltipVisible}
onOpenChange={setIsTooltipVisible}
{...props}
>
{children}
</Root>
</Provider>
);
};
TooltipBase.Trigger = TooltipTrigger;
TooltipBase.Content = TooltipContent;

View File

@ -0,0 +1,46 @@
import React from 'react';
import {
Arrow,
Content,
Portal,
type TooltipArrowProps,
type TooltipContentProps,
} from '@radix-ui/react-tooltip';
import { tooltipTheme } from '../Tooltip.theme';
export interface ContentProps extends TooltipContentProps {
hasArrow?: boolean;
arrowProps?: TooltipArrowProps;
}
export const TooltipContent = ({
children,
arrowProps,
className,
hasArrow = true,
...props
}: ContentProps) => {
const { content, arrow } = tooltipTheme();
return (
<Portal>
<Content
className={content({
className,
})}
sideOffset={5}
{...props}
>
{hasArrow && (
<Arrow
className={arrow({
className: arrowProps?.className,
})}
{...arrowProps}
/>
)}
{children}
</Content>
</Portal>
);
};

View File

@ -0,0 +1 @@
export * from './TooltipContent';

View File

@ -0,0 +1,12 @@
import React from 'react';
import { Trigger, type TooltipTriggerProps } from '@radix-ui/react-tooltip';
export type TriggerProps = TooltipTriggerProps;
export const TooltipTrigger = ({ children, ...props }: TriggerProps) => {
return (
<Trigger asChild {...props}>
{children}
</Trigger>
);
};

View File

@ -0,0 +1 @@
export * from './TooltipTrigger';

View File

@ -0,0 +1,2 @@
export * from './Tooltip';
export * from './TooltipBase';

View File

@ -10,6 +10,7 @@ import { renderBadges } from './renders/badge';
import { import {
renderButtonIcons, renderButtonIcons,
renderButtons, renderButtons,
renderDisabledButtons,
renderLinks, renderLinks,
} from './renders/button'; } from './renders/button';
import { import {
@ -17,16 +18,22 @@ import {
renderTabs, renderTabs,
renderVerticalTabs, renderVerticalTabs,
} from './renders/tabs'; } from './renders/tabs';
import { Switch } from 'components/shared/Switch';
import { RADIO_OPTIONS } from './renders/radio';
import { Radio } from 'components/shared/Radio';
import { import {
renderInlineNotificationWithDescriptions, renderInlineNotificationWithDescriptions,
renderInlineNotifications, renderInlineNotifications,
} from './renders/inlineNotifications'; } from './renders/inlineNotifications';
import { renderInputs } from './renders/input'; import { renderInputs } from './renders/input';
import { renderToast, renderToastsWithCta } from './renders/toast'; import { renderToast, renderToastsWithCta } from './renders/toast';
import { renderTooltips } from './renders/tooltip';
const Page = () => { const Page = () => {
const [singleDate, setSingleDate] = useState<Value>(); const [singleDate, setSingleDate] = useState<Value>();
const [dateRange, setDateRange] = useState<Value>(); const [dateRange, setDateRange] = useState<Value>();
const [switchValue, setSwitchValue] = useState(false);
const [selectedRadio, setSelectedRadio] = useState<string>('');
return ( return (
<div className="relative h-full min-h-full"> <div className="relative h-full min-h-full">
@ -52,17 +59,47 @@ const Page = () => {
{/* Button */} {/* Button */}
<div className="flex flex-col gap-10 items-center justify-between"> <div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Tooltip</h1>
<div className="flex w-full flex-wrap max-w-[680px] justify-center gap-10">
{renderTooltips()}
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Input */}
<h1 className="text-2xl font-bold">Input</h1> <h1 className="text-2xl font-bold">Input</h1>
<div className="flex w-full flex-col gap-10">{renderInputs()}</div> <div className="flex w-full flex-col gap-10">{renderInputs()}</div>
<div className="w-full h border border-gray-200 px-20 my-10" /> <div className="w-full h border border-gray-200 px-20 my-10" />
{/* Button */}
<h1 className="text-2xl font-bold">Button</h1> <h1 className="text-2xl font-bold">Button</h1>
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-10">
{renderButtons()} {renderButtons()}
{renderButtonIcons()} {renderButtonIcons()}
</div> </div>
{/* Link */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Link</h1>
<div className="flex gap-4 items-center justify-center">
{renderLinks()}
</div>
</div>
{/* Disabled button, icon only, and link */}
<div className="flex flex-col gap-10 items-center justify-between">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold text-center">Disabled</h1>
<p className="text-lg text-center text-gray-500">
Button icon only link
</p>
</div>
<div className="flex gap-10 items-center justify-center">
{renderDisabledButtons()}
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" /> <div className="w-full h border border-gray-200 px-20 my-10" />
{/* Badge */} {/* Badge */}
@ -139,6 +176,49 @@ const Page = () => {
<div className="w-full h border border-gray-200 px-20 my-10" /> <div className="w-full h border border-gray-200 px-20 my-10" />
{/* Switch */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Switch</h1>
<div className="flex flex-col gap-10 items-center justify-center">
<Switch
label="Label"
checked={switchValue}
onCheckedChange={setSwitchValue}
/>
<Switch
label="Label"
description="Additional information or context"
checked={switchValue}
onCheckedChange={setSwitchValue}
/>
<Switch disabled label="Disabled unchecked" />
<Switch disabled checked label="Disabled checked" />
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Radio */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Radio</h1>
<div className="flex gap-20 items-start">
<Radio
options={RADIO_OPTIONS}
orientation="vertical"
value={selectedRadio}
onValueChange={setSelectedRadio}
/>
<Radio
options={RADIO_OPTIONS}
orientation="horizontal"
value={selectedRadio}
onValueChange={setSelectedRadio}
/>
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Inline notification */} {/* Inline notification */}
<div className="flex flex-col gap-10 items-center justify-between"> <div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Inline Notification</h1> <h1 className="text-2xl font-bold">Inline Notification</h1>
@ -149,16 +229,6 @@ const Page = () => {
{renderInlineNotificationWithDescriptions()} {renderInlineNotificationWithDescriptions()}
</div> </div>
</div> </div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Link */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Link</h1>
<div className="flex gap-4 items-center justify-center">
{renderLinks()}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,44 +2,45 @@ import { Button, ButtonTheme } from 'components/shared/Button';
import { PlusIcon } from 'components/shared/CustomIcon'; import { PlusIcon } from 'components/shared/CustomIcon';
import React from 'react'; import React from 'react';
export const renderButtons = () => { const buttonVariants = ['primary', 'secondary', 'tertiary', 'danger'] as const;
return ['primary', 'secondary', 'tertiary', 'danger'].map( const buttonSizes = ['lg', 'md', 'sm', 'xs'] as const;
(variant, index) => ( const iconOnlyVariants = [
<div className="flex gap-5 flex-wrap" key={index}>
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
<Button
leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />}
variant={variant as ButtonTheme['variant']}
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'}
key={`${variant}-${size}`}
disabled={size === 'disabled'}
>
Button
</Button>
))}
</div>
),
);
};
export const renderButtonIcons = () => {
return [
'primary', 'primary',
'secondary', 'secondary',
'tertiary', 'tertiary',
'ghost', 'ghost',
'danger', 'danger',
'danger-ghost', 'danger-ghost',
].map((variant, index) => ( ] as const;
const linkVariants = ['link', 'link-emphasized'] as const;
export const renderButtons = () => {
return buttonVariants.map((variant, index) => (
<div className="flex gap-5 flex-wrap" key={index}> <div className="flex gap-5 flex-wrap" key={index}>
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => ( {buttonSizes.map((size) => (
<Button
leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />}
variant={variant as ButtonTheme['variant']}
size={size as ButtonTheme['size']}
key={`${variant}-${size}`}
>
Button
</Button>
))}
</div>
));
};
export const renderButtonIcons = () => {
return iconOnlyVariants.map((variant, index) => (
<div className="flex gap-5 flex-wrap" key={index}>
{buttonSizes.map((size) => (
<Button <Button
iconOnly iconOnly
variant={variant as ButtonTheme['variant']} variant={variant as ButtonTheme['variant']}
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'} size={size as ButtonTheme['size']}
key={`${variant}-${size}`} key={`${variant}-${size}`}
disabled={size === 'disabled'}
> >
<PlusIcon /> <PlusIcon />
</Button> </Button>
@ -49,17 +50,38 @@ export const renderButtonIcons = () => {
}; };
export const renderLinks = () => { export const renderLinks = () => {
return ['link', 'link-emphasized', 'disabled'].map((variant) => ( return linkVariants.map((variant) => (
<Button <Button
variant={ variant={variant}
variant !== 'disabled' ? (variant as ButtonTheme['variant']) : 'link'
}
key={variant} key={variant}
leftIcon={<PlusIcon />} leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />} rightIcon={<PlusIcon />}
disabled={variant === 'disabled'}
> >
Link Link
</Button> </Button>
)); ));
}; };
export const renderDisabledButtons = () => {
return (
<>
{/* Button variants */}
<Button leftIcon={<PlusIcon />} rightIcon={<PlusIcon />} disabled>
Button
</Button>
{/* Icon only variants */}
<Button iconOnly disabled>
<PlusIcon />
</Button>
{/* Link variantws */}
<Button
variant="link"
disabled
leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />}
>
Link
</Button>
</>
);
};

View File

@ -0,0 +1,16 @@
import { RadioOption } from 'components/shared/Radio';
export const RADIO_OPTIONS: RadioOption[] = [
{
label: 'Label 1',
value: '1',
},
{
label: 'Label 2',
value: '2',
},
{
label: 'Label 3',
value: '3',
},
];

View File

@ -0,0 +1,30 @@
import React from 'react';
import { Button } from 'components/shared/Button';
import { Tooltip } from 'components/shared/Tooltip';
import { ContentProps } from 'components/shared/Tooltip/TooltipContent';
const alignments: ContentProps['align'][] = ['start', 'center', 'end'];
const sides: ContentProps['side'][] = ['left', 'top', 'bottom', 'right'];
export const renderTooltips = () => {
const tooltips = sides.map((side) => {
return alignments.map((align) => {
return (
<Tooltip
key={`${side}-${align}`}
content="tooltip content"
contentProps={{ align, side }}
>
<Button
variant="ghost"
key={`${side}-${align}`}
className="h-16 self-center"
>
Tooltip ({side} - {align})
</Button>
</Tooltip>
);
});
});
return tooltips;
};

View File

@ -16,6 +16,7 @@ const DEFAULT_FILTER_VALUE: FilterValue = {
searchedBranch: '', searchedBranch: '',
status: StatusOptions.ALL_STATUS, status: StatusOptions.ALL_STATUS,
}; };
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
const DeploymentsTabPanel = () => { const DeploymentsTabPanel = () => {
const client = useGQLClient(); const client = useGQLClient();
@ -26,22 +27,30 @@ const DeploymentsTabPanel = () => {
const [deployments, setDeployments] = useState<Deployment[]>([]); const [deployments, setDeployments] = useState<Deployment[]>([]);
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]); const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
const fetchDeployments = async () => { const fetchDeployments = useCallback(async () => {
const { deployments } = await client.getDeployments(project.id); const { deployments } = await client.getDeployments(project.id);
setDeployments(deployments); setDeployments(deployments);
}; }, [client, project]);
const fetchProductionBranchDomains = useCallback(async () => { const fetchProductionBranchDomains = useCallback(async () => {
const { domains } = await client.getDomains(project.id, { const { domains } = await client.getDomains(project.id, {
branch: project.prodBranch, branch: project.prodBranch,
}); });
setProdBranchDomains(domains); setProdBranchDomains(domains);
}, []); }, [client, project]);
useEffect(() => { useEffect(() => {
fetchDeployments();
fetchProductionBranchDomains(); fetchProductionBranchDomains();
}, []); fetchDeployments();
const interval = setInterval(() => {
fetchDeployments();
}, FETCH_DEPLOYMENTS_INTERVAL);
return () => {
clearInterval(interval);
};
}, [fetchDeployments, fetchProductionBranchDomains]);
const currentDeployment = useMemo(() => { const currentDeployment = useMemo(() => {
return deployments.find((deployment) => { return deployments.find((deployment) => {

View File

@ -8,10 +8,13 @@ export default withMT({
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}', '../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
], ],
theme: { theme: {
extend: {
zIndex: {
tooltip: '52',
},
fontFamily: { fontFamily: {
sans: ['Inter', 'sans-serif'], sans: ['Inter', 'sans-serif'],
}, },
extend: {
fontSize: { fontSize: {
'2xs': '0.625rem', '2xs': '0.625rem',
'3xs': '0.5rem', '3xs': '0.5rem',
@ -151,6 +154,7 @@ export default withMT({
calendar: calendar:
'0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)', '0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)',
field: '0px 1px 2px rgba(0, 0, 0, 0.04)', field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)',
}, },
spacing: { spacing: {
2.5: '0.625rem', 2.5: '0.625rem',

115
yarn.lock
View File

@ -2032,6 +2032,13 @@
link-module-alias "^1.2.0" link-module-alias "^1.2.0"
shx "^0.3.4" shx "^0.3.4"
"@floating-ui/core@^1.0.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
dependencies:
"@floating-ui/utils" "^0.2.1"
"@floating-ui/core@^1.4.2": "@floating-ui/core@^1.4.2":
version "1.5.2" version "1.5.2"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071"
@ -2047,6 +2054,14 @@
"@floating-ui/core" "^1.4.2" "@floating-ui/core" "^1.4.2"
"@floating-ui/utils" "^0.1.3" "@floating-ui/utils" "^0.1.3"
"@floating-ui/dom@^1.6.1":
version "1.6.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
dependencies:
"@floating-ui/core" "^1.0.0"
"@floating-ui/utils" "^0.2.0"
"@floating-ui/react-dom@^1.2.2": "@floating-ui/react-dom@^1.2.2":
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3"
@ -2054,6 +2069,13 @@
dependencies: dependencies:
"@floating-ui/dom" "^1.2.1" "@floating-ui/dom" "^1.2.1"
"@floating-ui/react-dom@^2.0.0":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d"
integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==
dependencies:
"@floating-ui/dom" "^1.6.1"
"@floating-ui/react@0.19.0": "@floating-ui/react@0.19.0":
version "0.19.0" version "0.19.0"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.0.tgz#d8e19a3fcfaa0684d5ec3f335232b4e0ac0c87e1" resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.0.tgz#d8e19a3fcfaa0684d5ec3f335232b4e0ac0c87e1"
@ -2068,6 +2090,11 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
"@fontsource/inter@^5.0.16": "@fontsource/inter@^5.0.16":
version "5.0.16" version "5.0.16"
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50" resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50"
@ -3284,6 +3311,14 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-arrow@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d"
integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-avatar@^1.0.4": "@radix-ui/react-avatar@^1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
@ -3362,6 +3397,23 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1" "@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-popper@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==
dependencies:
"@babel/runtime" "^7.13.10"
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.0.3"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-use-rect" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/rect" "1.0.1"
"@radix-ui/react-portal@1.0.4": "@radix-ui/react-portal@1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
@ -3387,6 +3439,23 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.2" "@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-radio-group@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3"
integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/react-roving-focus@1.0.4": "@radix-ui/react-roving-focus@1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
@ -3411,6 +3480,20 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1" "@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-switch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e"
integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/react-tabs@^1.0.4": "@radix-ui/react-tabs@^1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"
@ -3443,6 +3526,23 @@
"@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.1" "@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1" "@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-tooltip@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e"
integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-popper" "1.1.3"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-visually-hidden" "1.0.3" "@radix-ui/react-visually-hidden" "1.0.3"
"@radix-ui/react-use-callback-ref@1.0.1": "@radix-ui/react-use-callback-ref@1.0.1":
@ -3482,6 +3582,14 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"
integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/rect" "1.0.1"
"@radix-ui/react-use-size@1.0.1": "@radix-ui/react-use-size@1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
@ -3498,6 +3606,13 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-primitive" "1.0.3"
"@radix-ui/rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@remix-run/router@1.13.1": "@remix-run/router@1.13.1":
version "1.13.1" version "1.13.1"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e"