forked from cerc-io/snowballtools-base
Merge branch 'andrehadianto/design-system-components' into andrehadianto/T-4869-toast
This commit is contained in:
commit
42bf1534fb
26
build-webapp.sh
Executable file
26
build-webapp.sh
Executable 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"
|
@ -25,6 +25,9 @@
|
||||
"allowArgumentsExplicitlyTypedAsAny": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "ignoreRestSiblings": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export interface GitHubConfig {
|
||||
oAuth: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface RegistryConfig {
|
||||
@ -27,7 +27,7 @@ export interface RegistryConfig {
|
||||
amount: string;
|
||||
denom: string;
|
||||
gas: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
|
@ -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';
|
||||
|
||||
// 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';
|
||||
|
@ -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 debug from 'debug';
|
||||
import assert from 'assert';
|
||||
@ -14,6 +20,12 @@ import { ProjectMember } from './entity/ProjectMember';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Domain } from './entity/Domain';
|
||||
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');
|
||||
|
||||
@ -36,6 +48,40 @@ export class Database {
|
||||
async init (): Promise<void> {
|
||||
await this.dataSource.initialize();
|
||||
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> {
|
||||
@ -60,7 +106,18 @@ export class Database {
|
||||
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 organization = await organizationRepository.findOne(options);
|
||||
|
||||
@ -95,7 +152,11 @@ export class Database {
|
||||
|
||||
const project = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoinAndSelect('project.owner', 'owner')
|
||||
@ -108,19 +169,29 @@ export class Database {
|
||||
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 projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.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,
|
||||
organizationSlug
|
||||
})
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
return projects;
|
||||
@ -129,7 +200,9 @@ export class Database {
|
||||
/**
|
||||
* 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 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 deployment = await deploymentRepository.findOne(options);
|
||||
|
||||
@ -182,8 +257,11 @@ export class Database {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
async getProjectMembersByProjectId (
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const projectMembers = await projectMemberRepository.find({
|
||||
relations: {
|
||||
@ -200,8 +278,12 @@ export class Database {
|
||||
return projectMembers;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
|
||||
const environmentVariables = await environmentVariableRepository.find({
|
||||
where: {
|
||||
@ -216,9 +298,12 @@ export class Database {
|
||||
}
|
||||
|
||||
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) {
|
||||
return deleteResult.affected > 0;
|
||||
@ -227,37 +312,63 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
|
||||
async updateProjectMemberById (
|
||||
projectMemberId: string,
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<boolean> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
const updateResult = await projectMemberRepository.update(
|
||||
{ id: projectMemberId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
async addProjectMember (
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<ProjectMember> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
const newProjectMember = await projectMemberRepository.save(data);
|
||||
|
||||
return newProjectMember;
|
||||
}
|
||||
|
||||
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
|
||||
async addEnvironmentVariables (
|
||||
data: DeepPartial<EnvironmentVariable>[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const savedEnvironmentVariables =
|
||||
await environmentVariableRepository.save(data);
|
||||
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
|
||||
async updateEnvironmentVariable (
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const updateResult = await environmentVariableRepository.update(
|
||||
{ id: environmentVariableId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
|
||||
async deleteEnvironmentVariable (
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const deleteResult = await environmentVariableRepository.delete({
|
||||
id: environmentVariableId
|
||||
});
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0;
|
||||
@ -267,7 +378,8 @@ export class Database {
|
||||
}
|
||||
|
||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const projectMemberWithProject = await projectMemberRepository.find({
|
||||
relations: {
|
||||
@ -279,8 +391,7 @@ export class Database {
|
||||
where: {
|
||||
id: projectMemberId
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (projectMemberWithProject.length === 0) {
|
||||
throw new Error('Member does not exist');
|
||||
@ -289,34 +400,49 @@ export class Database {
|
||||
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 projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.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,
|
||||
searchText: `%${searchText}%`
|
||||
})
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 updateResult = await deploymentRepository.update(criteria, data);
|
||||
|
||||
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 updateResult = await deploymentRepository
|
||||
@ -329,7 +455,11 @@ export class Database {
|
||||
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);
|
||||
|
||||
// TODO: Check if organization exists
|
||||
@ -352,9 +482,15 @@ export class Database {
|
||||
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 updateResult = await projectRepository.update({ id: projectId }, data);
|
||||
const updateResult = await projectRepository.update(
|
||||
{ id: projectId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
@ -401,14 +537,20 @@ export class Database {
|
||||
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 updateResult = await domainRepository.update({ id: domainId }, data);
|
||||
|
||||
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 domains = await domainRepository.find({
|
||||
|
@ -26,18 +26,27 @@ export enum DeploymentStatus {
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRequest {
|
||||
type: string;
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
config: string;
|
||||
meta: string;
|
||||
}
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string;
|
||||
version:string
|
||||
name: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
author?: string
|
||||
repository?: string[],
|
||||
app_version?: string
|
||||
repository_ref: string
|
||||
app_type: string
|
||||
version: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
license?: string;
|
||||
author?: string;
|
||||
repository?: string[];
|
||||
app_version?: string;
|
||||
repository_ref: string;
|
||||
app_type: string;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
@ -78,6 +87,12 @@ export class Deployment {
|
||||
@Column('simple-json')
|
||||
applicationRecordData!: ApplicationRecord;
|
||||
|
||||
@Column('varchar')
|
||||
applicationDeploymentRequestId!: string;
|
||||
|
||||
@Column('simple-json')
|
||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRecordId!: string | null;
|
||||
|
||||
|
@ -27,8 +27,12 @@ export class Organization {
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(userOrganization) => userOrganization.organization,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
@ -15,15 +15,6 @@ import { Organization } from './Organization';
|
||||
import { ProjectMember } from './ProjectMember';
|
||||
import { Deployment } from './Deployment';
|
||||
|
||||
export interface ApplicationDeploymentRequest {
|
||||
type: string
|
||||
version: string
|
||||
name: string
|
||||
application: string
|
||||
config: string,
|
||||
meta: string
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Project {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -52,12 +43,6 @@ export class Project {
|
||||
@Column('varchar', { length: 255, default: 'main' })
|
||||
prodBranch!: string;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRequestId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
|
||||
|
||||
@Column('text', { default: '' })
|
||||
description!: string;
|
||||
|
||||
@ -91,7 +76,7 @@ export class Project {
|
||||
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||
deployments!: Deployment[];
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
|
@ -15,7 +15,7 @@ import { User } from './User';
|
||||
|
||||
export enum Permission {
|
||||
View = 'View',
|
||||
Edit = 'Edit'
|
||||
Edit = 'Edit',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
|
@ -34,13 +34,17 @@ export class User {
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
|
||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(UserOrganization) => UserOrganization.member,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
@ -19,7 +19,9 @@ const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||
|
||||
export const main = async (): Promise<void> => {
|
||||
// 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({
|
||||
clientType: OAUTH_CLIENT_TYPE,
|
||||
@ -31,9 +33,16 @@ export const main = async (): Promise<void> => {
|
||||
await db.init();
|
||||
|
||||
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);
|
||||
|
||||
await createAndStartServer(server, typeDefs, resolvers, service);
|
||||
|
@ -6,8 +6,11 @@ import { DateTime } from 'luxon';
|
||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import { ApplicationDeploymentRequest } from './entity/Project';
|
||||
import { ApplicationRecord, Deployment } from './entity/Deployment';
|
||||
import {
|
||||
ApplicationRecord,
|
||||
Deployment,
|
||||
ApplicationDeploymentRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
@ -23,33 +26,52 @@ export class Registry {
|
||||
|
||||
constructor (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 ({
|
||||
appName,
|
||||
packageJSON,
|
||||
commitHash,
|
||||
appType,
|
||||
repoUrl
|
||||
}: {
|
||||
packageJSON: PackageJSON
|
||||
commitHash: string,
|
||||
appType: string,
|
||||
repoUrl: string
|
||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
appName: string;
|
||||
packageJSON: PackageJSON;
|
||||
commitHash: string;
|
||||
appType: string;
|
||||
repoUrl: string;
|
||||
}): Promise<{
|
||||
applicationRecordId: string;
|
||||
applicationRecordData: ApplicationRecord;
|
||||
}> {
|
||||
// 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
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords({
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_RECORD_TYPE,
|
||||
name: packageJSON.name
|
||||
}, true);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Get next version of record
|
||||
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
|
||||
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');
|
||||
const bondRecords = records.filter(
|
||||
(record: any) => record.bondId === this.registryConfig.bondId
|
||||
);
|
||||
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');
|
||||
|
||||
@ -60,11 +82,16 @@ export class Registry {
|
||||
repository_ref: commitHash,
|
||||
repository: [repoUrl],
|
||||
app_type: appType,
|
||||
name: packageJSON.name,
|
||||
name: appName,
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(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 })
|
||||
};
|
||||
|
||||
@ -81,26 +108,45 @@ export class Registry {
|
||||
log('Application record data:', applicationRecord);
|
||||
|
||||
// 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}`);
|
||||
|
||||
await this.registry.setName({ cid: result.data.id, crn }, 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);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
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: {
|
||||
appName: string,
|
||||
commitHash: string,
|
||||
repository: string,
|
||||
environmentVariables: { [key: string]: string }
|
||||
appName: string;
|
||||
packageJsonName: string;
|
||||
commitHash: string;
|
||||
repository: string;
|
||||
environmentVariables: { [key: string]: string };
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string,
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
}> {
|
||||
const crn = this.getCrn(data.appName);
|
||||
const crn = this.getCrn(data.packageJsonName, data.appName);
|
||||
const records = await this.registry.resolveNames([crn]);
|
||||
const applicationRecord = records[0];
|
||||
|
||||
@ -124,7 +170,9 @@ export class Registry {
|
||||
env: data.environmentVariables
|
||||
}),
|
||||
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_ref: data.commitHash
|
||||
})
|
||||
@ -142,31 +190,40 @@ export class Registry {
|
||||
log(`Application deployment request record published: ${result.data.id}`);
|
||||
log('Application deployment request data:', applicationDeploymentRequest);
|
||||
|
||||
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
|
||||
return {
|
||||
applicationDeploymentRequestId: result.data.id,
|
||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords for deployments
|
||||
*/
|
||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
||||
async getDeploymentRecords (
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRecord[]> {
|
||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||
// 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
|
||||
}, true);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// 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 {
|
||||
const [arg1, arg2] = packageJsonName.split('/');
|
||||
|
||||
if (arg2) {
|
||||
getCrn (packageJsonName: string, appName: string): string {
|
||||
const [arg1] = packageJsonName.split('/');
|
||||
const authority = arg1.replace('@', '');
|
||||
return `crn://${authority}/applications/${arg2}`;
|
||||
}
|
||||
|
||||
return `crn://${arg1}/applications/${arg1}`;
|
||||
return `crn://${authority}/applications/${appName}`;
|
||||
}
|
||||
}
|
||||
|
@ -25,15 +25,25 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getProjectById(projectId);
|
||||
},
|
||||
|
||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
||||
return service.getProjectsInOrganization(context.userId, organizationSlug);
|
||||
projectsInOrganization: async (
|
||||
_: any,
|
||||
{ organizationSlug }: { organizationSlug: string },
|
||||
context: any
|
||||
) => {
|
||||
return service.getProjectsInOrganization(
|
||||
context.userId,
|
||||
organizationSlug
|
||||
);
|
||||
},
|
||||
|
||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getDeploymentsByProjectId(projectId);
|
||||
},
|
||||
|
||||
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
||||
environmentVariables: async (
|
||||
_: any,
|
||||
{ projectId }: { projectId: string }
|
||||
) => {
|
||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||
},
|
||||
|
||||
@ -41,32 +51,55 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
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);
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
Mutation: {
|
||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
||||
removeProjectMember: async (
|
||||
_: any,
|
||||
{ projectMemberId }: { projectMemberId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.removeProjectMember(context.userId, projectMemberId);
|
||||
return await service.removeProjectMember(
|
||||
context.userId,
|
||||
projectMemberId
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateProjectMember: async (_: any, { projectMemberId, data }: {
|
||||
projectMemberId: string,
|
||||
updateProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectMemberId,
|
||||
data
|
||||
}: {
|
||||
projectMemberId: string;
|
||||
data: {
|
||||
permissions: Permission[]
|
||||
permissions: Permission[];
|
||||
};
|
||||
}
|
||||
}) => {
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProjectMember(projectMemberId, data);
|
||||
} catch (err) {
|
||||
@ -75,13 +108,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
addProjectMember: async (_: any, { projectId, data }: {
|
||||
projectId: string,
|
||||
addProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data
|
||||
}: {
|
||||
projectId: string;
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
}
|
||||
}) => {
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addProjectMember(projectId, data));
|
||||
} 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 {
|
||||
return Boolean(await service.addEnvironmentVariables(projectId, data));
|
||||
return Boolean(
|
||||
await service.addEnvironmentVariables(projectId, data)
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
|
||||
updateEnvironmentVariable: async (
|
||||
_: any,
|
||||
{
|
||||
environmentVariableId,
|
||||
data
|
||||
}: {
|
||||
environmentVariableId: string;
|
||||
data: DeepPartial<EnvironmentVariable>;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await service.updateEnvironmentVariable(environmentVariableId, data);
|
||||
return await service.updateEnvironmentVariable(
|
||||
environmentVariableId,
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
||||
removeEnvironmentVariable: async (
|
||||
_: any,
|
||||
{ environmentVariableId }: { environmentVariableId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||
} 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 {
|
||||
return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId));
|
||||
return Boolean(
|
||||
await service.updateDeploymentToProd(context.userId, deploymentId)
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
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 {
|
||||
return await service.addProject(context.userId, organizationSlug, data);
|
||||
return await service.addProject(
|
||||
context.userId,
|
||||
organizationSlug,
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
|
||||
updateProject: async (
|
||||
_: any,
|
||||
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProject(projectId, data);
|
||||
} 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 {
|
||||
return Boolean(await service.redeployToProd(context.userId, deploymentId));
|
||||
return Boolean(
|
||||
await service.redeployToProd(context.userId, deploymentId)
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@ -157,7 +248,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
try {
|
||||
return await service.deleteProject(projectId);
|
||||
} 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 {
|
||||
return await service.rollbackDeployment(projectId, deploymentId);
|
||||
} 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 {
|
||||
return Boolean(await service.addDomain(projectId, data));
|
||||
} 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 {
|
||||
return await service.updateDomain(domainId, data);
|
||||
} 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 {
|
||||
return await service.authenticateGitHub(code, context.userId);
|
||||
} catch (err) {
|
||||
@ -208,7 +316,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
|
||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||
try {
|
||||
return service.unauthenticateGitHub(context.userId, { gitHubToken: null });
|
||||
return service.unauthenticateGitHub(context.userId, {
|
||||
gitHubToken: null
|
||||
});
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
|
@ -188,10 +188,19 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
||||
updateProjectMember(
|
||||
projectMemberId: String!
|
||||
data: UpdateProjectMemberInput
|
||||
): Boolean!
|
||||
removeProjectMember(projectMemberId: String!): Boolean!
|
||||
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
|
||||
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
|
||||
addEnvironmentVariables(
|
||||
projectId: String!
|
||||
data: [AddEnvironmentVariableInput!]
|
||||
): Boolean!
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: String!
|
||||
data: UpdateEnvironmentVariableInput!
|
||||
): Boolean!
|
||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||
|
@ -15,15 +15,15 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
||||
import { User } from './entity/User';
|
||||
import { Registry } from './registry';
|
||||
import { GitHubConfig, RegistryConfig } from './config';
|
||||
import { AppDeploymentRecord, GitPushEventPayload } from './types';
|
||||
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:service');
|
||||
|
||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
||||
|
||||
interface Config {
|
||||
gitHubConfig: GitHubConfig
|
||||
registryConfig: RegistryConfig
|
||||
gitHubConfig: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
}
|
||||
|
||||
export class Service {
|
||||
@ -71,7 +71,9 @@ export class Service {
|
||||
});
|
||||
|
||||
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
|
||||
const records = await this.registry.getDeploymentRecords(deployments);
|
||||
@ -91,10 +93,12 @@ export class Service {
|
||||
/**
|
||||
* Update deployments with ApplicationDeploymentRecord data
|
||||
*/
|
||||
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
|
||||
async updateDeploymentsWithRecordData (
|
||||
records: AppDeploymentRecord[]
|
||||
): Promise<void> {
|
||||
// Get deployments for ApplicationDeploymentRecords
|
||||
const deployments = await this.db.getDeployments({
|
||||
where: records.map(record => ({
|
||||
where: records.map((record) => ({
|
||||
applicationRecordId: record.attributes.application
|
||||
})),
|
||||
order: {
|
||||
@ -103,38 +107,46 @@ export class Service {
|
||||
});
|
||||
|
||||
// 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) {
|
||||
acc.add(deployment.projectId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
},
|
||||
new Set<string>()
|
||||
);
|
||||
|
||||
// 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;
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// Update deployment data for ApplicationDeploymentRecords
|
||||
const deploymentUpdatePromises = records.map(async (record) => {
|
||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||
|
||||
await this.db.updateDeploymentById(
|
||||
deployment.id,
|
||||
{
|
||||
await this.db.updateDeploymentById(deployment.id, {
|
||||
applicationDeploymentRecordId: record.id,
|
||||
applicationDeploymentRecordData: record.attributes,
|
||||
url: record.attributes.url,
|
||||
status: DeploymentStatus.Ready,
|
||||
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);
|
||||
@ -150,7 +162,10 @@ export class Service {
|
||||
|
||||
async getOctokit (userId: string): Promise<Octokit> {
|
||||
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 });
|
||||
}
|
||||
@ -165,8 +180,14 @@ export class Service {
|
||||
return dbProject;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> {
|
||||
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug);
|
||||
async getProjectsInOrganization (
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
): Promise<Project[]> {
|
||||
const dbProjects = await this.db.getProjectsInOrganization(
|
||||
userId,
|
||||
organizationSlug
|
||||
);
|
||||
return dbProjects;
|
||||
}
|
||||
|
||||
@ -175,35 +196,52 @@ export class Service {
|
||||
return dbDeployments;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> {
|
||||
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
projectId: string
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const dbEnvironmentVariables =
|
||||
await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||
return dbEnvironmentVariables;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
||||
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId);
|
||||
async getProjectMembersByProjectId (
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const dbProjectMembers =
|
||||
await this.db.getProjectMembersByProjectId(projectId);
|
||||
return dbProjectMembers;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
async addProjectMember (projectId: string,
|
||||
async addProjectMember (
|
||||
projectId: string,
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
}): Promise<ProjectMember> {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
): Promise<ProjectMember> {
|
||||
// TODO: Send invitation
|
||||
let user = await this.db.getUser({
|
||||
where: {
|
||||
@ -231,7 +269,10 @@ export class Service {
|
||||
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);
|
||||
|
||||
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[]> {
|
||||
const formattedEnvironmentVariables = data.map((environmentVariable) => {
|
||||
async addEnvironmentVariables (
|
||||
projectId: string,
|
||||
data: { environments: string[]; key: string; value: string }[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const formattedEnvironmentVariables = data
|
||||
.map((environmentVariable) => {
|
||||
return environmentVariable.environments.map((environment) => {
|
||||
return ({
|
||||
return {
|
||||
key: environmentVariable.key,
|
||||
value: environmentVariable.value,
|
||||
environment: environment as Environment,
|
||||
project: Object.assign(new Project(), {
|
||||
id: projectId
|
||||
})
|
||||
};
|
||||
});
|
||||
});
|
||||
}).flat();
|
||||
})
|
||||
.flat();
|
||||
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
|
||||
formattedEnvironmentVariables
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
async removeEnvironmentVariable (
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
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({
|
||||
where: { id: deploymentId },
|
||||
relations: {
|
||||
@ -286,13 +342,14 @@ export class Service {
|
||||
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 newDeployment = await this.createDeployment(userId,
|
||||
octokit,
|
||||
{
|
||||
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||
project: oldDeployment.project,
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
@ -311,7 +368,9 @@ export class Service {
|
||||
recordData: { repoUrl?: string } = {}
|
||||
): Promise<Deployment> {
|
||||
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 { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
@ -326,28 +385,61 @@ export class Service {
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
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
|
||||
// TODO: Fix unique constraint error for domain
|
||||
await this.db.updateDeployment({
|
||||
await this.db.updateDeployment(
|
||||
{
|
||||
domainId: data.domain?.id
|
||||
}, {
|
||||
},
|
||||
{
|
||||
domain: null
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const newDeployment = await this.db.addDeployment({
|
||||
project: data.project,
|
||||
@ -358,17 +450,25 @@ export class Service {
|
||||
status: DeploymentStatus.Building,
|
||||
applicationRecordId,
|
||||
applicationRecordData,
|
||||
applicationDeploymentRequestId,
|
||||
applicationDeploymentRequestData,
|
||||
domain: data.domain,
|
||||
createdBy: Object.assign(new User(), {
|
||||
id: userId
|
||||
})
|
||||
});
|
||||
|
||||
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
|
||||
log(
|
||||
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`
|
||||
);
|
||||
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({
|
||||
where: {
|
||||
slug: organizationSlug
|
||||
@ -383,7 +483,9 @@ export class Service {
|
||||
const octokit = await this.getOctokit(userId);
|
||||
const [owner, repo] = project.repository.split('/');
|
||||
|
||||
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({
|
||||
const {
|
||||
data: [latestCommit]
|
||||
} = await octokit.rest.repos.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
sha: project.prodBranch,
|
||||
@ -393,7 +495,8 @@ export class Service {
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||
|
||||
// Create deployment with prod branch and latest commit
|
||||
const newDeployment = await this.createDeployment(userId,
|
||||
await this.createDeployment(
|
||||
userId,
|
||||
octokit,
|
||||
{
|
||||
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);
|
||||
|
||||
return project;
|
||||
@ -441,7 +523,10 @@ export class Service {
|
||||
owner,
|
||||
repo,
|
||||
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'
|
||||
},
|
||||
events: ['push']
|
||||
@ -449,9 +534,13 @@ export class Service {
|
||||
} catch (err) {
|
||||
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
||||
if (
|
||||
!(err instanceof RequestError &&
|
||||
!(
|
||||
err instanceof RequestError &&
|
||||
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;
|
||||
}
|
||||
@ -463,7 +552,9 @@ export class Service {
|
||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||
const { repository, ref, head_commit: headCommit } = data;
|
||||
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) {
|
||||
log(`No projects found for repository ${repository.full_name}`);
|
||||
@ -475,15 +566,18 @@ export class Service {
|
||||
|
||||
for await (const project of projects) {
|
||||
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
|
||||
await this.createDeployment(project.ownerId,
|
||||
octokit,
|
||||
{
|
||||
await this.createDeployment(project.ownerId, octokit, {
|
||||
project,
|
||||
branch,
|
||||
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
||||
environment:
|
||||
project.prodBranch === branch
|
||||
? Environment.Production
|
||||
: Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
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);
|
||||
}
|
||||
|
||||
@ -508,13 +605,18 @@ export class Service {
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
||||
async redeployToProd (
|
||||
userId: string,
|
||||
deploymentId: string
|
||||
): Promise<Deployment> {
|
||||
const oldDeployment = await this.db.getDeployment({
|
||||
relations: {
|
||||
project: true,
|
||||
@ -532,9 +634,7 @@ export class Service {
|
||||
|
||||
const octokit = await this.getOctokit(userId);
|
||||
|
||||
const newDeployment = await this.createDeployment(userId,
|
||||
octokit,
|
||||
{
|
||||
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||
project: oldDeployment.project,
|
||||
// TODO: Put isCurrent field in project
|
||||
branch: oldDeployment.branch,
|
||||
@ -547,7 +647,10 @@ export class Service {
|
||||
return newDeployment;
|
||||
}
|
||||
|
||||
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
||||
async rollbackDeployment (
|
||||
projectId: string,
|
||||
deploymentId: string
|
||||
): Promise<boolean> {
|
||||
// TODO: Implement transactions
|
||||
const oldCurrentDeployment = await this.db.getDeployment({
|
||||
relations: {
|
||||
@ -565,16 +668,25 @@ export class Service {
|
||||
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;
|
||||
}
|
||||
|
||||
async addDomain (projectId: string, data: { name: string }): Promise<{
|
||||
primaryDomain: Domain,
|
||||
redirectedDomain: Domain
|
||||
async addDomain (
|
||||
projectId: string,
|
||||
data: { name: string }
|
||||
): Promise<{
|
||||
primaryDomain: Domain;
|
||||
redirectedDomain: Domain;
|
||||
}> {
|
||||
const currentProject = await this.db.getProjectById(projectId);
|
||||
|
||||
@ -599,12 +711,20 @@ export class Service {
|
||||
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({
|
||||
where: {
|
||||
id: domainId
|
||||
@ -645,7 +765,9 @@ export class Service {
|
||||
}
|
||||
|
||||
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;
|
||||
@ -656,8 +778,13 @@ export class Service {
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
|
||||
const { authentication: { token } } = await this.oauthApp.createToken({
|
||||
async authenticateGitHub (
|
||||
code: string,
|
||||
userId: string
|
||||
): Promise<{ token: string }> {
|
||||
const {
|
||||
authentication: { token }
|
||||
} = await this.oauthApp.createToken({
|
||||
code
|
||||
});
|
||||
|
||||
@ -666,7 +793,10 @@ export class Service {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
export interface PackageJSON {
|
||||
name?: string;
|
||||
version?: string;
|
||||
name: string;
|
||||
version: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
@ -47,5 +47,5 @@ interface RegistryRecord {
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRecordAttributes
|
||||
attributes: AppDeploymentRecordAttributes;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import toml from 'toml';
|
||||
import debug from 'debug';
|
||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
|
||||
const log = debug('snowball:utils');
|
||||
|
||||
@ -19,3 +20,49 @@ export const getConfig = async <ConfigType>(
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -18,4 +18,4 @@ const main = async () => {
|
||||
deleteFile(config.database.dbPath);
|
||||
};
|
||||
|
||||
main().catch(err => log(err));
|
||||
main().catch((err) => log(err));
|
||||
|
22
packages/backend/test/fixtures/deployments.json
vendored
22
packages/backend/test/fixtures/deployments.json
vendored
@ -9,6 +9,8 @@
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -24,6 +26,8 @@
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -39,6 +43,8 @@
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -54,6 +60,8 @@
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "prod",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -69,6 +77,8 @@
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -84,6 +94,8 @@
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -99,6 +111,8 @@
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -114,6 +128,8 @@
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -129,6 +145,8 @@
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -144,6 +162,8 @@
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
@ -159,6 +179,8 @@
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
|
@ -2,77 +2,55 @@
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 0,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 0,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 1,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 2,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 2,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 3,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 3,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 4,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 4,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
}
|
||||
]
|
||||
|
10
packages/backend/test/fixtures/projects.json
vendored
10
packages/backend/test/fixtures/projects.json
vendored
@ -10,8 +10,6 @@
|
||||
"framework": "test",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"subDomain": "testProject.snowball.xyz"
|
||||
},
|
||||
{
|
||||
@ -25,8 +23,6 @@
|
||||
"framework": "test-2",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"subDomain": "testProject-2.snowball.xyz"
|
||||
},
|
||||
{
|
||||
@ -40,8 +36,6 @@
|
||||
"framework": "test-3",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"subDomain": "iglootools.snowball.xyz"
|
||||
},
|
||||
{
|
||||
@ -55,8 +49,6 @@
|
||||
"framework": "test-4",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"subDomain": "iglootools-2.snowball.xyz"
|
||||
},
|
||||
{
|
||||
@ -70,8 +62,6 @@
|
||||
"framework": "test-5",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"subDomain": "snowball-2.snowball.xyz"
|
||||
}
|
||||
]
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
import * as fs from 'fs/promises';
|
||||
import { DataSource } from 'typeorm';
|
||||
import debug from 'debug';
|
||||
import path from 'path';
|
||||
|
||||
@ -11,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
|
||||
import { Domain } from '../src/entity/Domain';
|
||||
import { ProjectMember } from '../src/entity/ProjectMember';
|
||||
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 { 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 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 savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
|
||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
const userEntities = await getEntities(
|
||||
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 = {
|
||||
owner: savedUsers,
|
||||
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);
|
||||
|
||||
@ -71,14 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
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 = {
|
||||
project: savedProjects,
|
||||
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();
|
||||
|
||||
@ -87,14 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
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 = {
|
||||
member: savedUsers,
|
||||
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 = {
|
||||
project: savedProjects,
|
||||
@ -102,23 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
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 = {
|
||||
project: savedProjects
|
||||
};
|
||||
|
||||
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
|
||||
};
|
||||
|
||||
const checkFileExists = async (filePath: string) => {
|
||||
try {
|
||||
await fs.access(filePath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
const environmentVariablesEntities = await getEntities(
|
||||
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
EnvironmentVariable,
|
||||
dataSource,
|
||||
environmentVariablesEntities,
|
||||
environmentVariableRelations
|
||||
);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
|
@ -17,16 +17,32 @@ const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
|
||||
async function main () {
|
||||
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);
|
||||
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) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||
await registry.reserveAuthority(
|
||||
{ name },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,15 @@ import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||
const log = debug('snowball:publish-deploy-records');
|
||||
|
||||
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({
|
||||
type: 'better-sqlite3',
|
||||
@ -53,7 +59,7 @@ async function main () {
|
||||
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
||||
}),
|
||||
|
||||
request: deployment.project.applicationDeploymentRequestId,
|
||||
request: deployment.applicationDeploymentRequestId,
|
||||
url
|
||||
};
|
||||
|
||||
|
@ -7,8 +7,11 @@
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^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-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
|
@ -98,19 +98,29 @@ const Button = forwardRef<
|
||||
href,
|
||||
...baseLinkProps,
|
||||
};
|
||||
return <a {...externalLinkProps}>{_children}</a>;
|
||||
return (
|
||||
// @ts-expect-error - ref
|
||||
<a ref={ref} {...externalLinkProps}>
|
||||
{_children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Internal link
|
||||
return (
|
||||
<Link {...baseLinkProps} to={href}>
|
||||
// @ts-expect-error - ref
|
||||
<Link ref={ref} {...baseLinkProps} to={href}>
|
||||
{_children}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
const { ...buttonProps } = _props;
|
||||
return (
|
||||
// @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 (
|
||||
<Component
|
||||
{...props}
|
||||
// @ts-expect-error - ref is not a valid prop for button elements
|
||||
ref={ref}
|
||||
className={buttonTheme({ ...styleProps, class: className })}
|
||||
>
|
||||
{cloneIcon(leftIcon, { ...iconSize })}
|
||||
|
@ -50,6 +50,7 @@ export const inputTheme = tv(
|
||||
'outline-offset-0',
|
||||
'outline-border-danger',
|
||||
'shadow-none',
|
||||
'focus:outline-border-danger',
|
||||
],
|
||||
helperText: 'text-elements-danger',
|
||||
},
|
||||
|
54
packages/frontend/src/components/shared/Radio/Radio.theme.ts
Normal file
54
packages/frontend/src/components/shared/Radio/Radio.theme.ts
Normal 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>;
|
63
packages/frontend/src/components/shared/Radio/Radio.tsx
Normal file
63
packages/frontend/src/components/shared/Radio/Radio.tsx
Normal 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>
|
||||
);
|
||||
};
|
74
packages/frontend/src/components/shared/Radio/RadioItem.tsx
Normal file
74
packages/frontend/src/components/shared/Radio/RadioItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
2
packages/frontend/src/components/shared/Radio/index.ts
Normal file
2
packages/frontend/src/components/shared/Radio/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './Radio';
|
||||
export * from './RadioItem';
|
@ -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>;
|
85
packages/frontend/src/components/shared/Switch/Switch.tsx
Normal file
85
packages/frontend/src/components/shared/Switch/Switch.tsx
Normal 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;
|
||||
};
|
1
packages/frontend/src/components/shared/Switch/index.ts
Normal file
1
packages/frontend/src/components/shared/Switch/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './Switch';
|
@ -9,6 +9,8 @@ export const tabsTheme = tv({
|
||||
// Horizontal – default
|
||||
'px-1',
|
||||
'pb-5',
|
||||
'cursor-default',
|
||||
'select-none',
|
||||
'text-elements-low-em',
|
||||
'border-b-2',
|
||||
'border-transparent',
|
||||
|
@ -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'],
|
||||
},
|
||||
});
|
47
packages/frontend/src/components/shared/Tooltip/Tooltip.tsx
Normal file
47
packages/frontend/src/components/shared/Tooltip/Tooltip.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './TooltipContent';
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './TooltipTrigger';
|
2
packages/frontend/src/components/shared/Tooltip/index.ts
Normal file
2
packages/frontend/src/components/shared/Tooltip/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './Tooltip';
|
||||
export * from './TooltipBase';
|
@ -10,6 +10,7 @@ import { renderBadges } from './renders/badge';
|
||||
import {
|
||||
renderButtonIcons,
|
||||
renderButtons,
|
||||
renderDisabledButtons,
|
||||
renderLinks,
|
||||
} from './renders/button';
|
||||
import {
|
||||
@ -17,16 +18,22 @@ import {
|
||||
renderTabs,
|
||||
renderVerticalTabs,
|
||||
} from './renders/tabs';
|
||||
import { Switch } from 'components/shared/Switch';
|
||||
import { RADIO_OPTIONS } from './renders/radio';
|
||||
import { Radio } from 'components/shared/Radio';
|
||||
import {
|
||||
renderInlineNotificationWithDescriptions,
|
||||
renderInlineNotifications,
|
||||
} from './renders/inlineNotifications';
|
||||
import { renderInputs } from './renders/input';
|
||||
import { renderToast, renderToastsWithCta } from './renders/toast';
|
||||
import { renderTooltips } from './renders/tooltip';
|
||||
|
||||
const Page = () => {
|
||||
const [singleDate, setSingleDate] = useState<Value>();
|
||||
const [dateRange, setDateRange] = useState<Value>();
|
||||
const [switchValue, setSwitchValue] = useState(false);
|
||||
const [selectedRadio, setSelectedRadio] = useState<string>('');
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-full">
|
||||
@ -52,17 +59,47 @@ const Page = () => {
|
||||
|
||||
{/* Button */}
|
||||
<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>
|
||||
<div className="flex w-full flex-col gap-10">{renderInputs()}</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Button */}
|
||||
<h1 className="text-2xl font-bold">Button</h1>
|
||||
<div className="flex flex-col gap-10">
|
||||
{renderButtons()}
|
||||
{renderButtonIcons()}
|
||||
</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" />
|
||||
|
||||
{/* Badge */}
|
||||
@ -139,6 +176,49 @@ const Page = () => {
|
||||
|
||||
<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 */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Inline Notification</h1>
|
||||
@ -149,16 +229,6 @@ const Page = () => {
|
||||
{renderInlineNotificationWithDescriptions()}
|
||||
</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>
|
||||
|
@ -2,44 +2,45 @@ import { Button, ButtonTheme } from 'components/shared/Button';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import React from 'react';
|
||||
|
||||
export const renderButtons = () => {
|
||||
return ['primary', 'secondary', 'tertiary', 'danger'].map(
|
||||
(variant, index) => (
|
||||
<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 [
|
||||
const buttonVariants = ['primary', 'secondary', 'tertiary', 'danger'] as const;
|
||||
const buttonSizes = ['lg', 'md', 'sm', 'xs'] as const;
|
||||
const iconOnlyVariants = [
|
||||
'primary',
|
||||
'secondary',
|
||||
'tertiary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'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}>
|
||||
{['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
|
||||
iconOnly
|
||||
variant={variant as ButtonTheme['variant']}
|
||||
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'}
|
||||
size={size as ButtonTheme['size']}
|
||||
key={`${variant}-${size}`}
|
||||
disabled={size === 'disabled'}
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
@ -49,17 +50,38 @@ export const renderButtonIcons = () => {
|
||||
};
|
||||
|
||||
export const renderLinks = () => {
|
||||
return ['link', 'link-emphasized', 'disabled'].map((variant) => (
|
||||
return linkVariants.map((variant) => (
|
||||
<Button
|
||||
variant={
|
||||
variant !== 'disabled' ? (variant as ButtonTheme['variant']) : 'link'
|
||||
}
|
||||
variant={variant}
|
||||
key={variant}
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
disabled={variant === 'disabled'}
|
||||
>
|
||||
Link
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
16
packages/frontend/src/pages/components/renders/radio.ts
Normal file
16
packages/frontend/src/pages/components/renders/radio.ts
Normal 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',
|
||||
},
|
||||
];
|
30
packages/frontend/src/pages/components/renders/tooltip.tsx
Normal file
30
packages/frontend/src/pages/components/renders/tooltip.tsx
Normal 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;
|
||||
};
|
@ -16,6 +16,7 @@ const DEFAULT_FILTER_VALUE: FilterValue = {
|
||||
searchedBranch: '',
|
||||
status: StatusOptions.ALL_STATUS,
|
||||
};
|
||||
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
|
||||
|
||||
const DeploymentsTabPanel = () => {
|
||||
const client = useGQLClient();
|
||||
@ -26,22 +27,30 @@ const DeploymentsTabPanel = () => {
|
||||
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
||||
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
||||
|
||||
const fetchDeployments = async () => {
|
||||
const fetchDeployments = useCallback(async () => {
|
||||
const { deployments } = await client.getDeployments(project.id);
|
||||
setDeployments(deployments);
|
||||
};
|
||||
}, [client, project]);
|
||||
|
||||
const fetchProductionBranchDomains = useCallback(async () => {
|
||||
const { domains } = await client.getDomains(project.id, {
|
||||
branch: project.prodBranch,
|
||||
});
|
||||
setProdBranchDomains(domains);
|
||||
}, []);
|
||||
}, [client, project]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDeployments();
|
||||
fetchProductionBranchDomains();
|
||||
}, []);
|
||||
fetchDeployments();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDeployments();
|
||||
}, FETCH_DEPLOYMENTS_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [fetchDeployments, fetchProductionBranchDomains]);
|
||||
|
||||
const currentDeployment = useMemo(() => {
|
||||
return deployments.find((deployment) => {
|
||||
|
@ -8,10 +8,13 @@ export default withMT({
|
||||
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
zIndex: {
|
||||
tooltip: '52',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
extend: {
|
||||
fontSize: {
|
||||
'2xs': '0.625rem',
|
||||
'3xs': '0.5rem',
|
||||
@ -151,6 +154,7 @@ export default withMT({
|
||||
calendar:
|
||||
'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)',
|
||||
inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)',
|
||||
},
|
||||
spacing: {
|
||||
2.5: '0.625rem',
|
||||
|
115
yarn.lock
115
yarn.lock
@ -2032,6 +2032,13 @@
|
||||
link-module-alias "^1.2.0"
|
||||
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":
|
||||
version "1.5.2"
|
||||
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/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":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3"
|
||||
@ -2054,6 +2069,13 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "0.19.0"
|
||||
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"
|
||||
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":
|
||||
version "5.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50"
|
||||
@ -3284,6 +3311,14 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "1.0.4"
|
||||
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"
|
||||
"@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":
|
||||
version "1.0.4"
|
||||
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"
|
||||
"@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":
|
||||
version "1.0.4"
|
||||
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"
|
||||
"@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":
|
||||
version "1.0.4"
|
||||
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-controllable-state" "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-use-callback-ref@1.0.1":
|
||||
@ -3482,6 +3582,14 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "1.0.1"
|
||||
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"
|
||||
"@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":
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e"
|
||||
|
Loading…
Reference in New Issue
Block a user