Compare commits

..
69 changed files with 517 additions and 2744 deletions
-26
View File
@@ -1,26 +0,0 @@
#!/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"
+1 -4
View File
@@ -25,9 +25,6 @@
"allowArgumentsExplicitlyTypedAsAny": true
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{ "ignoreRestSiblings": true }
]
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
}
}
+2 -2
View File
@@ -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 {
+3 -9
View File
@@ -1,14 +1,8 @@
import process from 'process';
export const DEFAULT_CONFIG_FILE_PATH =
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
export const DEFAULT_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 =
process.env.SNOWBALL_BACKEND_USER_ID ||
'59f4355d-9549-4aac-9b54-eeefceeabef0';
export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0';
export const PROJECT_DOMAIN =
process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz';
export const PROJECT_DOMAIN = 'snowball.xyz';
+47 -189
View File
@@ -1,10 +1,4 @@
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';
@@ -20,12 +14,6 @@ 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');
@@ -48,40 +36,6 @@ 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> {
@@ -106,18 +60,7 @@ export class Database {
return updateResult.affected > 0;
}
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> {
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options);
@@ -152,11 +95,7 @@ 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')
@@ -169,29 +108,19 @@ 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',
{
userId,
organizationSlug
}
)
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
userId,
organizationSlug
})
.getMany();
return projects;
@@ -200,9 +129,7 @@ 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);
@@ -227,9 +154,7 @@ 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);
@@ -257,11 +182,8 @@ 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: {
@@ -278,12 +200,8 @@ 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: {
@@ -298,12 +216,9 @@ 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;
@@ -312,63 +227,37 @@ 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;
@@ -378,8 +267,7 @@ 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: {
@@ -391,7 +279,8 @@ export class Database {
where: {
id: projectMemberId
}
});
}
);
if (projectMemberWithProject.length === 0) {
throw new Error('Member does not exist');
@@ -400,49 +289,34 @@ 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',
{
userId,
searchText: `%${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
@@ -455,11 +329,7 @@ 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
@@ -482,15 +352,9 @@ 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);
}
@@ -537,20 +401,14 @@ 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({
+10 -25
View File
@@ -26,27 +26,18 @@ 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()
@@ -87,12 +78,6 @@ export class Deployment {
@Column('simple-json')
applicationRecordData!: ApplicationRecord;
@Column('varchar')
applicationDeploymentRequestId!: string;
@Column('simple-json')
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
@Column('varchar', { nullable: true })
applicationDeploymentRecordId!: string | null;
+1 -1
View File
@@ -39,7 +39,7 @@ export class Domain {
@ManyToOne(() => Domain)
@JoinColumn({ name: 'redirectToId' })
// eslint-disable-next-line no-use-before-define
// eslint-disable-next-line no-use-before-define
redirectTo!: Domain | null;
@Column({
+3 -7
View File
@@ -27,12 +27,8 @@ export class Organization {
@UpdateDateColumn()
updatedAt!: Date;
@OneToMany(
() => UserOrganization,
(userOrganization) => userOrganization.organization,
{
cascade: ['soft-remove']
}
)
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
cascade: ['soft-remove']
})
userOrganizations!: UserOrganization[];
}
+16 -1
View File
@@ -15,6 +15,15 @@ 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')
@@ -43,6 +52,12 @@ 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;
@@ -76,7 +91,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[];
+1 -1
View File
@@ -15,7 +15,7 @@ import { User } from './User';
export enum Permission {
View = 'View',
Edit = 'Edit',
Edit = 'Edit'
}
@Entity()
+4 -8
View File
@@ -34,17 +34,13 @@ 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,
{
cascade: ['soft-remove']
}
)
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
cascade: ['soft-remove']
})
userOrganizations!: UserOrganization[];
}
+3 -12
View File
@@ -19,9 +19,7 @@ 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,
@@ -33,16 +31,9 @@ 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);
+46 -103
View File
@@ -6,11 +6,8 @@ import { DateTime } from 'luxon';
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config';
import {
ApplicationRecord,
Deployment,
ApplicationDeploymentRequest
} from './entity/Deployment';
import { ApplicationDeploymentRequest } from './entity/Project';
import { ApplicationRecord, Deployment } from './entity/Deployment';
import { AppDeploymentRecord, PackageJSON } from './types';
const log = debug('snowball:registry');
@@ -24,54 +21,35 @@ export class Registry {
private registry: LaconicRegistry;
private registryConfig: RegistryConfig;
constructor (registryConfig: RegistryConfig) {
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
}: {
appName: string;
packageJSON: PackageJSON;
commitHash: string;
appType: string;
repoUrl: string;
}): Promise<{
applicationRecordId: string;
applicationRecordData: ApplicationRecord;
}> {
packageJSON: PackageJSON
commitHash: string,
appType: string,
repoUrl: string
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
assert(packageJSON.name, "name field doesn't exist in package.json");
// 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(
{
type: APP_RECORD_TYPE,
name: packageJSON.name
},
true
);
const records = await this.registry.queryRecords({
type: APP_RECORD_TYPE,
name: packageJSON.name
}, 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');
@@ -82,16 +60,11 @@ export class Registry {
repository_ref: commitHash,
repository: [repoUrl],
app_type: appType,
name: appName,
name: packageJSON.name,
...(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 })
};
@@ -108,45 +81,26 @@ export class Registry {
log('Application record data:', applicationRecord);
// TODO: Discuss computation of CRN
const crn = this.getCrn(packageJSON.name, appName);
const crn = this.getCrn(packageJSON.name);
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;
packageJsonName: string;
commitHash: string;
repository: string;
environmentVariables: { [key: string]: string };
appName: string,
commitHash: string,
repository: string,
environmentVariables: { [key: string]: string }
}): Promise<{
applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest;
applicationDeploymentRequestId: string,
applicationDeploymentRequestData: ApplicationDeploymentRequest
}> {
const crn = this.getCrn(data.packageJsonName, data.appName);
const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]);
const applicationRecord = records[0];
@@ -170,9 +124,7 @@ 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
})
@@ -190,40 +142,31 @@ 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(
{
type: APP_DEPLOYMENT_RECORD_TYPE
},
true
);
const records = await this.registry.queryRecords({
type: APP_DEPLOYMENT_RECORD_TYPE
}, 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, appName: string): string {
const [arg1] = packageJsonName.split('/');
const authority = arg1.replace('@', '');
getCrn (packageJsonName: string): string {
const [arg1, arg2] = packageJsonName.split('/');
return `crn://${authority}/applications/${appName}`;
if (arg2) {
const authority = arg1.replace('@', '');
return `crn://${authority}/applications/${arg2}`;
}
return `crn://${arg1}/applications/${arg1}`;
}
}
+37 -147
View File
@@ -17,7 +17,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getUser(context.userId);
},
organizations: async (_: any, __: any, context: any) => {
organizations: async (_:any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.userId);
},
@@ -25,25 +25,15 @@ 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);
},
@@ -51,55 +41,32 @@ 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;
data: {
permissions: Permission[];
};
updateProjectMember: async (_: any, { projectMemberId, data }: {
projectMemberId: string,
data: {
permissions: Permission[]
}
) => {
}) => {
try {
return await service.updateProjectMember(projectMemberId, data);
} catch (err) {
@@ -108,19 +75,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
addProjectMember: async (
_: any,
{
projectId,
data
}: {
projectId: string;
data: {
email: string;
permissions: Permission[];
};
addProjectMember: async (_: any, { projectId, data }: {
projectId: string,
data: {
email: string,
permissions: Permission[]
}
) => {
}) => {
try {
return Boolean(await service.addProjectMember(projectId, data));
} catch (err) {
@@ -129,51 +90,25 @@ 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) {
@@ -182,45 +117,25 @@ 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) {
@@ -229,15 +144,9 @@ 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;
@@ -248,8 +157,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
try {
return await service.deleteProject(projectId);
} catch (err) {
log(err);
return false;
log(err); return false;
}
},
@@ -262,13 +170,7 @@ 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) {
@@ -277,10 +179,7 @@ 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) {
@@ -289,10 +188,7 @@ 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) {
@@ -301,11 +197,7 @@ 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) {
@@ -316,9 +208,7 @@ 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;
+3 -12
View File
@@ -188,19 +188,10 @@ 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!
+152 -282
View File
@@ -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, PackageJSON } from './types';
import { AppDeploymentRecord, GitPushEventPayload } 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,9 +71,7 @@ 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);
@@ -93,12 +91,10 @@ 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: {
@@ -107,46 +103,38 @@ export class Service {
});
// Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId);
}
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId);
}
return acc;
},
new Set<string>()
);
return acc;
}, 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) => {
acc[deployment.applicationRecordId] = deployment;
return acc;
},
{}
);
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, {
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}`
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}`);
});
await Promise.all(deploymentUpdatePromises);
@@ -162,10 +150,7 @@ 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 });
}
@@ -180,14 +165,8 @@ 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;
}
@@ -196,52 +175,35 @@ 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: {
@@ -269,10 +231,7 @@ 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) {
@@ -289,48 +248,33 @@ export class Service {
}
}
async addEnvironmentVariables (
projectId: string,
data: { environments: string[]; key: string; value: string }[]
): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data
.map((environmentVariable) => {
return environmentVariable.environments.map((environment) => {
return {
key: environmentVariable.key,
value: environmentVariable.value,
environment: environment as Environment,
project: Object.assign(new Project(), {
id: projectId
})
};
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data.map((environmentVariable) => {
return environmentVariable.environments.map((environment) => {
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: {
@@ -342,21 +286,20 @@ 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, {
project: oldDeployment.project,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
const newDeployment = await this.createDeployment(userId,
octokit,
{
project: oldDeployment.project,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
return newDeployment;
}
@@ -368,9 +311,7 @@ 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({
@@ -385,61 +326,28 @@ export class Service {
}
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
assert(packageJSON.name, "name field doesn't exist in package.json");
const packageJSON = JSON.parse(atob(packageJSONData.content));
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({
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
});
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl: recordData.repoUrl
});
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
await this.db.updateDeployment(
{
domainId: data.domain?.id
},
{
domain: null
}
);
await this.db.updateDeployment({
domainId: data.domain?.id
}, {
domain: null
});
const newDeployment = await this.db.addDeployment({
project: data.project,
@@ -450,25 +358,17 @@ 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
@@ -483,9 +383,7 @@ 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,
@@ -495,8 +393,7 @@ export class Service {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
// Create deployment with prod branch and latest commit
await this.createDeployment(
userId,
const newDeployment = await this.createDeployment(userId,
octokit,
{
project,
@@ -511,6 +408,27 @@ 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;
@@ -523,10 +441,7 @@ 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']
@@ -534,13 +449,9 @@ 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.status === 422 &&
(err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
)
)
!(err instanceof RequestError &&
err.status === 422 &&
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR))
) {
throw err;
}
@@ -552,9 +463,7 @@ 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}`);
@@ -566,29 +475,23 @@ 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, {
project,
branch,
environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message
});
await this.createDeployment(project.ownerId,
octokit,
{
project,
branch,
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message
});
}
}
async updateProject (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
return this.db.updateProjectById(projectId, data);
}
@@ -605,18 +508,13 @@ 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,
@@ -634,23 +532,22 @@ export class Service {
const octokit = await this.getOctokit(userId);
const newDeployment = await this.createDeployment(userId, octokit, {
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
const newDeployment = await this.createDeployment(userId,
octokit,
{
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
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: {
@@ -668,25 +565,16 @@ 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);
@@ -711,20 +599,12 @@ 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
@@ -765,9 +645,7 @@ 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;
@@ -778,13 +656,8 @@ 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
});
@@ -793,10 +666,7 @@ 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);
}
}
+3 -3
View File
@@ -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
}
-47
View File
@@ -2,7 +2,6 @@ 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');
@@ -20,49 +19,3 @@ 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;
};
+1 -1
View File
@@ -18,4 +18,4 @@ const main = async () => {
deleteFile(config.database.dbPath);
};
main().catch((err) => log(err));
main().catch(err => log(err));
+20 -42
View File
@@ -1,16 +1,14 @@
[
{
"projectIndex": 0,
"domainIndex": 0,
"domainIndex":0,
"createdByIndex": 0,
"id": "ffhae3zq",
"id":"ffhae3zq",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -18,16 +16,14 @@
},
{
"projectIndex": 0,
"domainIndex": 1,
"domainIndex":1,
"createdByIndex": 0,
"id": "vehagei8",
"id":"vehagei8",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -35,16 +31,14 @@
},
{
"projectIndex": 0,
"domainIndex": 2,
"domainIndex":2,
"createdByIndex": 0,
"id": "qmgekyte",
"id":"qmgekyte",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -54,14 +48,12 @@
"projectIndex": 0,
"domainIndex": null,
"createdByIndex": 0,
"id": "f8wsyim6",
"id":"f8wsyim6",
"status": "Ready",
"environment": "Production",
"isCurrent": false,
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "prod",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -69,16 +61,14 @@
},
{
"projectIndex": 1,
"domainIndex": 3,
"domainIndex":3,
"createdByIndex": 1,
"id": "eO8cckxk",
"id":"eO8cckxk",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -86,16 +76,14 @@
},
{
"projectIndex": 1,
"domainIndex": 4,
"domainIndex":4,
"createdByIndex": 1,
"id": "yaq0t5yw",
"id":"yaq0t5yw",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -103,16 +91,14 @@
},
{
"projectIndex": 1,
"domainIndex": 5,
"domainIndex":5,
"createdByIndex": 1,
"id": "hwwr6sbx",
"id":"hwwr6sbx",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -120,16 +106,14 @@
},
{
"projectIndex": 2,
"domainIndex": 9,
"domainIndex":9,
"createdByIndex": 2,
"id": "ndxje48a",
"id":"ndxje48a",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -137,16 +121,14 @@
},
{
"projectIndex": 2,
"domainIndex": 7,
"domainIndex":7,
"createdByIndex": 2,
"id": "gtgpgvei",
"id":"gtgpgvei",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -154,16 +136,14 @@
},
{
"projectIndex": 2,
"domainIndex": 8,
"domainIndex":8,
"createdByIndex": 2,
"id": "b4bpthjr",
"id":"b4bpthjr",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {},
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -173,14 +153,12 @@
"projectIndex": 3,
"domainIndex": 6,
"createdByIndex": 2,
"id": "b4bpthjr",
"id":"b4bpthjr",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
+31 -9
View File
@@ -2,55 +2,77 @@
{
"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
View File
@@ -10,6 +10,8 @@
"framework": "test",
"webhooks": [],
"icon": "",
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject.snowball.xyz"
},
{
@@ -23,6 +25,8 @@
"framework": "test-2",
"webhooks": [],
"icon": "",
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject-2.snowball.xyz"
},
{
@@ -36,6 +40,8 @@
"framework": "test-3",
"webhooks": [],
"icon": "",
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools.snowball.xyz"
},
{
@@ -49,6 +55,8 @@
"framework": "test-4",
"webhooks": [],
"icon": "",
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools-2.snowball.xyz"
},
{
@@ -62,6 +70,8 @@
"framework": "test-5",
"webhooks": [],
"icon": "",
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "snowball-2.snowball.xyz"
}
]
+49 -83
View File
@@ -1,4 +1,5 @@
import { DataSource } from 'typeorm';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
import * as fs from 'fs/promises';
import debug from 'debug';
import path from 'path';
@@ -10,12 +11,7 @@ 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 {
checkFileExists,
getConfig,
getEntities,
loadAndSaveData
} from '../src/utils';
import { getConfig } from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
@@ -31,35 +27,43 @@ 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 generateTestData = async (dataSource: DataSource) => {
const userEntities = await getEntities(
path.resolve(__dirname, USER_DATA_PATH)
);
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
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 orgEntities = await getEntities(
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
);
const savedOrgs = await loadAndSaveData(
Organization,
dataSource,
orgEntities
);
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 projectRelations = {
owner: savedUsers,
organization: savedOrgs
};
const projectEntities = await getEntities(
path.resolve(__dirname, PROJECT_DATA_PATH)
);
const savedProjects = await loadAndSaveData(
Project,
dataSource,
projectEntities,
projectRelations
);
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
const domainRepository = dataSource.getRepository(Domain);
@@ -67,30 +71,14 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects
};
const primaryDomainsEntities = await getEntities(
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
);
const savedPrimaryDomains = await loadAndSaveData(
Domain,
dataSource,
primaryDomainsEntities,
domainPrimaryRelations
);
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations);
const domainRedirectedRelations = {
project: savedProjects,
redirectTo: savedPrimaryDomains
};
const redirectDomainsEntities = await getEntities(
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
);
await loadAndSaveData(
Domain,
dataSource,
redirectDomainsEntities,
domainRedirectedRelations
);
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations);
const savedDomains = await domainRepository.find();
@@ -99,30 +87,14 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs
};
const userOrganizationsEntities = await getEntities(
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
);
await loadAndSaveData(
UserOrganization,
dataSource,
userOrganizationsEntities,
userOrganizationRelations
);
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
const projectMemberRelations = {
member: savedUsers,
project: savedProjects
};
const projectMembersEntities = await getEntities(
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
);
await loadAndSaveData(
ProjectMember,
dataSource,
projectMembersEntities,
projectMemberRelations
);
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
const deploymentRelations = {
project: savedProjects,
@@ -130,29 +102,23 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers
};
const deploymentsEntities = await getEntities(
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
);
await loadAndSaveData(
Deployment,
dataSource,
deploymentsEntities,
deploymentRelations
);
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
const environmentVariableRelations = {
project: savedProjects
};
const environmentVariablesEntities = await getEntities(
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
);
await loadAndSaveData(
EnvironmentVariable,
dataSource,
environmentVariablesEntities,
environmentVariableRelations
);
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 main = async () => {
+4 -20
View File
@@ -17,32 +17,16 @@ 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,15 +12,9 @@ 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',
@@ -59,7 +53,7 @@ async function main () {
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
}),
request: deployment.applicationDeploymentRequestId,
request: deployment.project.applicationDeploymentRequestId,
url
};
-5
View File
@@ -7,12 +7,7 @@
"@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@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",
@@ -62,7 +62,7 @@ export const buttonTheme = tv(
'text-elements-on-tertiary',
'border',
'border-border-interactive/10',
'bg-controls-tertiary',
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
@@ -9,7 +9,7 @@ import { cloneIcon } from 'utils/cloneIcon';
/**
* Represents the properties of a base button component.
*/
export interface ButtonBaseProps {
interface ButtonBaseProps {
/**
* The optional left icon element for a component.
* @type {ReactNode}
@@ -98,29 +98,19 @@ const Button = forwardRef<
href,
...baseLinkProps,
};
return (
// @ts-expect-error - ref
<a ref={ref} {...externalLinkProps}>
{_children}
</a>
);
return <a {...externalLinkProps}>{_children}</a>;
}
// Internal link
return (
// @ts-expect-error - ref
<Link ref={ref} {...baseLinkProps} to={href}>
<Link {...baseLinkProps} to={href}>
{_children}
</Link>
);
} else {
const { ...buttonProps } = _props;
return (
// @ts-expect-error - as prop is not a valid prop for button elements
<button ref={ref} {...buttonProps}>
{_children}
</button>
);
// @ts-expect-error - as prop is not a valid prop for button elements
return <button {...buttonProps}>{_children}</button>;
}
},
[],
@@ -171,6 +161,8 @@ 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 })}
@@ -1,21 +0,0 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CalendarIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="19"
viewBox="0 0 18 19"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -1,21 +0,0 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -1,21 +0,0 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const InfoRoundFilledIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM10 11C10 10.4477 10.4477 10 11 10H12C12.5523 10 13 10.4477 13 11V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12C10.4477 12 10 11.5523 10 11ZM12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -1,21 +0,0 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const LoadingIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.0002 1.66669C10.4605 1.66669 10.8336 2.03978 10.8336 2.50002V5.00002C10.8336 5.46026 10.4605 5.83335 10.0002 5.83335C9.54 5.83335 9.1669 5.46026 9.1669 5.00002V2.50002C9.1669 2.03978 9.54 1.66669 10.0002 1.66669ZM15.8928 4.10746C16.2182 4.4329 16.2182 4.96054 15.8928 5.28597L14.125 7.05374C13.7996 7.37918 13.272 7.37918 12.9465 7.05374C12.6211 6.7283 12.6211 6.20067 12.9465 5.87523L14.7143 4.10746C15.0397 3.78203 15.5674 3.78203 15.8928 4.10746ZM4.10768 4.10746C4.43312 3.78203 4.96076 3.78203 5.28619 4.10746L7.05396 5.87523C7.3794 6.20067 7.3794 6.7283 7.05396 7.05374C6.72852 7.37918 6.20088 7.37918 5.87545 7.05374L4.10768 5.28597C3.78224 4.96054 3.78224 4.4329 4.10768 4.10746ZM1.66666 10.0006C1.66666 9.54035 2.03975 9.16725 2.49999 9.16725H4.99999C5.46023 9.16725 5.83332 9.54035 5.83332 10.0006C5.83332 10.4608 5.46023 10.8339 4.99999 10.8339H2.49999C2.03975 10.8339 1.66666 10.4608 1.66666 10.0006ZM14.1667 10.0006C14.1667 9.54035 14.5398 9.16725 15 9.16725H17.5C17.9602 9.16725 18.3333 9.54035 18.3333 10.0006C18.3333 10.4608 17.9602 10.8339 17.5 10.8339H15C14.5398 10.8339 14.1667 10.4608 14.1667 10.0006ZM7.05396 12.9463C7.3794 13.2717 7.3794 13.7994 7.05396 14.1248L5.28619 15.8926C4.96076 16.218 4.43312 16.218 4.10768 15.8926C3.78224 15.5671 3.78224 15.0395 4.10768 14.7141L5.87545 12.9463C6.20088 12.6209 6.72852 12.6209 7.05396 12.9463ZM12.9465 12.9463C13.272 12.6209 13.7996 12.6209 14.125 12.9463L15.8928 14.7141C16.2182 15.0395 16.2182 15.5671 15.8928 15.8926C15.5674 16.218 15.0397 16.218 14.7143 15.8926L12.9465 14.1248C12.6211 13.7994 12.6211 13.2717 12.9465 12.9463ZM10.0002 14.1667C10.4605 14.1667 10.8336 14.5398 10.8336 15V17.5C10.8336 17.9603 10.4605 18.3334 10.0002 18.3334C9.54 18.3334 9.1669 17.9603 9.1669 17.5V15C9.1669 14.5398 9.54 14.1667 10.0002 14.1667Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -9,7 +9,3 @@ export * from './WarningIcon';
export * from './SearchIcon';
export * from './CrossIcon';
export * from './GlobeIcon';
export * from './CalendarIcon';
export * from './CheckRoundFilledIcon';
export * from './InfoRoundFilledIcon';
export * from './LoadingIcon';
@@ -1,9 +0,0 @@
import { VariantProps, tv } from 'tailwind-variants';
export const datePickerTheme = tv({
slots: {
input: [],
},
});
export type DatePickerTheme = VariantProps<typeof datePickerTheme>;
@@ -1,100 +0,0 @@
import React, { useCallback, useState } from 'react';
import { Input, InputProps } from 'components/shared/Input';
import * as Popover from '@radix-ui/react-popover';
import { datePickerTheme } from './DatePicker.theme';
import { Calendar, CalendarProps } from 'components/shared/Calendar';
import { CalendarIcon } from 'components/shared/CustomIcon';
import { Value } from 'react-calendar/dist/cjs/shared/types';
import { format } from 'date-fns';
export interface DatePickerProps
extends Omit<InputProps, 'onChange' | 'value'> {
/**
* The props for the calendar component.
*/
calendarProps?: CalendarProps;
/**
* Optional callback function that is called when the value of the input changes.
* @param {string} value - The new value of the input.
* @returns None
*/
onChange?: (value: Value) => void;
/**
* The value of the input.
*/
value?: Value;
/**
* Whether to allow the selection of a date range.
*/
selectRange?: boolean;
}
/**
* A date picker component that allows users to select a date from a calendar.
* @param {DatePickerProps} props - The props for the date picker component.
* @returns The rendered date picker component.
*/
export const DatePicker = ({
className,
calendarProps,
value,
onChange,
selectRange = false,
...props
}: DatePickerProps) => {
const { input } = datePickerTheme();
const [open, setOpen] = useState(false);
/**
* Renders the value of the date based on the current state of `props.value`.
* @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy.
*/
const renderValue = useCallback(() => {
if (!value) return undefined;
if (Array.isArray(value)) {
return value
.map((date) => format(date as Date, 'dd/MM/yyyy'))
.join(' - ');
}
return format(value, 'dd/MM/yyyy');
}, [value]);
/**
* Handles the selection of a date from the calendar.
*/
const handleSelect = useCallback(
(date: Value) => {
setOpen(false);
onChange?.(date);
},
[setOpen, onChange],
);
return (
<Popover.Root open={open}>
<Popover.Trigger>
<Input
{...props}
rightIcon={<CalendarIcon onClick={() => setOpen(true)} />}
readOnly
placeholder="Select a date..."
value={renderValue()}
className={input({ className })}
onClick={() => setOpen(true)}
/>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content onInteractOutside={() => setOpen(false)}>
<Calendar
{...calendarProps}
selectRange={selectRange}
value={value}
onCancel={() => setOpen(false)}
onSelect={handleSelect}
/>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
};
@@ -1 +0,0 @@
export * from './DatePicker';
@@ -50,7 +50,6 @@ export const inputTheme = tv(
'outline-offset-0',
'outline-border-danger',
'shadow-none',
'focus:outline-border-danger',
],
helperText: 'text-elements-danger',
},
@@ -55,7 +55,7 @@ export const Input = ({
const renderLeftIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
{cloneIcon(leftIcon, { className: iconCls(), 'aria-hidden': true })}
{cloneIcon(leftIcon, { className: iconCls(), ariaHidden: true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
@@ -63,7 +63,7 @@ export const Input = ({
const renderRightIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
{cloneIcon(rightIcon, { className: iconCls(), 'aria-hidden': true })}
{cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
@@ -73,7 +73,7 @@ export const Input = ({
<div className={helperTextCls()}>
{state &&
cloneIcon(<WarningIcon className={helperIconCls()} />, {
'aria-hidden': true,
ariaHidden: true,
})}
<p>{helperText}</p>
</div>
@@ -1,54 +0,0 @@
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>;
@@ -1,63 +0,0 @@
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>
);
};
@@ -1,74 +0,0 @@
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>
);
};
@@ -1,2 +0,0 @@
export * from './Radio';
export * from './RadioItem';
@@ -1,84 +0,0 @@
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>;
@@ -1,85 +0,0 @@
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 +0,0 @@
export * from './Switch';
@@ -9,8 +9,6 @@ export const tabsTheme = tv({
// Horizontal default
'px-1',
'pb-5',
'cursor-default',
'select-none',
'text-elements-low-em',
'border-b-2',
'border-transparent',
@@ -1,58 +0,0 @@
import { VariantProps, tv } from 'tailwind-variants';
export const simpleToastTheme = tv(
{
slots: {
wrapper: [
'flex',
'items-center',
'py-2',
'pl-2',
'pr-1.5',
'gap-2',
'rounded-full',
'mx-auto',
'mt-3',
'w-fit',
'overflow-hidden',
'bg-surface-high-contrast',
'shadow-sm',
],
icon: ['flex', 'items-center', 'justify-center', 'w-5', 'h-5'],
closeIcon: [
'cursor-pointer',
'flex',
'items-center',
'justify-center',
'w-6',
'h-6',
'text-elements-on-high-contrast',
],
title: ['text-sm', 'text-elements-on-high-contrast'],
},
variants: {
variant: {
success: {
icon: ['text-elements-success'],
},
error: {
icon: ['text-elements-danger'],
},
warning: {
icon: ['text-elements-warning'],
},
info: {
icon: ['text-elements-info'],
},
loading: {
icon: ['text-elements-info'],
},
},
},
},
{
responsiveVariants: true,
},
);
export type SimpleToastTheme = VariantProps<typeof simpleToastTheme>;
@@ -1,94 +0,0 @@
import React, { useMemo } from 'react';
import * as ToastPrimitive from '@radix-ui/react-toast';
import { ToastProps } from '@radix-ui/react-toast';
import { motion } from 'framer-motion';
import { simpleToastTheme, type SimpleToastTheme } from './SimpleToast.theme';
import {
LoadingIcon,
CheckRoundFilledIcon,
CrossIcon,
InfoRoundFilledIcon,
WarningIcon,
} from 'components/shared/CustomIcon';
import { Button, type ButtonOrLinkProps } from 'components/shared/Button';
import { cloneIcon } from 'utils/cloneIcon';
type CtaProps = ButtonOrLinkProps & {
buttonLabel: string;
};
export interface SimpleToastProps extends ToastProps {
id: string;
title: string;
variant?: SimpleToastTheme['variant'];
cta?: CtaProps[];
onDismiss: (toastId: string) => void;
}
export const SimpleToast = ({
id,
className,
title,
variant = 'success',
cta = [],
onDismiss,
...props
}: SimpleToastProps) => {
const hasCta = cta.length > 0;
const {
wrapper: wrapperCls,
icon: iconCls,
closeIcon: closeIconCls,
title: titleCls,
} = simpleToastTheme({ variant });
const Icon = useMemo(() => {
if (variant === 'success') return <CheckRoundFilledIcon />;
if (variant === 'error') return <WarningIcon />;
if (variant === 'warning') return <WarningIcon />;
if (variant === 'info') return <InfoRoundFilledIcon />;
return <LoadingIcon />; // variant === 'loading'
}, [variant]);
const renderCta = useMemo(() => {
if (!hasCta) return null;
return (
<div className="flex gap-1.5 ml-2">
{cta.map(({ buttonLabel, ...props }, index) => (
<Button key={index} {...props}>
{buttonLabel}
</Button>
))}
</div>
);
}, [cta]);
const renderCloseButton = useMemo(
() => (
<div onClick={() => onDismiss(id)} className={closeIconCls()}>
<CrossIcon className="h-3 w-3" />
</div>
),
[id],
);
return (
<ToastPrimitive.Root {...props} asChild>
<motion.li
animate={{
y: 'var(--radix-toast-swipe-move-y, 0)',
opacity: 1,
}}
className={wrapperCls({ class: className })}
exit={{ y: '100%', opacity: 0 }}
initial={{ y: '100%', opacity: 0 }}
>
{cloneIcon(Icon, { className: iconCls() })}
<ToastPrimitive.Title asChild>
<p className={titleCls()}>{title}</p>
</ToastPrimitive.Title>
{renderCta}
{renderCloseButton}
</motion.li>
</ToastPrimitive.Root>
);
};
@@ -1,15 +0,0 @@
import React from 'react';
import {
Provider,
Viewport,
type ToastProviderProps,
} from '@radix-ui/react-toast';
export const ToastProvider = ({ children, ...props }: ToastProviderProps) => {
return (
<Provider {...props}>
{children}
<Viewport className="fixed inset-x-0 bottom-0 px-4 py-10" />
</Provider>
);
};
@@ -1,25 +0,0 @@
import React, { ComponentPropsWithoutRef, useMemo } from 'react';
import { Provider, Viewport } from '@radix-ui/react-toast';
import { SimpleToast, SimpleToastProps } from './SimpleToast';
import { useToast } from './useToast';
interface ToasterProps extends ComponentPropsWithoutRef<'div'> {}
export const Toaster = ({}: ToasterProps) => {
const { toasts } = useToast();
const renderToasts = useMemo(
() =>
toasts.map(({ id, ...props }) => (
<SimpleToast key={id} {...(props as SimpleToastProps)} id={id} />
)),
[toasts],
);
return (
<Provider>
{renderToasts}
<Viewport className="z-toast fixed inset-x-0 bottom-0 mx-auto w-fit px-4 pb-10" />
</Provider>
);
};
@@ -1,2 +0,0 @@
export * from './Toaster';
export * from './useToast';
@@ -1,192 +0,0 @@
// Inspired by react-hot-toast library
import React from 'react';
import { type ToastProps } from '@radix-ui/react-toast';
import { SimpleToastProps } from './SimpleToast';
const TOAST_LIMIT = 3;
const TOAST_REMOVE_DELAY_DEFAULT = 7000;
type ToasterToast = ToastProps &
SimpleToastProps & {
id: string;
};
const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const;
let count = 0;
const genId = () => {
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
};
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType['ADD_TOAST'];
toast: ToasterToast;
}
| {
type: ActionType['UPDATE_TOAST'];
toast: Partial<ToasterToast>;
}
| {
type: ActionType['DISMISS_TOAST'];
toastId?: ToasterToast['id'];
duration?: ToasterToast['duration'];
}
| {
type: ActionType['REMOVE_TOAST'];
toastId?: ToasterToast['id'];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string, duration: number) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: 'REMOVE_TOAST',
toastId: toastId,
});
}, duration ?? TOAST_REMOVE_DELAY_DEFAULT);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'ADD_TOAST':
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case 'UPDATE_TOAST':
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id
? ({ ...t, ...action.toast } as ToasterToast)
: t,
),
};
case 'DISMISS_TOAST': {
const { toastId, duration = TOAST_REMOVE_DELAY_DEFAULT } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId, duration);
} else {
state.toasts.forEach((_toast) => {
addToRemoveQueue(_toast.id, duration);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case 'REMOVE_TOAST':
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(_state: State) => void> = [];
let memoryState: State = { toasts: [] };
const dispatch = (action: Action) => {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
};
const toast = (props: ToasterToast) => {
if (!props.duration) {
props.duration = 2000;
}
const id = genId();
const update = (_props: ToasterToast) =>
dispatch({
type: 'UPDATE_TOAST',
toast: { ..._props, id },
});
const dismiss = () =>
dispatch({ type: 'DISMISS_TOAST', toastId: id, duration: props.duration });
dispatch({
type: 'ADD_TOAST',
toast: {
...props,
id,
open: true,
onOpenChange: (open: boolean) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
};
const useToast = () => {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
};
};
export { toast, useToast };
@@ -1,14 +0,0 @@
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'],
},
});
@@ -1,47 +0,0 @@
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>
);
};
@@ -1,38 +0,0 @@
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;
@@ -1,46 +0,0 @@
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>
);
};
@@ -1 +0,0 @@
export * from './TooltipContent';
@@ -1,12 +0,0 @@
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>
);
};
@@ -1 +0,0 @@
export * from './TooltipTrigger';
@@ -1,2 +0,0 @@
export * from './Tooltip';
export * from './TooltipBase';
+2 -2
View File
@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import assert from 'assert';
import { Toaster } from 'react-hot-toast';
import { GQLClient } from 'gql-client';
import { ThemeProvider } from '@material-tailwind/react';
@@ -10,7 +11,6 @@ import '@fontsource/inter';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { GQLClientProvider } from './context/GQLClientContext';
import { Toaster } from 'components/shared/Toast';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
@@ -26,7 +26,7 @@ root.render(
<ThemeProvider>
<GQLClientProvider client={gqlClient}>
<App />
<Toaster />
<Toaster position="bottom-center" />
</GQLClientProvider>
</ThemeProvider>
</React.StrictMode>,
+11 -102
View File
@@ -10,7 +10,6 @@ import { renderBadges } from './renders/badge';
import {
renderButtonIcons,
renderButtons,
renderDisabledButtons,
renderLinks,
} from './renders/button';
import {
@@ -18,23 +17,15 @@ 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 { DatePicker } from 'components/shared/DatePicker';
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">
@@ -49,58 +40,19 @@ const Page = () => {
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Toast */}
{/* Button */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Toasts</h1>
{renderToastsWithCta()}
{renderToast()}
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Tooltip */}
<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 */}
@@ -149,16 +101,6 @@ const Page = () => {
/>
</div>
</div>
<h1 className="text-2xl font-bold">Date Picker</h1>
<div className="flex flex-col gap-10 items-center justify-center">
<DatePicker value={singleDate} onChange={setSingleDate} />
<DatePicker
selectRange
value={dateRange}
onChange={setDateRange}
/>
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
@@ -187,49 +129,6 @@ 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>
@@ -240,6 +139,16 @@ 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,45 +2,44 @@ import { Button, ButtonTheme } from 'components/shared/Button';
import { PlusIcon } from 'components/shared/CustomIcon';
import React from 'react';
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',
] 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}>
{buttonSizes.map((size) => (
<Button
leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />}
variant={variant as ButtonTheme['variant']}
size={size as ButtonTheme['size']}
key={`${variant}-${size}`}
>
Button
</Button>
))}
</div>
));
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 iconOnlyVariants.map((variant, index) => (
return [
'primary',
'secondary',
'tertiary',
'ghost',
'danger',
'danger-ghost',
].map((variant, index) => (
<div className="flex gap-5 flex-wrap" key={index}>
{buttonSizes.map((size) => (
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
<Button
iconOnly
variant={variant as ButtonTheme['variant']}
size={size as ButtonTheme['size']}
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'}
key={`${variant}-${size}`}
disabled={size === 'disabled'}
>
<PlusIcon />
</Button>
@@ -50,38 +49,17 @@ export const renderButtonIcons = () => {
};
export const renderLinks = () => {
return linkVariants.map((variant) => (
return ['link', 'link-emphasized', 'disabled'].map((variant) => (
<Button
variant={variant}
variant={
variant !== 'disabled' ? (variant as ButtonTheme['variant']) : 'link'
}
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>
</>
);
};
@@ -1,16 +0,0 @@
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',
},
];
@@ -1,66 +0,0 @@
import React from 'react';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
export const renderToastsWithCta = () => {
const { toast, dismiss } = useToast();
return (
<div className="flex gap-10">
{(['success', 'error', 'warning', 'info', 'loading'] as const).map(
(variant, index) => (
<Button
onClick={() =>
toast({
onDismiss: dismiss,
id: `${variant}_${index}`,
title: 'Project created',
cta: [
{
buttonLabel: 'Button',
size: 'xs',
variant: 'tertiary',
},
{
buttonLabel: 'Button',
size: 'xs',
},
],
variant,
})
}
key={`${variant}_${index}`}
>
{variant} with cta
</Button>
),
)}
</div>
);
};
export const renderToast = () => {
const { toast, dismiss } = useToast();
return (
<div className="flex gap-10">
{(['success', 'error', 'warning', 'info', 'loading'] as const).map(
(variant, index) => (
<Button
onClick={() =>
toast({
onDismiss: dismiss,
id: `${variant}_${index}`,
title: 'Project created',
variant,
})
}
key={`${variant}_${index}`}
>
{variant}
</Button>
),
)}
</div>
);
};
@@ -1,30 +0,0 @@
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,7 +16,6 @@ const DEFAULT_FILTER_VALUE: FilterValue = {
searchedBranch: '',
status: StatusOptions.ALL_STATUS,
};
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
const DeploymentsTabPanel = () => {
const client = useGQLClient();
@@ -27,30 +26,22 @@ const DeploymentsTabPanel = () => {
const [deployments, setDeployments] = useState<Deployment[]>([]);
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
const fetchDeployments = useCallback(async () => {
const fetchDeployments = 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(() => {
fetchProductionBranchDomains();
fetchDeployments();
const interval = setInterval(() => {
fetchDeployments();
}, FETCH_DEPLOYMENTS_INTERVAL);
return () => {
clearInterval(interval);
};
}, [fetchDeployments, fetchProductionBranchDomains]);
fetchProductionBranchDomains();
}, []);
const currentDeployment = useMemo(() => {
return deployments.find((deployment) => {
+3 -10
View File
@@ -8,13 +8,10 @@ export default withMT({
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
],
theme: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
extend: {
zIndex: {
tooltip: '52',
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
fontSize: {
'2xs': '0.625rem',
'3xs': '0.5rem',
@@ -154,16 +151,12 @@ 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',
3.25: '0.8125rem',
3.5: '0.875rem',
},
zIndex: {
toast: '9999',
},
},
},
plugins: [],
+1 -270
View File
@@ -2032,13 +2032,6 @@
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"
@@ -2054,14 +2047,6 @@
"@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"
@@ -2069,13 +2054,6 @@
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"
@@ -2090,11 +2068,6 @@
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"
@@ -3311,14 +3284,6 @@
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"
@@ -3377,35 +3342,6 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dismissable-layer@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4"
integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
"@radix-ui/react-focus-guards@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-focus-scope@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525"
integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-id@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0"
@@ -3414,53 +3350,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-popover@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
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-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@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"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@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"
integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-presence@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
@@ -3478,23 +3367,6 @@
"@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"
@@ -3519,20 +3391,6 @@
"@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"
@@ -3548,42 +3406,6 @@
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-toast@^1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6"
integrity sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-collection" "1.0.3"
"@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-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@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":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
@@ -3599,14 +3421,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755"
integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399"
@@ -3621,14 +3435,6 @@
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"
@@ -3637,21 +3443,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-visually-hidden@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac"
integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==
dependencies:
"@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"
@@ -5491,7 +5282,7 @@ argparse@^2.0.1:
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-hidden@^1.1.1, aria-hidden@^1.1.3:
aria-hidden@^1.1.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==
@@ -7477,11 +7268,6 @@ detect-newline@^3.0.0:
resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
detect-node@^2.0.4:
version "2.1.0"
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
@@ -9072,11 +8858,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@
has-symbols "^1.0.3"
hasown "^2.0.0"
get-nonce@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2"
resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz"
@@ -9906,13 +9687,6 @@ interpret@^1.0.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
ip@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
@@ -13975,25 +13749,6 @@ react-refresh@^0.11.0:
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-remove-scroll-bar@^2.3.3:
version "2.3.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz#cd2543b3ed7716c7c5b446342d21b0e0b303f47c"
integrity sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==
dependencies:
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll@2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-router-dom@^6.20.1:
version "6.20.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.20.1.tgz#e34f8075b9304221420de3609e072bb349824984"
@@ -14064,15 +13819,6 @@ react-scripts@5.0.1:
optionalDependencies:
fsevents "^2.3.2"
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==
dependencies:
get-nonce "^1.0.0"
invariant "^2.2.4"
tslib "^2.0.0"
react-syntax-highlighter@^15.5.0:
version "15.5.0"
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
@@ -16234,21 +15980,6 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
use-callback-ref@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0"
integrity sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==
dependencies:
tslib "^2.0.0"
use-sidecar@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
dependencies:
detect-node-es "^1.1.0"
tslib "^2.0.0"
usehooks-ts@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.10.0.tgz#ccd0d63168e4db95b061e26e585b0068d3fad0ed"