forked from cerc-io/snowballtools-base
Merge branch 'andrehadianto/design-system-components' into andrehadianto/T-4869-toast
This commit is contained in:
commit
42bf1534fb
26
build-webapp.sh
Executable file
26
build-webapp.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PKG_DIR="./packages/frontend"
|
||||||
|
OUTPUT_DIR="${PKG_DIR}/build"
|
||||||
|
DEST_DIR=${1:-/data}
|
||||||
|
|
||||||
|
if [[ -d "$DEST_DIR" ]]; then
|
||||||
|
echo "${DEST_DIR} already exists." 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > $PKG_DIR/.env <<EOF
|
||||||
|
REACT_APP_GQL_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_gql_url'
|
||||||
|
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||||
|
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
yarn || exit 1
|
||||||
|
yarn build || exit 1
|
||||||
|
|
||||||
|
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||||
|
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$OUTPUT_DIR" "$DEST_DIR"
|
@ -25,6 +25,9 @@
|
|||||||
"allowArgumentsExplicitlyTypedAsAny": true
|
"allowArgumentsExplicitlyTypedAsAny": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ "ignoreRestSiblings": true }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export interface GitHubConfig {
|
|||||||
oAuth: {
|
oAuth: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegistryConfig {
|
export interface RegistryConfig {
|
||||||
@ -27,7 +27,7 @@ export interface RegistryConfig {
|
|||||||
amount: string;
|
amount: string;
|
||||||
denom: string;
|
denom: string;
|
||||||
gas: string;
|
gas: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml';
|
import process from 'process';
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG_FILE_PATH =
|
||||||
|
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||||
|
|
||||||
export const DEFAULT_GQL_PATH = '/graphql';
|
export const DEFAULT_GQL_PATH = '/graphql';
|
||||||
|
|
||||||
// Note: temporary hardcoded user, later to be derived from auth token
|
// Note: temporary hardcoded user, later to be derived from auth token
|
||||||
export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0';
|
export const USER_ID =
|
||||||
|
process.env.SNOWBALL_BACKEND_USER_ID ||
|
||||||
|
'59f4355d-9549-4aac-9b54-eeefceeabef0';
|
||||||
|
|
||||||
export const PROJECT_DOMAIN = 'snowball.xyz';
|
export const PROJECT_DOMAIN =
|
||||||
|
process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz';
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
|
import {
|
||||||
|
DataSource,
|
||||||
|
DeepPartial,
|
||||||
|
FindManyOptions,
|
||||||
|
FindOneOptions,
|
||||||
|
FindOptionsWhere
|
||||||
|
} from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
@ -14,6 +20,12 @@ import { ProjectMember } from './entity/ProjectMember';
|
|||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { PROJECT_DOMAIN } from './constants';
|
import { PROJECT_DOMAIN } from './constants';
|
||||||
|
import { getEntities, loadAndSaveData } from './utils';
|
||||||
|
import { UserOrganization } from './entity/UserOrganization';
|
||||||
|
|
||||||
|
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||||
|
const USER_DATA_PATH = '../test/fixtures/users.json';
|
||||||
|
const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json';
|
||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
@ -36,6 +48,40 @@ export class Database {
|
|||||||
async init (): Promise<void> {
|
async init (): Promise<void> {
|
||||||
await this.dataSource.initialize();
|
await this.dataSource.initialize();
|
||||||
log('database initialized');
|
log('database initialized');
|
||||||
|
|
||||||
|
const organizations = await this.getOrganizations({});
|
||||||
|
|
||||||
|
if (!organizations.length) {
|
||||||
|
const orgEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [
|
||||||
|
orgEntities[0]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO: Remove user once authenticated
|
||||||
|
const userEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedUsers = await loadAndSaveData(User, this.dataSource, [
|
||||||
|
userEntities[0]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userOrganizationRelations = {
|
||||||
|
member: savedUsers,
|
||||||
|
organization: savedOrgs
|
||||||
|
};
|
||||||
|
|
||||||
|
const userOrgEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
UserOrganization,
|
||||||
|
this.dataSource,
|
||||||
|
[userOrgEntities[0]],
|
||||||
|
userOrganizationRelations
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
||||||
@ -60,7 +106,18 @@ export class Database {
|
|||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
|
async getOrganizations (
|
||||||
|
options: FindManyOptions<Organization>
|
||||||
|
): Promise<Organization[]> {
|
||||||
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
|
const organizations = await organizationRepository.find(options);
|
||||||
|
|
||||||
|
return organizations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganization (
|
||||||
|
options: FindOneOptions<Organization>
|
||||||
|
): Promise<Organization | null> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
const organization = await organizationRepository.findOne(options);
|
const organization = await organizationRepository.findOne(options);
|
||||||
|
|
||||||
@ -95,7 +152,11 @@ export class Database {
|
|||||||
|
|
||||||
const project = await projectRepository
|
const project = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
.leftJoinAndSelect(
|
||||||
|
'project.deployments',
|
||||||
|
'deployments',
|
||||||
|
'deployments.isCurrent = true'
|
||||||
|
)
|
||||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
.leftJoinAndSelect('project.owner', 'owner')
|
.leftJoinAndSelect('project.owner', 'owner')
|
||||||
@ -108,19 +169,29 @@ export class Database {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
|
async getProjectsInOrganization (
|
||||||
|
userId: string,
|
||||||
|
organizationSlug: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
.leftJoinAndSelect(
|
||||||
|
'project.deployments',
|
||||||
|
'deployments',
|
||||||
|
'deployments.isCurrent = true'
|
||||||
|
)
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.leftJoin('project.organization', 'organization')
|
.leftJoin('project.organization', 'organization')
|
||||||
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
|
.where(
|
||||||
userId,
|
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
|
||||||
organizationSlug
|
{
|
||||||
})
|
userId,
|
||||||
|
organizationSlug
|
||||||
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
@ -129,7 +200,9 @@ export class Database {
|
|||||||
/**
|
/**
|
||||||
* Get deployments with specified filter
|
* Get deployments with specified filter
|
||||||
*/
|
*/
|
||||||
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
|
async getDeployments (
|
||||||
|
options: FindManyOptions<Deployment>
|
||||||
|
): Promise<Deployment[]> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployments = await deploymentRepository.find(options);
|
const deployments = await deploymentRepository.find(options);
|
||||||
|
|
||||||
@ -154,7 +227,9 @@ export class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
|
async getDeployment (
|
||||||
|
options: FindOneOptions<Deployment>
|
||||||
|
): Promise<Deployment | null> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployment = await deploymentRepository.findOne(options);
|
const deployment = await deploymentRepository.findOne(options);
|
||||||
|
|
||||||
@ -182,8 +257,11 @@ export class Database {
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
async getProjectMembersByProjectId (
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
projectId: string
|
||||||
|
): Promise<ProjectMember[]> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const projectMembers = await projectMemberRepository.find({
|
const projectMembers = await projectMemberRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
@ -200,8 +278,12 @@ export class Database {
|
|||||||
return projectMembers;
|
return projectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
|
async getEnvironmentVariablesByProjectId (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
|
||||||
const environmentVariables = await environmentVariableRepository.find({
|
const environmentVariables = await environmentVariableRepository.find({
|
||||||
where: {
|
where: {
|
||||||
@ -216,9 +298,12 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const deleteResult = await projectMemberRepository.delete({ id: projectMemberId });
|
const deleteResult = await projectMemberRepository.delete({
|
||||||
|
id: projectMemberId
|
||||||
|
});
|
||||||
|
|
||||||
if (deleteResult.affected) {
|
if (deleteResult.affected) {
|
||||||
return deleteResult.affected > 0;
|
return deleteResult.affected > 0;
|
||||||
@ -227,37 +312,63 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
|
async updateProjectMemberById (
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
projectMemberId: string,
|
||||||
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
|
data: DeepPartial<ProjectMember>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
const updateResult = await projectMemberRepository.update(
|
||||||
|
{ id: projectMemberId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
|
async addProjectMember (
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
data: DeepPartial<ProjectMember>
|
||||||
|
): Promise<ProjectMember> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
const newProjectMember = await projectMemberRepository.save(data);
|
const newProjectMember = await projectMemberRepository.save(data);
|
||||||
|
|
||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
|
async addEnvironmentVariables (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
data: DeepPartial<EnvironmentVariable>[]
|
||||||
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const savedEnvironmentVariables =
|
||||||
|
await environmentVariableRepository.save(data);
|
||||||
|
|
||||||
return savedEnvironmentVariables;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
async updateEnvironmentVariable (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
environmentVariableId: string,
|
||||||
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
|
data: DeepPartial<EnvironmentVariable>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const updateResult = await environmentVariableRepository.update(
|
||||||
|
{ id: environmentVariableId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
async deleteEnvironmentVariable (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
environmentVariableId: string
|
||||||
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
|
): Promise<boolean> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const deleteResult = await environmentVariableRepository.delete({
|
||||||
|
id: environmentVariableId
|
||||||
|
});
|
||||||
|
|
||||||
if (deleteResult.affected) {
|
if (deleteResult.affected) {
|
||||||
return deleteResult.affected > 0;
|
return deleteResult.affected > 0;
|
||||||
@ -267,7 +378,8 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const projectMemberWithProject = await projectMemberRepository.find({
|
const projectMemberWithProject = await projectMemberRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
@ -279,8 +391,7 @@ export class Database {
|
|||||||
where: {
|
where: {
|
||||||
id: projectMemberId
|
id: projectMemberId
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (projectMemberWithProject.length === 0) {
|
if (projectMemberWithProject.length === 0) {
|
||||||
throw new Error('Member does not exist');
|
throw new Error('Member does not exist');
|
||||||
@ -289,34 +400,49 @@ export class Database {
|
|||||||
return projectMemberWithProject[0];
|
return projectMemberWithProject[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
|
async getProjectsBySearchText (
|
||||||
|
userId: string,
|
||||||
|
searchText: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
|
.where(
|
||||||
userId,
|
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
|
||||||
searchText: `%${searchText}%`
|
{
|
||||||
})
|
userId,
|
||||||
|
searchText: `%${searchText}%`
|
||||||
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentById (
|
||||||
|
deploymentId: string,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.updateDeployment({ id: deploymentId }, data);
|
return this.updateDeployment({ id: deploymentId }, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeployment (
|
||||||
|
criteria: FindOptionsWhere<Deployment>,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const updateResult = await deploymentRepository.update(criteria, data);
|
const updateResult = await deploymentRepository.update(criteria, data);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentsByProjectIds (
|
||||||
|
projectIds: string[],
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const updateResult = await deploymentRepository
|
const updateResult = await deploymentRepository
|
||||||
@ -329,7 +455,11 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
async addProject (
|
||||||
|
userId: string,
|
||||||
|
organizationId: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<Project> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
// TODO: Check if organization exists
|
// TODO: Check if organization exists
|
||||||
@ -352,9 +482,15 @@ export class Database {
|
|||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
async updateProjectById (
|
||||||
|
projectId: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<boolean> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const updateResult = await projectRepository.update({ id: projectId }, data);
|
const updateResult = await projectRepository.update(
|
||||||
|
{ id: projectId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
@ -401,14 +537,20 @@ export class Database {
|
|||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
async updateDomainById (
|
||||||
|
domainId: string,
|
||||||
|
data: DeepPartial<Domain>
|
||||||
|
): Promise<boolean> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const updateResult = await domainRepository.update({ id: domainId }, data);
|
const updateResult = await domainRepository.update({ id: domainId }, data);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
async getDomainsByProjectId (
|
||||||
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<Domain>
|
||||||
|
): Promise<Domain[]> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
|
|
||||||
const domains = await domainRepository.find({
|
const domains = await domainRepository.find({
|
||||||
|
@ -26,18 +26,27 @@ export enum DeploymentStatus {
|
|||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDeploymentRequest {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
name: string;
|
||||||
|
application: string;
|
||||||
|
config: string;
|
||||||
|
meta: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
version:string
|
version: string;
|
||||||
name: string
|
name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
homepage?: string
|
homepage?: string;
|
||||||
license?: string
|
license?: string;
|
||||||
author?: string
|
author?: string;
|
||||||
repository?: string[],
|
repository?: string[];
|
||||||
app_version?: string
|
app_version?: string;
|
||||||
repository_ref: string
|
repository_ref: string;
|
||||||
app_type: string
|
app_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@ -78,6 +87,12 @@ export class Deployment {
|
|||||||
@Column('simple-json')
|
@Column('simple-json')
|
||||||
applicationRecordData!: ApplicationRecord;
|
applicationRecordData!: ApplicationRecord;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
applicationDeploymentRequestId!: string;
|
||||||
|
|
||||||
|
@Column('simple-json')
|
||||||
|
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
applicationDeploymentRecordId!: string | null;
|
applicationDeploymentRecordId!: string | null;
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export class Domain {
|
|||||||
|
|
||||||
@ManyToOne(() => Domain)
|
@ManyToOne(() => Domain)
|
||||||
@JoinColumn({ name: 'redirectToId' })
|
@JoinColumn({ name: 'redirectToId' })
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
redirectTo!: Domain | null;
|
redirectTo!: Domain | null;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
|
@ -27,8 +27,12 @@ export class Organization {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
@OneToMany(
|
||||||
cascade: ['soft-remove']
|
() => UserOrganization,
|
||||||
})
|
(userOrganization) => userOrganization.organization,
|
||||||
|
{
|
||||||
|
cascade: ['soft-remove']
|
||||||
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,6 @@ import { Organization } from './Organization';
|
|||||||
import { ProjectMember } from './ProjectMember';
|
import { ProjectMember } from './ProjectMember';
|
||||||
import { Deployment } from './Deployment';
|
import { Deployment } from './Deployment';
|
||||||
|
|
||||||
export interface ApplicationDeploymentRequest {
|
|
||||||
type: string
|
|
||||||
version: string
|
|
||||||
name: string
|
|
||||||
application: string
|
|
||||||
config: string,
|
|
||||||
meta: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Project {
|
export class Project {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -52,12 +43,6 @@ export class Project {
|
|||||||
@Column('varchar', { length: 255, default: 'main' })
|
@Column('varchar', { length: 255, default: 'main' })
|
||||||
prodBranch!: string;
|
prodBranch!: string;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
applicationDeploymentRequestId!: string | null;
|
|
||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
|
||||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
|
|
||||||
|
|
||||||
@Column('text', { default: '' })
|
@Column('text', { default: '' })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
@ -91,7 +76,7 @@ export class Project {
|
|||||||
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||||
deployments!: Deployment[];
|
deployments!: Deployment[];
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
})
|
||||||
projectMembers!: ProjectMember[];
|
projectMembers!: ProjectMember[];
|
||||||
|
@ -15,7 +15,7 @@ import { User } from './User';
|
|||||||
|
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
View = 'View',
|
View = 'View',
|
||||||
Edit = 'Edit'
|
Edit = 'Edit',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -34,13 +34,17 @@ export class User {
|
|||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
})
|
||||||
projectMembers!: ProjectMember[];
|
projectMembers!: ProjectMember[];
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
@OneToMany(
|
||||||
cascade: ['soft-remove']
|
() => UserOrganization,
|
||||||
})
|
(UserOrganization) => UserOrganization.member,
|
||||||
|
{
|
||||||
|
cascade: ['soft-remove']
|
||||||
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,9 @@ const OAUTH_CLIENT_TYPE = 'oauth-app';
|
|||||||
|
|
||||||
export const main = async (): Promise<void> => {
|
export const main = async (): Promise<void> => {
|
||||||
// TODO: get config path using cli
|
// TODO: get config path using cli
|
||||||
const { server, database, gitHub, registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { server, database, gitHub, registryConfig } = await getConfig<Config>(
|
||||||
|
DEFAULT_CONFIG_FILE_PATH
|
||||||
|
);
|
||||||
|
|
||||||
const app = new OAuthApp({
|
const app = new OAuthApp({
|
||||||
clientType: OAUTH_CLIENT_TYPE,
|
clientType: OAUTH_CLIENT_TYPE,
|
||||||
@ -31,9 +33,16 @@ export const main = async (): Promise<void> => {
|
|||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig);
|
const registry = new Registry(registryConfig);
|
||||||
const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
|
const service = new Service(
|
||||||
|
{ gitHubConfig: gitHub, registryConfig },
|
||||||
|
db,
|
||||||
|
app,
|
||||||
|
registry
|
||||||
|
);
|
||||||
|
|
||||||
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
|
const typeDefs = fs
|
||||||
|
.readFileSync(path.join(__dirname, 'schema.gql'))
|
||||||
|
.toString();
|
||||||
const resolvers = await createResolvers(service);
|
const resolvers = await createResolvers(service);
|
||||||
|
|
||||||
await createAndStartServer(server, typeDefs, resolvers, service);
|
await createAndStartServer(server, typeDefs, resolvers, service);
|
||||||
|
@ -6,8 +6,11 @@ import { DateTime } from 'luxon';
|
|||||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import { ApplicationDeploymentRequest } from './entity/Project';
|
import {
|
||||||
import { ApplicationRecord, Deployment } from './entity/Deployment';
|
ApplicationRecord,
|
||||||
|
Deployment,
|
||||||
|
ApplicationDeploymentRequest
|
||||||
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
@ -21,35 +24,54 @@ export class Registry {
|
|||||||
private registry: LaconicRegistry;
|
private registry: LaconicRegistry;
|
||||||
private registryConfig: RegistryConfig;
|
private registryConfig: RegistryConfig;
|
||||||
|
|
||||||
constructor (registryConfig : RegistryConfig) {
|
constructor (registryConfig: RegistryConfig) {
|
||||||
this.registryConfig = registryConfig;
|
this.registryConfig = registryConfig;
|
||||||
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
this.registry = new LaconicRegistry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
registryConfig.chainId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationRecord ({
|
async createApplicationRecord ({
|
||||||
|
appName,
|
||||||
packageJSON,
|
packageJSON,
|
||||||
commitHash,
|
commitHash,
|
||||||
appType,
|
appType,
|
||||||
repoUrl
|
repoUrl
|
||||||
}: {
|
}: {
|
||||||
packageJSON: PackageJSON
|
appName: string;
|
||||||
commitHash: string,
|
packageJSON: PackageJSON;
|
||||||
appType: string,
|
commitHash: string;
|
||||||
repoUrl: string
|
appType: string;
|
||||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
repoUrl: string;
|
||||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
}): Promise<{
|
||||||
|
applicationRecordId: string;
|
||||||
|
applicationRecordData: ApplicationRecord;
|
||||||
|
}> {
|
||||||
// Use laconic-sdk to publish record
|
// Use laconic-sdk to publish record
|
||||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||||
// Fetch previous records
|
// Fetch previous records
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords(
|
||||||
type: APP_RECORD_TYPE,
|
{
|
||||||
name: packageJSON.name
|
type: APP_RECORD_TYPE,
|
||||||
}, true);
|
name: packageJSON.name
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Get next version of record
|
// Get next version of record
|
||||||
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
|
const bondRecords = records.filter(
|
||||||
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
(record: any) => record.bondId === this.registryConfig.bondId
|
||||||
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
);
|
||||||
|
const [latestBondRecord] = bondRecords.sort(
|
||||||
|
(a: any, b: any) =>
|
||||||
|
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||||
|
);
|
||||||
|
const nextVersion = semverInc(
|
||||||
|
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||||
|
'patch'
|
||||||
|
);
|
||||||
|
|
||||||
assert(nextVersion, 'Application record version not valid');
|
assert(nextVersion, 'Application record version not valid');
|
||||||
|
|
||||||
@ -60,11 +82,16 @@ export class Registry {
|
|||||||
repository_ref: commitHash,
|
repository_ref: commitHash,
|
||||||
repository: [repoUrl],
|
repository: [repoUrl],
|
||||||
app_type: appType,
|
app_type: appType,
|
||||||
name: packageJSON.name,
|
name: appName,
|
||||||
...(packageJSON.description && { description: packageJSON.description }),
|
...(packageJSON.description && { description: packageJSON.description }),
|
||||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||||
...(packageJSON.license && { license: packageJSON.license }),
|
...(packageJSON.license && { license: packageJSON.license }),
|
||||||
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }),
|
...(packageJSON.author && {
|
||||||
|
author:
|
||||||
|
typeof packageJSON.author === 'object'
|
||||||
|
? JSON.stringify(packageJSON.author)
|
||||||
|
: packageJSON.author
|
||||||
|
}),
|
||||||
...(packageJSON.version && { app_version: packageJSON.version })
|
...(packageJSON.version && { app_version: packageJSON.version })
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,26 +108,45 @@ export class Registry {
|
|||||||
log('Application record data:', applicationRecord);
|
log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// TODO: Discuss computation of CRN
|
// TODO: Discuss computation of CRN
|
||||||
const crn = this.getCrn(packageJSON.name);
|
const crn = this.getCrn(packageJSON.name, appName);
|
||||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||||
|
|
||||||
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
|
await this.registry.setName(
|
||||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
{ cid: result.data.id, crn },
|
||||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
this.registryConfig.privateKey,
|
||||||
|
this.registryConfig.fee
|
||||||
|
);
|
||||||
|
await this.registry.setName(
|
||||||
|
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
this.registryConfig.fee
|
||||||
|
);
|
||||||
|
await this.registry.setName(
|
||||||
|
{
|
||||||
|
cid: result.data.id,
|
||||||
|
crn: `${crn}@${applicationRecord.repository_ref}`
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
this.registryConfig.fee
|
||||||
|
);
|
||||||
|
|
||||||
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
|
return {
|
||||||
|
applicationRecordId: result.data.id,
|
||||||
|
applicationRecordData: applicationRecord
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRequest (data: {
|
async createApplicationDeploymentRequest (data: {
|
||||||
appName: string,
|
appName: string;
|
||||||
commitHash: string,
|
packageJsonName: string;
|
||||||
repository: string,
|
commitHash: string;
|
||||||
environmentVariables: { [key: string]: string }
|
repository: string;
|
||||||
|
environmentVariables: { [key: string]: string };
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRequestId: string,
|
applicationDeploymentRequestId: string;
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||||
}> {
|
}> {
|
||||||
const crn = this.getCrn(data.appName);
|
const crn = this.getCrn(data.packageJsonName, data.appName);
|
||||||
const records = await this.registry.resolveNames([crn]);
|
const records = await this.registry.resolveNames([crn]);
|
||||||
const applicationRecord = records[0];
|
const applicationRecord = records[0];
|
||||||
|
|
||||||
@ -124,7 +170,9 @@ export class Registry {
|
|||||||
env: data.environmentVariables
|
env: data.environmentVariables
|
||||||
}),
|
}),
|
||||||
meta: JSON.stringify({
|
meta: JSON.stringify({
|
||||||
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
|
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
||||||
|
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||||
|
)}`,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
repository_ref: data.commitHash
|
repository_ref: data.commitHash
|
||||||
})
|
})
|
||||||
@ -142,31 +190,40 @@ export class Registry {
|
|||||||
log(`Application deployment request record published: ${result.data.id}`);
|
log(`Application deployment request record published: ${result.data.id}`);
|
||||||
log('Application deployment request data:', applicationDeploymentRequest);
|
log('Application deployment request data:', applicationDeploymentRequest);
|
||||||
|
|
||||||
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
|
return {
|
||||||
|
applicationDeploymentRequestId: result.data.id,
|
||||||
|
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords for deployments
|
* Fetch ApplicationDeploymentRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
async getDeploymentRecords (
|
||||||
|
deployments: Deployment[]
|
||||||
|
): Promise<AppDeploymentRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords(
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
{
|
||||||
}, true);
|
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ids
|
// Filter records with ApplicationRecord ids
|
||||||
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
|
deployments.some(
|
||||||
|
(deployment) =>
|
||||||
|
deployment.applicationRecordId === record.attributes.application
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCrn (packageJsonName: string): string {
|
getCrn (packageJsonName: string, appName: string): string {
|
||||||
const [arg1, arg2] = packageJsonName.split('/');
|
const [arg1] = packageJsonName.split('/');
|
||||||
|
const authority = arg1.replace('@', '');
|
||||||
|
|
||||||
if (arg2) {
|
return `crn://${authority}/applications/${appName}`;
|
||||||
const authority = arg1.replace('@', '');
|
|
||||||
return `crn://${authority}/applications/${arg2}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `crn://${arg1}/applications/${arg1}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.getUser(context.userId);
|
return service.getUser(context.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
organizations: async (_:any, __: any, context: any) => {
|
organizations: async (_: any, __: any, context: any) => {
|
||||||
return service.getOrganizationsByUserId(context.userId);
|
return service.getOrganizationsByUserId(context.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -25,15 +25,25 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.getProjectById(projectId);
|
return service.getProjectById(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
projectsInOrganization: async (
|
||||||
return service.getProjectsInOrganization(context.userId, organizationSlug);
|
_: any,
|
||||||
|
{ organizationSlug }: { organizationSlug: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
return service.getProjectsInOrganization(
|
||||||
|
context.userId,
|
||||||
|
organizationSlug
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
return service.getDeploymentsByProjectId(projectId);
|
return service.getDeploymentsByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
environmentVariables: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId }: { projectId: string }
|
||||||
|
) => {
|
||||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -41,32 +51,55 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.getProjectMembersByProjectId(projectId);
|
return service.getProjectMembersByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
searchProjects: async (
|
||||||
|
_: any,
|
||||||
|
{ searchText }: { searchText: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
return service.searchProjects(context.userId, searchText);
|
return service.searchProjects(context.userId, searchText);
|
||||||
},
|
},
|
||||||
|
|
||||||
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
|
domains: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
filter
|
||||||
|
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
|
||||||
|
) => {
|
||||||
return service.getDomainsByProjectId(projectId, filter);
|
return service.getDomainsByProjectId(projectId, filter);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Return error in GQL response
|
// TODO: Return error in GQL response
|
||||||
Mutation: {
|
Mutation: {
|
||||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
removeProjectMember: async (
|
||||||
|
_: any,
|
||||||
|
{ projectMemberId }: { projectMemberId: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeProjectMember(context.userId, projectMemberId);
|
return await service.removeProjectMember(
|
||||||
|
context.userId,
|
||||||
|
projectMemberId
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProjectMember: async (_: any, { projectMemberId, data }: {
|
updateProjectMember: async (
|
||||||
projectMemberId: string,
|
_: any,
|
||||||
data: {
|
{
|
||||||
permissions: Permission[]
|
projectMemberId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
projectMemberId: string;
|
||||||
|
data: {
|
||||||
|
permissions: Permission[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProjectMember(projectMemberId, data);
|
return await service.updateProjectMember(projectMemberId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -75,13 +108,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addProjectMember: async (_: any, { projectId, data }: {
|
addProjectMember: async (
|
||||||
projectId: string,
|
_: any,
|
||||||
data: {
|
{
|
||||||
email: string,
|
projectId,
|
||||||
permissions: Permission[]
|
data
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
data: {
|
||||||
|
email: string;
|
||||||
|
permissions: Permission[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addProjectMember(projectId, data));
|
return Boolean(await service.addProjectMember(projectId, data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -90,25 +129,51 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => {
|
addEnvironmentVariables: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
data: { environments: string[]; key: string; value: string }[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addEnvironmentVariables(projectId, data));
|
return Boolean(
|
||||||
|
await service.addEnvironmentVariables(projectId, data)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
|
updateEnvironmentVariable: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
environmentVariableId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
environmentVariableId: string;
|
||||||
|
data: DeepPartial<EnvironmentVariable>;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateEnvironmentVariable(environmentVariableId, data);
|
return await service.updateEnvironmentVariable(
|
||||||
|
environmentVariableId,
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
removeEnvironmentVariable: async (
|
||||||
|
_: any,
|
||||||
|
{ environmentVariableId }: { environmentVariableId: string }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -117,25 +182,45 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
updateDeploymentToProd: async (
|
||||||
|
_: any,
|
||||||
|
{ deploymentId }: { deploymentId: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId));
|
return Boolean(
|
||||||
|
await service.updateDeploymentToProd(context.userId, deploymentId)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => {
|
addProject: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
organizationSlug,
|
||||||
|
data
|
||||||
|
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.addProject(context.userId, organizationSlug, data);
|
return await service.addProject(
|
||||||
|
context.userId,
|
||||||
|
organizationSlug,
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
|
updateProject: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProject(projectId, data);
|
return await service.updateProject(projectId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -144,9 +229,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
redeployToProd: async (
|
||||||
|
_: any,
|
||||||
|
{ deploymentId }: { deploymentId: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.redeployToProd(context.userId, deploymentId));
|
return Boolean(
|
||||||
|
await service.redeployToProd(context.userId, deploymentId)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@ -157,7 +248,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
try {
|
try {
|
||||||
return await service.deleteProject(projectId);
|
return await service.deleteProject(projectId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err); return false;
|
log(err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -170,7 +262,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
|
rollbackDeployment: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
deploymentId
|
||||||
|
}: { deploymentId: string; projectId: string }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.rollbackDeployment(projectId, deploymentId);
|
return await service.rollbackDeployment(projectId, deploymentId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -179,7 +277,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => {
|
addDomain: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId, data }: { projectId: string; data: { name: string } }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addDomain(projectId, data));
|
return Boolean(await service.addDomain(projectId, data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -188,7 +289,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => {
|
updateDomain: async (
|
||||||
|
_: any,
|
||||||
|
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateDomain(domainId, data);
|
return await service.updateDomain(domainId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -197,7 +301,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
|
authenticateGitHub: async (
|
||||||
|
_: any,
|
||||||
|
{ code }: { code: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.authenticateGitHub(code, context.userId);
|
return await service.authenticateGitHub(code, context.userId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -208,7 +316,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||||
try {
|
try {
|
||||||
return service.unauthenticateGitHub(context.userId, { gitHubToken: null });
|
return service.unauthenticateGitHub(context.userId, {
|
||||||
|
gitHubToken: null
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
|
@ -188,10 +188,19 @@ type Query {
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
updateProjectMember(
|
||||||
|
projectMemberId: String!
|
||||||
|
data: UpdateProjectMemberInput
|
||||||
|
): Boolean!
|
||||||
removeProjectMember(projectMemberId: String!): Boolean!
|
removeProjectMember(projectMemberId: String!): Boolean!
|
||||||
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
|
addEnvironmentVariables(
|
||||||
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
|
projectId: String!
|
||||||
|
data: [AddEnvironmentVariableInput!]
|
||||||
|
): Boolean!
|
||||||
|
updateEnvironmentVariable(
|
||||||
|
environmentVariableId: String!
|
||||||
|
data: UpdateEnvironmentVariableInput!
|
||||||
|
): Boolean!
|
||||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||||
|
@ -15,15 +15,15 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
|||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
import { GitHubConfig, RegistryConfig } from './config';
|
import { GitHubConfig, RegistryConfig } from './config';
|
||||||
import { AppDeploymentRecord, GitPushEventPayload } from './types';
|
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:service');
|
const log = debug('snowball:service');
|
||||||
|
|
||||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
gitHubConfig: GitHubConfig
|
gitHubConfig: GitHubConfig;
|
||||||
registryConfig: RegistryConfig
|
registryConfig: RegistryConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service {
|
export class Service {
|
||||||
@ -71,7 +71,9 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (deployments.length) {
|
if (deployments.length) {
|
||||||
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`);
|
log(
|
||||||
|
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch ApplicationDeploymentRecord for deployments
|
// Fetch ApplicationDeploymentRecord for deployments
|
||||||
const records = await this.registry.getDeploymentRecords(deployments);
|
const records = await this.registry.getDeploymentRecords(deployments);
|
||||||
@ -91,10 +93,12 @@ export class Service {
|
|||||||
/**
|
/**
|
||||||
* Update deployments with ApplicationDeploymentRecord data
|
* Update deployments with ApplicationDeploymentRecord data
|
||||||
*/
|
*/
|
||||||
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
|
async updateDeploymentsWithRecordData (
|
||||||
|
records: AppDeploymentRecord[]
|
||||||
|
): Promise<void> {
|
||||||
// Get deployments for ApplicationDeploymentRecords
|
// Get deployments for ApplicationDeploymentRecords
|
||||||
const deployments = await this.db.getDeployments({
|
const deployments = await this.db.getDeployments({
|
||||||
where: records.map(record => ({
|
where: records.map((record) => ({
|
||||||
applicationRecordId: record.attributes.application
|
applicationRecordId: record.attributes.application
|
||||||
})),
|
})),
|
||||||
order: {
|
order: {
|
||||||
@ -103,38 +107,46 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get project IDs of deployments that are in production environment
|
// Get project IDs of deployments that are in production environment
|
||||||
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
|
const productionDeploymentProjectIds = deployments.reduce(
|
||||||
if (deployment.environment === Environment.Production) {
|
(acc, deployment): Set<string> => {
|
||||||
acc.add(deployment.projectId);
|
if (deployment.environment === Environment.Production) {
|
||||||
}
|
acc.add(deployment.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, new Set<string>());
|
},
|
||||||
|
new Set<string>()
|
||||||
|
);
|
||||||
|
|
||||||
// Set old deployments isCurrent to false
|
// Set old deployments isCurrent to false
|
||||||
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false });
|
await this.db.updateDeploymentsByProjectIds(
|
||||||
|
Array.from(productionDeploymentProjectIds),
|
||||||
|
{ isCurrent: false }
|
||||||
|
);
|
||||||
|
|
||||||
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => {
|
const recordToDeploymentsMap = deployments.reduce(
|
||||||
acc[deployment.applicationRecordId] = deployment;
|
(acc: { [key: string]: Deployment }, deployment) => {
|
||||||
return acc;
|
acc[deployment.applicationRecordId] = deployment;
|
||||||
}, {});
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
// Update deployment data for ApplicationDeploymentRecords
|
// Update deployment data for ApplicationDeploymentRecords
|
||||||
const deploymentUpdatePromises = records.map(async (record) => {
|
const deploymentUpdatePromises = records.map(async (record) => {
|
||||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||||
|
|
||||||
await this.db.updateDeploymentById(
|
await this.db.updateDeploymentById(deployment.id, {
|
||||||
deployment.id,
|
applicationDeploymentRecordId: record.id,
|
||||||
{
|
applicationDeploymentRecordData: record.attributes,
|
||||||
applicationDeploymentRecordId: record.id,
|
url: record.attributes.url,
|
||||||
applicationDeploymentRecordData: record.attributes,
|
status: DeploymentStatus.Ready,
|
||||||
url: record.attributes.url,
|
isCurrent: deployment.environment === Environment.Production
|
||||||
status: DeploymentStatus.Ready,
|
});
|
||||||
isCurrent: deployment.environment === Environment.Production
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`);
|
log(
|
||||||
|
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(deploymentUpdatePromises);
|
await Promise.all(deploymentUpdatePromises);
|
||||||
@ -150,7 +162,10 @@ export class Service {
|
|||||||
|
|
||||||
async getOctokit (userId: string): Promise<Octokit> {
|
async getOctokit (userId: string): Promise<Octokit> {
|
||||||
const user = await this.db.getUser({ where: { id: userId } });
|
const user = await this.db.getUser({ where: { id: userId } });
|
||||||
assert(user && user.gitHubToken, 'User needs to be authenticated with GitHub token');
|
assert(
|
||||||
|
user && user.gitHubToken,
|
||||||
|
'User needs to be authenticated with GitHub token'
|
||||||
|
);
|
||||||
|
|
||||||
return new Octokit({ auth: user.gitHubToken });
|
return new Octokit({ auth: user.gitHubToken });
|
||||||
}
|
}
|
||||||
@ -165,8 +180,14 @@ export class Service {
|
|||||||
return dbProject;
|
return dbProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> {
|
async getProjectsInOrganization (
|
||||||
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug);
|
userId: string,
|
||||||
|
organizationSlug: string
|
||||||
|
): Promise<Project[]> {
|
||||||
|
const dbProjects = await this.db.getProjectsInOrganization(
|
||||||
|
userId,
|
||||||
|
organizationSlug
|
||||||
|
);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,35 +196,52 @@ export class Service {
|
|||||||
return dbDeployments;
|
return dbDeployments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> {
|
async getEnvironmentVariablesByProjectId (
|
||||||
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId);
|
projectId: string
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const dbEnvironmentVariables =
|
||||||
|
await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||||
return dbEnvironmentVariables;
|
return dbEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
async getProjectMembersByProjectId (
|
||||||
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId);
|
projectId: string
|
||||||
|
): Promise<ProjectMember[]> {
|
||||||
|
const dbProjectMembers =
|
||||||
|
await this.db.getProjectMembersByProjectId(projectId);
|
||||||
return dbProjectMembers;
|
return dbProjectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
|
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
|
||||||
const dbProjects = await this.db.getProjectsBySearchText(userId, searchText);
|
const dbProjects = await this.db.getProjectsBySearchText(
|
||||||
|
userId,
|
||||||
|
searchText
|
||||||
|
);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
async getDomainsByProjectId (
|
||||||
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<Domain>
|
||||||
|
): Promise<Domain[]> {
|
||||||
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
||||||
return dbDomains;
|
return dbDomains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
|
async updateProjectMember (
|
||||||
|
projectMemberId: string,
|
||||||
|
data: { permissions: Permission[] }
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateProjectMemberById(projectMemberId, data);
|
return this.db.updateProjectMemberById(projectMemberId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectMember (projectId: string,
|
async addProjectMember (
|
||||||
|
projectId: string,
|
||||||
data: {
|
data: {
|
||||||
email: string,
|
email: string;
|
||||||
permissions: Permission[]
|
permissions: Permission[];
|
||||||
}): Promise<ProjectMember> {
|
}
|
||||||
|
): Promise<ProjectMember> {
|
||||||
// TODO: Send invitation
|
// TODO: Send invitation
|
||||||
let user = await this.db.getUser({
|
let user = await this.db.getUser({
|
||||||
where: {
|
where: {
|
||||||
@ -231,7 +269,10 @@ export class Service {
|
|||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMember (userId: string, projectMemberId: string): Promise<boolean> {
|
async removeProjectMember (
|
||||||
|
userId: string,
|
||||||
|
projectMemberId: string
|
||||||
|
): Promise<boolean> {
|
||||||
const member = await this.db.getProjectMemberById(projectMemberId);
|
const member = await this.db.getProjectMemberById(projectMemberId);
|
||||||
|
|
||||||
if (String(member.member.id) === userId) {
|
if (String(member.member.id) === userId) {
|
||||||
@ -248,33 +289,48 @@ export class Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
|
async addEnvironmentVariables (
|
||||||
const formattedEnvironmentVariables = data.map((environmentVariable) => {
|
projectId: string,
|
||||||
return environmentVariable.environments.map((environment) => {
|
data: { environments: string[]; key: string; value: string }[]
|
||||||
return ({
|
): Promise<EnvironmentVariable[]> {
|
||||||
key: environmentVariable.key,
|
const formattedEnvironmentVariables = data
|
||||||
value: environmentVariable.value,
|
.map((environmentVariable) => {
|
||||||
environment: environment as Environment,
|
return environmentVariable.environments.map((environment) => {
|
||||||
project: Object.assign(new Project(), {
|
return {
|
||||||
id: projectId
|
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;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
async updateEnvironmentVariable (
|
||||||
|
environmentVariableId: string,
|
||||||
|
data: DeepPartial<EnvironmentVariable>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
async removeEnvironmentVariable (
|
||||||
|
environmentVariableId: string
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
async updateDeploymentToProd (
|
||||||
|
userId: string,
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
where: { id: deploymentId },
|
where: { id: deploymentId },
|
||||||
relations: {
|
relations: {
|
||||||
@ -286,20 +342,21 @@ export class Service {
|
|||||||
throw new Error('Deployment does not exist');
|
throw new Error('Deployment does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch });
|
const prodBranchDomains = await this.db.getDomainsByProjectId(
|
||||||
|
oldDeployment.project.id,
|
||||||
|
{ branch: oldDeployment.project.prodBranch }
|
||||||
|
);
|
||||||
|
|
||||||
const octokit = await this.getOctokit(userId);
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(userId,
|
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||||
octokit,
|
project: oldDeployment.project,
|
||||||
{
|
branch: oldDeployment.branch,
|
||||||
project: oldDeployment.project,
|
environment: Environment.Production,
|
||||||
branch: oldDeployment.branch,
|
domain: prodBranchDomains[0],
|
||||||
environment: Environment.Production,
|
commitHash: oldDeployment.commitHash,
|
||||||
domain: prodBranchDomains[0],
|
commitMessage: oldDeployment.commitMessage
|
||||||
commitHash: oldDeployment.commitHash,
|
});
|
||||||
commitMessage: oldDeployment.commitMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
@ -311,7 +368,9 @@ export class Service {
|
|||||||
recordData: { repoUrl?: string } = {}
|
recordData: { repoUrl?: string } = {}
|
||||||
): Promise<Deployment> {
|
): Promise<Deployment> {
|
||||||
assert(data.project?.repository, 'Project repository not found');
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
log(`Creating deployment in project ${data.project.name} from branch ${data.branch}`);
|
log(
|
||||||
|
`Creating deployment in project ${data.project.name} from branch ${data.branch}`
|
||||||
|
);
|
||||||
const [owner, repo] = data.project.repository.split('/');
|
const [owner, repo] = data.project.repository.split('/');
|
||||||
|
|
||||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||||
@ -326,28 +385,61 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||||
const packageJSON = JSON.parse(atob(packageJSONData.content));
|
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||||
|
|
||||||
|
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||||
|
|
||||||
if (!recordData.repoUrl) {
|
if (!recordData.repoUrl) {
|
||||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
const { data: repoDetails } = await octokit.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo
|
||||||
|
});
|
||||||
recordData.repoUrl = repoDetails.html_url;
|
recordData.repoUrl = repoDetails.html_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||||
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
|
const { applicationRecordId, applicationRecordData } =
|
||||||
packageJSON,
|
await this.registry.createApplicationRecord({
|
||||||
appType: data.project!.template!,
|
appName: repo,
|
||||||
commitHash: data.commitHash!,
|
packageJSON,
|
||||||
repoUrl: recordData.repoUrl
|
appType: data.project!.template!,
|
||||||
});
|
commitHash: data.commitHash!,
|
||||||
|
repoUrl: recordData.repoUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
const environmentVariables =
|
||||||
|
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
|
||||||
|
environment: Environment.Production
|
||||||
|
});
|
||||||
|
|
||||||
|
const environmentVariablesObj = environmentVariables.reduce(
|
||||||
|
(acc, env) => {
|
||||||
|
acc[env.key] = env.value;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as { [key: string]: string }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
||||||
|
await this.registry.createApplicationDeploymentRequest({
|
||||||
|
appName: repo,
|
||||||
|
packageJsonName: packageJSON.name,
|
||||||
|
commitHash: data.commitHash!,
|
||||||
|
repository: recordData.repoUrl,
|
||||||
|
environmentVariables: environmentVariablesObj
|
||||||
|
});
|
||||||
|
|
||||||
// Update previous deployment with prod branch domain
|
// Update previous deployment with prod branch domain
|
||||||
// TODO: Fix unique constraint error for domain
|
// TODO: Fix unique constraint error for domain
|
||||||
await this.db.updateDeployment({
|
await this.db.updateDeployment(
|
||||||
domainId: data.domain?.id
|
{
|
||||||
}, {
|
domainId: data.domain?.id
|
||||||
domain: null
|
},
|
||||||
});
|
{
|
||||||
|
domain: null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const newDeployment = await this.db.addDeployment({
|
const newDeployment = await this.db.addDeployment({
|
||||||
project: data.project,
|
project: data.project,
|
||||||
@ -358,17 +450,25 @@ export class Service {
|
|||||||
status: DeploymentStatus.Building,
|
status: DeploymentStatus.Building,
|
||||||
applicationRecordId,
|
applicationRecordId,
|
||||||
applicationRecordData,
|
applicationRecordData,
|
||||||
|
applicationDeploymentRequestId,
|
||||||
|
applicationDeploymentRequestData,
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
createdBy: Object.assign(new User(), {
|
createdBy: Object.assign(new User(), {
|
||||||
id: userId
|
id: userId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
|
log(
|
||||||
|
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`
|
||||||
|
);
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
|
async addProject (
|
||||||
|
userId: string,
|
||||||
|
organizationSlug: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<Project | undefined> {
|
||||||
const organization = await this.db.getOrganization({
|
const organization = await this.db.getOrganization({
|
||||||
where: {
|
where: {
|
||||||
slug: organizationSlug
|
slug: organizationSlug
|
||||||
@ -383,7 +483,9 @@ export class Service {
|
|||||||
const octokit = await this.getOctokit(userId);
|
const octokit = await this.getOctokit(userId);
|
||||||
const [owner, repo] = project.repository.split('/');
|
const [owner, repo] = project.repository.split('/');
|
||||||
|
|
||||||
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({
|
const {
|
||||||
|
data: [latestCommit]
|
||||||
|
} = await octokit.rest.repos.listCommits({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
sha: project.prodBranch,
|
sha: project.prodBranch,
|
||||||
@ -393,7 +495,8 @@ export class Service {
|
|||||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||||
|
|
||||||
// Create deployment with prod branch and latest commit
|
// Create deployment with prod branch and latest commit
|
||||||
const newDeployment = await this.createDeployment(userId,
|
await this.createDeployment(
|
||||||
|
userId,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project,
|
project,
|
||||||
@ -408,27 +511,6 @@ export class Service {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(project.id, { environment: Environment.Production });
|
|
||||||
|
|
||||||
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
|
|
||||||
acc[env.key] = env.value;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {} as { [key: string]: string });
|
|
||||||
|
|
||||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
|
|
||||||
{
|
|
||||||
appName: newDeployment.applicationRecordData.name!,
|
|
||||||
commitHash: latestCommit.sha,
|
|
||||||
repository: repoDetails.html_url,
|
|
||||||
environmentVariables: environmentVariablesObj
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.updateProjectById(project.id, {
|
|
||||||
applicationDeploymentRequestId,
|
|
||||||
applicationDeploymentRequestData
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.createRepoHook(octokit, project);
|
await this.createRepoHook(octokit, project);
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
@ -441,7 +523,10 @@ export class Service {
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
config: {
|
config: {
|
||||||
url: new URL('api/github/webhook', this.config.gitHubConfig.webhookUrl).href,
|
url: new URL(
|
||||||
|
'api/github/webhook',
|
||||||
|
this.config.gitHubConfig.webhookUrl
|
||||||
|
).href,
|
||||||
content_type: 'json'
|
content_type: 'json'
|
||||||
},
|
},
|
||||||
events: ['push']
|
events: ['push']
|
||||||
@ -449,9 +534,13 @@ export class Service {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
||||||
if (
|
if (
|
||||||
!(err instanceof RequestError &&
|
!(
|
||||||
err.status === 422 &&
|
err instanceof RequestError &&
|
||||||
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR))
|
err.status === 422 &&
|
||||||
|
(err.response?.data as any).errors.some(
|
||||||
|
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@ -463,7 +552,9 @@ export class Service {
|
|||||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||||
const { repository, ref, head_commit: headCommit } = data;
|
const { repository, ref, head_commit: headCommit } = data;
|
||||||
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
||||||
const projects = await this.db.getProjects({ where: { repository: repository.full_name } });
|
const projects = await this.db.getProjects({
|
||||||
|
where: { repository: repository.full_name }
|
||||||
|
});
|
||||||
|
|
||||||
if (!projects.length) {
|
if (!projects.length) {
|
||||||
log(`No projects found for repository ${repository.full_name}`);
|
log(`No projects found for repository ${repository.full_name}`);
|
||||||
@ -475,23 +566,29 @@ export class Service {
|
|||||||
|
|
||||||
for await (const project of projects) {
|
for await (const project of projects) {
|
||||||
const octokit = await this.getOctokit(project.ownerId);
|
const octokit = await this.getOctokit(project.ownerId);
|
||||||
const [domain] = await this.db.getDomainsByProjectId(project.id, { branch });
|
const [domain] = await this.db.getDomainsByProjectId(project.id, {
|
||||||
|
branch
|
||||||
|
});
|
||||||
|
|
||||||
// Create deployment with branch and latest commit in GitHub data
|
// Create deployment with branch and latest commit in GitHub data
|
||||||
await this.createDeployment(project.ownerId,
|
await this.createDeployment(project.ownerId, octokit, {
|
||||||
octokit,
|
project,
|
||||||
{
|
branch,
|
||||||
project,
|
environment:
|
||||||
branch,
|
project.prodBranch === branch
|
||||||
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
? Environment.Production
|
||||||
domain,
|
: Environment.Preview,
|
||||||
commitHash: headCommit.id,
|
domain,
|
||||||
commitMessage: headCommit.message
|
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);
|
return this.db.updateProjectById(projectId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,13 +605,18 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (domainsRedirectedFrom.length > 0) {
|
if (domainsRedirectedFrom.length > 0) {
|
||||||
throw new Error('Cannot delete domain since it has redirects from other domains');
|
throw new Error(
|
||||||
|
'Cannot delete domain since it has redirects from other domains'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.db.deleteDomainById(domainId);
|
return this.db.deleteDomainById(domainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
async redeployToProd (
|
||||||
|
userId: string,
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
@ -532,22 +634,23 @@ export class Service {
|
|||||||
|
|
||||||
const octokit = await this.getOctokit(userId);
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(userId,
|
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||||
octokit,
|
project: oldDeployment.project,
|
||||||
{
|
// TODO: Put isCurrent field in project
|
||||||
project: oldDeployment.project,
|
branch: oldDeployment.branch,
|
||||||
// TODO: Put isCurrent field in project
|
environment: Environment.Production,
|
||||||
branch: oldDeployment.branch,
|
domain: oldDeployment.domain,
|
||||||
environment: Environment.Production,
|
commitHash: oldDeployment.commitHash,
|
||||||
domain: oldDeployment.domain,
|
commitMessage: oldDeployment.commitMessage
|
||||||
commitHash: oldDeployment.commitHash,
|
});
|
||||||
commitMessage: oldDeployment.commitMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
async rollbackDeployment (
|
||||||
|
projectId: string,
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<boolean> {
|
||||||
// TODO: Implement transactions
|
// TODO: Implement transactions
|
||||||
const oldCurrentDeployment = await this.db.getDeployment({
|
const oldCurrentDeployment = await this.db.getDeployment({
|
||||||
relations: {
|
relations: {
|
||||||
@ -565,16 +668,25 @@ export class Service {
|
|||||||
throw new Error('Current deployment doesnot exist');
|
throw new Error('Current deployment doesnot exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
|
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||||
|
oldCurrentDeployment.id,
|
||||||
|
{ isCurrent: false, domain: null }
|
||||||
|
);
|
||||||
|
|
||||||
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain });
|
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||||
|
deploymentId,
|
||||||
|
{ isCurrent: true, domain: oldCurrentDeployment?.domain }
|
||||||
|
);
|
||||||
|
|
||||||
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDomain (projectId: string, data: { name: string }): Promise<{
|
async addDomain (
|
||||||
primaryDomain: Domain,
|
projectId: string,
|
||||||
redirectedDomain: Domain
|
data: { name: string }
|
||||||
|
): Promise<{
|
||||||
|
primaryDomain: Domain;
|
||||||
|
redirectedDomain: Domain;
|
||||||
}> {
|
}> {
|
||||||
const currentProject = await this.db.getProjectById(projectId);
|
const currentProject = await this.db.getProjectById(projectId);
|
||||||
|
|
||||||
@ -599,12 +711,20 @@ export class Service {
|
|||||||
redirectTo: savedPrimaryDomain
|
redirectTo: savedPrimaryDomain
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails);
|
const savedRedirectedDomain = await this.db.addDomain(
|
||||||
|
redirectedDomainDetails
|
||||||
|
);
|
||||||
|
|
||||||
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain };
|
return {
|
||||||
|
primaryDomain: savedPrimaryDomain,
|
||||||
|
redirectedDomain: savedRedirectedDomain
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomain (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
async updateDomain (
|
||||||
|
domainId: string,
|
||||||
|
data: DeepPartial<Domain>
|
||||||
|
): Promise<boolean> {
|
||||||
const domain = await this.db.getDomain({
|
const domain = await this.db.getDomain({
|
||||||
where: {
|
where: {
|
||||||
id: domainId
|
id: domainId
|
||||||
@ -645,7 +765,9 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (redirectedDomain.redirectToId) {
|
if (redirectedDomain.redirectToId) {
|
||||||
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
|
throw new Error(
|
||||||
|
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
newDomain.redirectTo = redirectedDomain;
|
newDomain.redirectTo = redirectedDomain;
|
||||||
@ -656,8 +778,13 @@ export class Service {
|
|||||||
return updateResult;
|
return updateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
|
async authenticateGitHub (
|
||||||
const { authentication: { token } } = await this.oauthApp.createToken({
|
code: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<{ token: string }> {
|
||||||
|
const {
|
||||||
|
authentication: { token }
|
||||||
|
} = await this.oauthApp.createToken({
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -666,7 +793,10 @@ export class Service {
|
|||||||
return { token };
|
return { token };
|
||||||
}
|
}
|
||||||
|
|
||||||
async unauthenticateGitHub (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
async unauthenticateGitHub (
|
||||||
|
userId: string,
|
||||||
|
data: DeepPartial<User>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateUser(userId, data);
|
return this.db.updateUser(userId, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export interface PackageJSON {
|
export interface PackageJSON {
|
||||||
name?: string;
|
name: string;
|
||||||
version?: string;
|
version: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
homepage?: string;
|
homepage?: string;
|
||||||
@ -47,5 +47,5 @@ interface RegistryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
export interface AppDeploymentRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRecordAttributes
|
attributes: AppDeploymentRecordAttributes;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import toml from 'toml';
|
import toml from 'toml';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
const log = debug('snowball:utils');
|
const log = debug('snowball:utils');
|
||||||
|
|
||||||
@ -19,3 +20,49 @@ export const getConfig = async <ConfigType>(
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkFileExists = async (filePath: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntities = async (filePath: string): Promise<any> => {
|
||||||
|
const entitiesData = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const entities = JSON.parse(entitiesData);
|
||||||
|
return entities;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||||
|
entityType: EntityTarget<Entity>,
|
||||||
|
dataSource: DataSource,
|
||||||
|
entities: any,
|
||||||
|
relations?: any | undefined
|
||||||
|
): Promise<Entity[]> => {
|
||||||
|
const entityRepository = dataSource.getRepository(entityType);
|
||||||
|
|
||||||
|
const savedEntity: Entity[] = [];
|
||||||
|
|
||||||
|
for (const entityData of entities) {
|
||||||
|
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
||||||
|
|
||||||
|
if (relations) {
|
||||||
|
for (const field in relations) {
|
||||||
|
const valueIndex = String(field + 'Index');
|
||||||
|
|
||||||
|
entity = {
|
||||||
|
...entity,
|
||||||
|
[field]: relations[field][entityData[valueIndex]]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dbEntity = await entityRepository.save(entity);
|
||||||
|
savedEntity.push(dbEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedEntity;
|
||||||
|
};
|
||||||
|
@ -18,4 +18,4 @@ const main = async () => {
|
|||||||
deleteFile(config.database.dbPath);
|
deleteFile(config.database.dbPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
main().catch(err => log(err));
|
main().catch((err) => log(err));
|
||||||
|
62
packages/backend/test/fixtures/deployments.json
vendored
62
packages/backend/test/fixtures/deployments.json
vendored
@ -1,14 +1,16 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex":0,
|
"domainIndex": 0,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"ffhae3zq",
|
"id": "ffhae3zq",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -16,14 +18,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex":1,
|
"domainIndex": 1,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"vehagei8",
|
"id": "vehagei8",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -31,14 +35,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex":2,
|
"domainIndex": 2,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"qmgekyte",
|
"id": "qmgekyte",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -48,12 +54,14 @@
|
|||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex": null,
|
"domainIndex": null,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"f8wsyim6",
|
"id": "f8wsyim6",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "prod",
|
"branch": "prod",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -61,14 +69,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"domainIndex":3,
|
"domainIndex": 3,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"eO8cckxk",
|
"id": "eO8cckxk",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -76,14 +86,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"domainIndex":4,
|
"domainIndex": 4,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"yaq0t5yw",
|
"id": "yaq0t5yw",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -91,14 +103,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"domainIndex":5,
|
"domainIndex": 5,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"hwwr6sbx",
|
"id": "hwwr6sbx",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -106,14 +120,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"domainIndex":9,
|
"domainIndex": 9,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"ndxje48a",
|
"id": "ndxje48a",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -121,14 +137,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"domainIndex":7,
|
"domainIndex": 7,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"gtgpgvei",
|
"id": "gtgpgvei",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -136,14 +154,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"domainIndex":8,
|
"domainIndex": 8,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"b4bpthjr",
|
"id": "b4bpthjr",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@ -153,12 +173,14 @@
|
|||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"domainIndex": 6,
|
"domainIndex": 6,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"b4bpthjr",
|
"id": "b4bpthjr",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
|
@ -2,77 +2,55 @@
|
|||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 0,
|
"memberIndex": 0,
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 0,
|
"memberIndex": 0,
|
||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 4,
|
"projectIndex": 4,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 4,
|
"projectIndex": 4,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
10
packages/backend/test/fixtures/projects.json
vendored
10
packages/backend/test/fixtures/projects.json
vendored
@ -10,8 +10,6 @@
|
|||||||
"framework": "test",
|
"framework": "test",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "testProject.snowball.xyz"
|
"subDomain": "testProject.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -25,8 +23,6 @@
|
|||||||
"framework": "test-2",
|
"framework": "test-2",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "testProject-2.snowball.xyz"
|
"subDomain": "testProject-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -40,8 +36,6 @@
|
|||||||
"framework": "test-3",
|
"framework": "test-3",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "iglootools.snowball.xyz"
|
"subDomain": "iglootools.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -55,8 +49,6 @@
|
|||||||
"framework": "test-4",
|
"framework": "test-4",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "iglootools-2.snowball.xyz"
|
"subDomain": "iglootools-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -70,8 +62,6 @@
|
|||||||
"framework": "test-5",
|
"framework": "test-5",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "snowball-2.snowball.xyz"
|
"subDomain": "snowball-2.snowball.xyz"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@ -11,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
|
|||||||
import { Domain } from '../src/entity/Domain';
|
import { Domain } from '../src/entity/Domain';
|
||||||
import { ProjectMember } from '../src/entity/ProjectMember';
|
import { ProjectMember } from '../src/entity/ProjectMember';
|
||||||
import { Deployment } from '../src/entity/Deployment';
|
import { Deployment } from '../src/entity/Deployment';
|
||||||
import { getConfig } from '../src/utils';
|
import {
|
||||||
|
checkFileExists,
|
||||||
|
getConfig,
|
||||||
|
getEntities,
|
||||||
|
loadAndSaveData
|
||||||
|
} from '../src/utils';
|
||||||
import { Config } from '../src/config';
|
import { Config } from '../src/config';
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||||
|
|
||||||
@ -27,43 +31,35 @@ const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
|
|||||||
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
||||||
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
||||||
|
|
||||||
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
|
|
||||||
const entitiesData = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const entities = JSON.parse(entitiesData);
|
|
||||||
const entityRepository = dataSource.getRepository(entityType);
|
|
||||||
|
|
||||||
const savedEntity:Entity[] = [];
|
|
||||||
|
|
||||||
for (const entityData of entities) {
|
|
||||||
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
|
||||||
|
|
||||||
if (relations) {
|
|
||||||
for (const field in relations) {
|
|
||||||
const valueIndex = String(field + 'Index');
|
|
||||||
|
|
||||||
entity = {
|
|
||||||
...entity,
|
|
||||||
[field]: relations[field][entityData[valueIndex]]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dbEntity = await entityRepository.save(entity);
|
|
||||||
savedEntity.push(dbEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return savedEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateTestData = async (dataSource: DataSource) => {
|
const generateTestData = async (dataSource: DataSource) => {
|
||||||
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
|
const userEntities = await getEntities(
|
||||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
path.resolve(__dirname, USER_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
||||||
|
|
||||||
|
const orgEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedOrgs = await loadAndSaveData(
|
||||||
|
Organization,
|
||||||
|
dataSource,
|
||||||
|
orgEntities
|
||||||
|
);
|
||||||
|
|
||||||
const projectRelations = {
|
const projectRelations = {
|
||||||
owner: savedUsers,
|
owner: savedUsers,
|
||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
|
const projectEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedProjects = await loadAndSaveData(
|
||||||
|
Project,
|
||||||
|
dataSource,
|
||||||
|
projectEntities,
|
||||||
|
projectRelations
|
||||||
|
);
|
||||||
|
|
||||||
const domainRepository = dataSource.getRepository(Domain);
|
const domainRepository = dataSource.getRepository(Domain);
|
||||||
|
|
||||||
@ -71,14 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations);
|
const primaryDomainsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedPrimaryDomains = await loadAndSaveData(
|
||||||
|
Domain,
|
||||||
|
dataSource,
|
||||||
|
primaryDomainsEntities,
|
||||||
|
domainPrimaryRelations
|
||||||
|
);
|
||||||
|
|
||||||
const domainRedirectedRelations = {
|
const domainRedirectedRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
redirectTo: savedPrimaryDomains
|
redirectTo: savedPrimaryDomains
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations);
|
const redirectDomainsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Domain,
|
||||||
|
dataSource,
|
||||||
|
redirectDomainsEntities,
|
||||||
|
domainRedirectedRelations
|
||||||
|
);
|
||||||
|
|
||||||
const savedDomains = await domainRepository.find();
|
const savedDomains = await domainRepository.find();
|
||||||
|
|
||||||
@ -87,14 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
|
const userOrganizationsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
UserOrganization,
|
||||||
|
dataSource,
|
||||||
|
userOrganizationsEntities,
|
||||||
|
userOrganizationRelations
|
||||||
|
);
|
||||||
|
|
||||||
const projectMemberRelations = {
|
const projectMemberRelations = {
|
||||||
member: savedUsers,
|
member: savedUsers,
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
|
const projectMembersEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
ProjectMember,
|
||||||
|
dataSource,
|
||||||
|
projectMembersEntities,
|
||||||
|
projectMemberRelations
|
||||||
|
);
|
||||||
|
|
||||||
const deploymentRelations = {
|
const deploymentRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
@ -102,23 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
createdBy: savedUsers
|
createdBy: savedUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
|
const deploymentsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Deployment,
|
||||||
|
dataSource,
|
||||||
|
deploymentsEntities,
|
||||||
|
deploymentRelations
|
||||||
|
);
|
||||||
|
|
||||||
const environmentVariableRelations = {
|
const environmentVariableRelations = {
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
|
const environmentVariablesEntities = await getEntities(
|
||||||
};
|
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||||
|
);
|
||||||
const checkFileExists = async (filePath: string) => {
|
await loadAndSaveData(
|
||||||
try {
|
EnvironmentVariable,
|
||||||
await fs.access(filePath, fs.constants.F_OK);
|
dataSource,
|
||||||
return true;
|
environmentVariablesEntities,
|
||||||
} catch (err) {
|
environmentVariableRelations
|
||||||
log(err);
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
@ -17,16 +17,32 @@ const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
|
|||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
const registry = new Registry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
registryConfig.chainId
|
||||||
|
);
|
||||||
|
|
||||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||||
log('bondId:', bondId);
|
log('bondId:', bondId);
|
||||||
await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee);
|
await registry.createBond(
|
||||||
|
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
|
|
||||||
for await (const name of AUTHORITY_NAMES) {
|
for await (const name of AUTHORITY_NAMES) {
|
||||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
await registry.reserveAuthority(
|
||||||
|
{ name },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
log('Reserved authority name:', name);
|
log('Reserved authority name:', name);
|
||||||
await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee);
|
await registry.setAuthorityBond(
|
||||||
|
{ name, bondId },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
log(`Bond ${bondId} set for authority ${name}`);
|
log(`Bond ${bondId} set for authority ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,15 @@ import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
|||||||
const log = debug('snowball:publish-deploy-records');
|
const log = debug('snowball:publish-deploy-records');
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig, database } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig, database } = await getConfig<Config>(
|
||||||
|
DEFAULT_CONFIG_FILE_PATH
|
||||||
|
);
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
const registry = new Registry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
registryConfig.chainId
|
||||||
|
);
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
@ -53,7 +59,7 @@ async function main () {
|
|||||||
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
request: deployment.project.applicationDeploymentRequestId,
|
request: deployment.applicationDeploymentRequestId,
|
||||||
url
|
url
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,8 +7,11 @@
|
|||||||
"@material-tailwind/react": "^2.1.7",
|
"@material-tailwind/react": "^2.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
@ -98,19 +98,29 @@ const Button = forwardRef<
|
|||||||
href,
|
href,
|
||||||
...baseLinkProps,
|
...baseLinkProps,
|
||||||
};
|
};
|
||||||
return <a {...externalLinkProps}>{_children}</a>;
|
return (
|
||||||
|
// @ts-expect-error - ref
|
||||||
|
<a ref={ref} {...externalLinkProps}>
|
||||||
|
{_children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal link
|
// Internal link
|
||||||
return (
|
return (
|
||||||
<Link {...baseLinkProps} to={href}>
|
// @ts-expect-error - ref
|
||||||
|
<Link ref={ref} {...baseLinkProps} to={href}>
|
||||||
{_children}
|
{_children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const { ...buttonProps } = _props;
|
const { ...buttonProps } = _props;
|
||||||
// @ts-expect-error - as prop is not a valid prop for button elements
|
return (
|
||||||
return <button {...buttonProps}>{_children}</button>;
|
// @ts-expect-error - as prop is not a valid prop for button elements
|
||||||
|
<button ref={ref} {...buttonProps}>
|
||||||
|
{_children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@ -161,8 +171,6 @@ const Button = forwardRef<
|
|||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...props}
|
{...props}
|
||||||
// @ts-expect-error - ref is not a valid prop for button elements
|
|
||||||
ref={ref}
|
|
||||||
className={buttonTheme({ ...styleProps, class: className })}
|
className={buttonTheme({ ...styleProps, class: className })}
|
||||||
>
|
>
|
||||||
{cloneIcon(leftIcon, { ...iconSize })}
|
{cloneIcon(leftIcon, { ...iconSize })}
|
||||||
|
@ -50,6 +50,7 @@ export const inputTheme = tv(
|
|||||||
'outline-offset-0',
|
'outline-offset-0',
|
||||||
'outline-border-danger',
|
'outline-border-danger',
|
||||||
'shadow-none',
|
'shadow-none',
|
||||||
|
'focus:outline-border-danger',
|
||||||
],
|
],
|
||||||
helperText: 'text-elements-danger',
|
helperText: 'text-elements-danger',
|
||||||
},
|
},
|
||||||
|
54
packages/frontend/src/components/shared/Radio/Radio.theme.ts
Normal file
54
packages/frontend/src/components/shared/Radio/Radio.theme.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const radioTheme = tv({
|
||||||
|
slots: {
|
||||||
|
root: ['flex', 'gap-3', 'flex-wrap'],
|
||||||
|
wrapper: ['flex', 'items-center', 'gap-2', 'group'],
|
||||||
|
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
|
||||||
|
radio: [
|
||||||
|
'w-5',
|
||||||
|
'h-5',
|
||||||
|
'rounded-full',
|
||||||
|
'border',
|
||||||
|
'group',
|
||||||
|
'border-border-interactive/10',
|
||||||
|
'shadow-button',
|
||||||
|
'group-hover:border-border-interactive/[0.14]',
|
||||||
|
'focus-ring',
|
||||||
|
// Checked
|
||||||
|
'data-[state=checked]:bg-controls-primary',
|
||||||
|
'data-[state=checked]:group-hover:bg-controls-primary-hovered',
|
||||||
|
],
|
||||||
|
indicator: [
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'justify-center',
|
||||||
|
'relative',
|
||||||
|
'w-full',
|
||||||
|
'h-full',
|
||||||
|
'after:content-[""]',
|
||||||
|
'after:block',
|
||||||
|
'after:w-2.5',
|
||||||
|
'after:h-2.5',
|
||||||
|
'after:rounded-full',
|
||||||
|
'after:bg-transparent',
|
||||||
|
'after:group-hover:bg-controls-disabled',
|
||||||
|
'after:group-focus-visible:bg-controls-disabled',
|
||||||
|
// Checked
|
||||||
|
'after:data-[state=checked]:bg-elements-on-primary',
|
||||||
|
'after:data-[state=checked]:group-hover:bg-elements-on-primary',
|
||||||
|
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
vertical: { root: ['flex-col'] },
|
||||||
|
horizontal: { root: ['flex-row'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: 'vertical',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RadioTheme = VariantProps<typeof radioTheme>;
|
63
packages/frontend/src/components/shared/Radio/Radio.tsx
Normal file
63
packages/frontend/src/components/shared/Radio/Radio.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Root as RadixRoot,
|
||||||
|
RadioGroupProps,
|
||||||
|
} from '@radix-ui/react-radio-group';
|
||||||
|
import { RadioTheme, radioTheme } from './Radio.theme';
|
||||||
|
import { RadioItem, RadioItemProps } from './RadioItem';
|
||||||
|
|
||||||
|
export interface RadioOption extends RadioItemProps {
|
||||||
|
/**
|
||||||
|
* The label of the radio option.
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* The value of the radio option.
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadioProps extends RadioGroupProps, RadioTheme {
|
||||||
|
/**
|
||||||
|
* The options of the radio.
|
||||||
|
* @default []
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const options = [
|
||||||
|
* {
|
||||||
|
* label: 'Label 1',
|
||||||
|
* value: '1',
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* label: 'Label 2',
|
||||||
|
* value: '2',
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* label: 'Label 3',
|
||||||
|
* value: '3',
|
||||||
|
* },
|
||||||
|
* ];
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
options: RadioOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Radio component is used to select one option from a list of options.
|
||||||
|
*/
|
||||||
|
export const Radio = ({
|
||||||
|
className,
|
||||||
|
options,
|
||||||
|
orientation,
|
||||||
|
...props
|
||||||
|
}: RadioProps) => {
|
||||||
|
const { root } = radioTheme({ orientation });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadixRoot {...props} className={root({ className })}>
|
||||||
|
{options.map((option) => (
|
||||||
|
<RadioItem key={option.value} {...option} />
|
||||||
|
))}
|
||||||
|
</RadixRoot>
|
||||||
|
);
|
||||||
|
};
|
74
packages/frontend/src/components/shared/Radio/RadioItem.tsx
Normal file
74
packages/frontend/src/components/shared/Radio/RadioItem.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React, { ComponentPropsWithoutRef } from 'react';
|
||||||
|
import {
|
||||||
|
Item as RadixRadio,
|
||||||
|
Indicator as RadixIndicator,
|
||||||
|
RadioGroupItemProps,
|
||||||
|
RadioGroupIndicatorProps,
|
||||||
|
} from '@radix-ui/react-radio-group';
|
||||||
|
import { radioTheme } from './Radio.theme';
|
||||||
|
|
||||||
|
export interface RadioItemProps extends RadioGroupItemProps {
|
||||||
|
/**
|
||||||
|
* The wrapper props of the radio item.
|
||||||
|
* You can use this prop to customize the wrapper props.
|
||||||
|
*/
|
||||||
|
wrapperProps?: ComponentPropsWithoutRef<'div'>;
|
||||||
|
/**
|
||||||
|
* The label props of the radio item.
|
||||||
|
* You can use this prop to customize the label props.
|
||||||
|
*/
|
||||||
|
labelProps?: ComponentPropsWithoutRef<'label'>;
|
||||||
|
/**
|
||||||
|
* The indicator props of the radio item.
|
||||||
|
* You can use this prop to customize the indicator props.
|
||||||
|
*/
|
||||||
|
indicatorProps?: RadioGroupIndicatorProps;
|
||||||
|
/**
|
||||||
|
* The id of the radio item.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
/**
|
||||||
|
* The label of the radio item.
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RadioItem component is used to render a radio item.
|
||||||
|
*/
|
||||||
|
export const RadioItem = ({
|
||||||
|
className,
|
||||||
|
wrapperProps,
|
||||||
|
labelProps,
|
||||||
|
indicatorProps,
|
||||||
|
label,
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
}: RadioItemProps) => {
|
||||||
|
const { wrapper, label: labelClass, radio, indicator } = radioTheme();
|
||||||
|
|
||||||
|
// Generate a unique id for the radio item from the label if the id is not provided
|
||||||
|
const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const componentId = id ?? kebabCaseLabel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={wrapper({ className: wrapperProps?.className })}>
|
||||||
|
<RadixRadio {...props} className={radio({ className })} id={componentId}>
|
||||||
|
<RadixIndicator
|
||||||
|
forceMount
|
||||||
|
{...indicatorProps}
|
||||||
|
className={indicator({ className: indicatorProps?.className })}
|
||||||
|
/>
|
||||||
|
</RadixRadio>
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
{...labelProps}
|
||||||
|
className={labelClass({ className: labelProps?.className })}
|
||||||
|
htmlFor={componentId}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
2
packages/frontend/src/components/shared/Radio/index.ts
Normal file
2
packages/frontend/src/components/shared/Radio/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Radio';
|
||||||
|
export * from './RadioItem';
|
@ -0,0 +1,84 @@
|
|||||||
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const switchTheme = tv({
|
||||||
|
slots: {
|
||||||
|
wrapper: ['flex', 'items-start', 'gap-4', 'w-[375px]'],
|
||||||
|
switch: [
|
||||||
|
'h-6',
|
||||||
|
'w-12',
|
||||||
|
'rounded-full',
|
||||||
|
'transition-all',
|
||||||
|
'duration-500',
|
||||||
|
'relative',
|
||||||
|
'cursor-default',
|
||||||
|
'shadow-inset',
|
||||||
|
'focus-ring',
|
||||||
|
'outline-none',
|
||||||
|
],
|
||||||
|
thumb: [
|
||||||
|
'block',
|
||||||
|
'h-4',
|
||||||
|
'w-4',
|
||||||
|
'translate-x-1',
|
||||||
|
'transition-transform',
|
||||||
|
'duration-100',
|
||||||
|
'will-change-transform',
|
||||||
|
'rounded-full',
|
||||||
|
'shadow-button',
|
||||||
|
'data-[state=checked]:translate-x-7',
|
||||||
|
'bg-controls-elevated',
|
||||||
|
],
|
||||||
|
label: [
|
||||||
|
'flex',
|
||||||
|
'flex-1',
|
||||||
|
'flex-col',
|
||||||
|
'px-1',
|
||||||
|
'gap-1',
|
||||||
|
'text-sm',
|
||||||
|
'text-elements-high-em',
|
||||||
|
'tracking-[-0.006em]',
|
||||||
|
],
|
||||||
|
description: ['text-xs', 'text-elements-low-em'],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
checked: {
|
||||||
|
true: {
|
||||||
|
switch: [
|
||||||
|
'bg-controls-primary',
|
||||||
|
'hover:bg-controls-primary-hovered',
|
||||||
|
'focus-visible:bg-controls-primary-hovered',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
switch: [
|
||||||
|
'bg-controls-inset',
|
||||||
|
'hover:bg-controls-inset-hovered',
|
||||||
|
'focus-visible:bg-controls-inset-hovered',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
true: {
|
||||||
|
switch: ['bg-controls-disabled', 'cursor-not-allowed'],
|
||||||
|
thumb: ['bg-elements-on-disabled'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
true: {
|
||||||
|
wrapper: ['w-full', 'justify-between'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
checked: true,
|
||||||
|
disabled: true,
|
||||||
|
class: {
|
||||||
|
switch: ['bg-controls-disabled-active'],
|
||||||
|
thumb: ['bg-snowball-900'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SwitchVariants = VariantProps<typeof switchTheme>;
|
85
packages/frontend/src/components/shared/Switch/Switch.tsx
Normal file
85
packages/frontend/src/components/shared/Switch/Switch.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React, { type ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { type SwitchProps as SwitchRadixProps } from '@radix-ui/react-switch';
|
||||||
|
import * as SwitchRadix from '@radix-ui/react-switch';
|
||||||
|
|
||||||
|
import { switchTheme, type SwitchVariants } from './Switch.theme';
|
||||||
|
|
||||||
|
interface SwitchProps
|
||||||
|
extends Omit<SwitchRadixProps, 'checked'>,
|
||||||
|
SwitchVariants {
|
||||||
|
/**
|
||||||
|
* The label of the switch.
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
/**
|
||||||
|
* The description of the switch.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Custom wrapper props for the switch.
|
||||||
|
*/
|
||||||
|
wrapperProps?: ComponentPropsWithoutRef<'div'>;
|
||||||
|
/**
|
||||||
|
* Function that is called when the checked state of the switch changes.
|
||||||
|
* @param checked The new checked state of the switch.
|
||||||
|
*/
|
||||||
|
onCheckedChange?(checked: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A switch is a component used for toggling between two states.
|
||||||
|
*/
|
||||||
|
export const Switch = ({
|
||||||
|
className,
|
||||||
|
checked,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
disabled,
|
||||||
|
name,
|
||||||
|
wrapperProps,
|
||||||
|
fullWidth,
|
||||||
|
...props
|
||||||
|
}: SwitchProps) => {
|
||||||
|
const {
|
||||||
|
wrapper,
|
||||||
|
switch: switchClass,
|
||||||
|
thumb,
|
||||||
|
label: labelClass,
|
||||||
|
description: descriptionClass,
|
||||||
|
} = switchTheme({
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
fullWidth,
|
||||||
|
});
|
||||||
|
|
||||||
|
const switchComponent = (
|
||||||
|
<SwitchRadix.Root
|
||||||
|
{...props}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
className={switchClass({ className })}
|
||||||
|
>
|
||||||
|
<SwitchRadix.Thumb className={thumb()} />
|
||||||
|
</SwitchRadix.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a label is provided, wrap the switch in a label element.
|
||||||
|
if (label) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...wrapperProps}
|
||||||
|
className={wrapper({ className: wrapperProps?.className })}
|
||||||
|
>
|
||||||
|
<label className={labelClass()} htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
{description && (
|
||||||
|
<span className={descriptionClass()}>{description}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
{switchComponent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return switchComponent;
|
||||||
|
};
|
1
packages/frontend/src/components/shared/Switch/index.ts
Normal file
1
packages/frontend/src/components/shared/Switch/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Switch';
|
@ -9,6 +9,8 @@ export const tabsTheme = tv({
|
|||||||
// Horizontal – default
|
// Horizontal – default
|
||||||
'px-1',
|
'px-1',
|
||||||
'pb-5',
|
'pb-5',
|
||||||
|
'cursor-default',
|
||||||
|
'select-none',
|
||||||
'text-elements-low-em',
|
'text-elements-low-em',
|
||||||
'border-b-2',
|
'border-b-2',
|
||||||
'border-transparent',
|
'border-transparent',
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const tooltipTheme = tv({
|
||||||
|
slots: {
|
||||||
|
content: [
|
||||||
|
'z-tooltip',
|
||||||
|
'rounded-md',
|
||||||
|
'bg-surface-high-contrast',
|
||||||
|
'p-2',
|
||||||
|
'text-elements-on-high-contrast',
|
||||||
|
],
|
||||||
|
arrow: ['fill-surface-high-contrast'],
|
||||||
|
},
|
||||||
|
});
|
47
packages/frontend/src/components/shared/Tooltip/Tooltip.tsx
Normal file
47
packages/frontend/src/components/shared/Tooltip/Tooltip.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type {
|
||||||
|
TooltipContentProps,
|
||||||
|
TooltipTriggerProps,
|
||||||
|
} from '@radix-ui/react-tooltip';
|
||||||
|
import { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
|
import { TooltipBase, type TooltipBaseProps } from './TooltipBase';
|
||||||
|
|
||||||
|
export interface TooltipProps extends TooltipBaseProps {
|
||||||
|
triggerProps?: TooltipTriggerProps;
|
||||||
|
contentProps?: TooltipContentProps;
|
||||||
|
content?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/radix-ui/primitives/issues/955#issuecomment-1798201143
|
||||||
|
// Wrap on top of Tooltip base to make tooltip open on mobile via click
|
||||||
|
export const Tooltip = ({
|
||||||
|
children,
|
||||||
|
triggerProps,
|
||||||
|
contentProps,
|
||||||
|
content,
|
||||||
|
...props
|
||||||
|
}: TooltipProps) => {
|
||||||
|
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipBase
|
||||||
|
open={isTooltipVisible}
|
||||||
|
onOpenChange={setIsTooltipVisible}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<TooltipBase.Trigger
|
||||||
|
asChild
|
||||||
|
onBlur={() => setIsTooltipVisible(false)}
|
||||||
|
onClick={() => setIsTooltipVisible((prevOpen) => !prevOpen)}
|
||||||
|
onFocus={() => setTimeout(() => setIsTooltipVisible(true), 0)}
|
||||||
|
{...triggerProps}
|
||||||
|
>
|
||||||
|
{triggerProps?.children ?? children}
|
||||||
|
</TooltipBase.Trigger>
|
||||||
|
<TooltipBase.Content {...contentProps}>
|
||||||
|
{content ?? contentProps?.children ?? 'Coming soon'}
|
||||||
|
</TooltipBase.Content>
|
||||||
|
</TooltipBase>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Provider,
|
||||||
|
TooltipProps as RadixTooltipProps,
|
||||||
|
Root,
|
||||||
|
type TooltipProviderProps,
|
||||||
|
} from '@radix-ui/react-tooltip';
|
||||||
|
import { useState, type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { TooltipContent } from './TooltipContent';
|
||||||
|
import { TooltipTrigger } from './TooltipTrigger';
|
||||||
|
|
||||||
|
export interface TooltipBaseProps extends RadixTooltipProps {
|
||||||
|
providerProps?: TooltipProviderProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TooltipBase = ({
|
||||||
|
children,
|
||||||
|
providerProps,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<TooltipBaseProps>) => {
|
||||||
|
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider {...providerProps}>
|
||||||
|
<Root
|
||||||
|
open={isTooltipVisible}
|
||||||
|
onOpenChange={setIsTooltipVisible}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Root>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TooltipBase.Trigger = TooltipTrigger;
|
||||||
|
TooltipBase.Content = TooltipContent;
|
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Arrow,
|
||||||
|
Content,
|
||||||
|
Portal,
|
||||||
|
type TooltipArrowProps,
|
||||||
|
type TooltipContentProps,
|
||||||
|
} from '@radix-ui/react-tooltip';
|
||||||
|
|
||||||
|
import { tooltipTheme } from '../Tooltip.theme';
|
||||||
|
|
||||||
|
export interface ContentProps extends TooltipContentProps {
|
||||||
|
hasArrow?: boolean;
|
||||||
|
arrowProps?: TooltipArrowProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TooltipContent = ({
|
||||||
|
children,
|
||||||
|
arrowProps,
|
||||||
|
className,
|
||||||
|
hasArrow = true,
|
||||||
|
...props
|
||||||
|
}: ContentProps) => {
|
||||||
|
const { content, arrow } = tooltipTheme();
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Content
|
||||||
|
className={content({
|
||||||
|
className,
|
||||||
|
})}
|
||||||
|
sideOffset={5}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{hasArrow && (
|
||||||
|
<Arrow
|
||||||
|
className={arrow({
|
||||||
|
className: arrowProps?.className,
|
||||||
|
})}
|
||||||
|
{...arrowProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Content>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './TooltipContent';
|
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Trigger, type TooltipTriggerProps } from '@radix-ui/react-tooltip';
|
||||||
|
|
||||||
|
export type TriggerProps = TooltipTriggerProps;
|
||||||
|
|
||||||
|
export const TooltipTrigger = ({ children, ...props }: TriggerProps) => {
|
||||||
|
return (
|
||||||
|
<Trigger asChild {...props}>
|
||||||
|
{children}
|
||||||
|
</Trigger>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './TooltipTrigger';
|
2
packages/frontend/src/components/shared/Tooltip/index.ts
Normal file
2
packages/frontend/src/components/shared/Tooltip/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Tooltip';
|
||||||
|
export * from './TooltipBase';
|
@ -10,6 +10,7 @@ import { renderBadges } from './renders/badge';
|
|||||||
import {
|
import {
|
||||||
renderButtonIcons,
|
renderButtonIcons,
|
||||||
renderButtons,
|
renderButtons,
|
||||||
|
renderDisabledButtons,
|
||||||
renderLinks,
|
renderLinks,
|
||||||
} from './renders/button';
|
} from './renders/button';
|
||||||
import {
|
import {
|
||||||
@ -17,16 +18,22 @@ import {
|
|||||||
renderTabs,
|
renderTabs,
|
||||||
renderVerticalTabs,
|
renderVerticalTabs,
|
||||||
} from './renders/tabs';
|
} from './renders/tabs';
|
||||||
|
import { Switch } from 'components/shared/Switch';
|
||||||
|
import { RADIO_OPTIONS } from './renders/radio';
|
||||||
|
import { Radio } from 'components/shared/Radio';
|
||||||
import {
|
import {
|
||||||
renderInlineNotificationWithDescriptions,
|
renderInlineNotificationWithDescriptions,
|
||||||
renderInlineNotifications,
|
renderInlineNotifications,
|
||||||
} from './renders/inlineNotifications';
|
} from './renders/inlineNotifications';
|
||||||
import { renderInputs } from './renders/input';
|
import { renderInputs } from './renders/input';
|
||||||
import { renderToast, renderToastsWithCta } from './renders/toast';
|
import { renderToast, renderToastsWithCta } from './renders/toast';
|
||||||
|
import { renderTooltips } from './renders/tooltip';
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const [singleDate, setSingleDate] = useState<Value>();
|
const [singleDate, setSingleDate] = useState<Value>();
|
||||||
const [dateRange, setDateRange] = useState<Value>();
|
const [dateRange, setDateRange] = useState<Value>();
|
||||||
|
const [switchValue, setSwitchValue] = useState(false);
|
||||||
|
const [selectedRadio, setSelectedRadio] = useState<string>('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full min-h-full">
|
<div className="relative h-full min-h-full">
|
||||||
@ -52,17 +59,47 @@ const Page = () => {
|
|||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<div className="flex flex-col gap-10 items-center justify-between">
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Tooltip</h1>
|
||||||
|
<div className="flex w-full flex-wrap max-w-[680px] justify-center gap-10">
|
||||||
|
{renderTooltips()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
<h1 className="text-2xl font-bold">Input</h1>
|
<h1 className="text-2xl font-bold">Input</h1>
|
||||||
<div className="flex w-full flex-col gap-10">{renderInputs()}</div>
|
<div className="flex w-full flex-col gap-10">{renderInputs()}</div>
|
||||||
|
|
||||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
|
{/* Button */}
|
||||||
<h1 className="text-2xl font-bold">Button</h1>
|
<h1 className="text-2xl font-bold">Button</h1>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
{renderButtons()}
|
{renderButtons()}
|
||||||
{renderButtonIcons()}
|
{renderButtonIcons()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Link */}
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Link</h1>
|
||||||
|
<div className="flex gap-4 items-center justify-center">
|
||||||
|
{renderLinks()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Disabled button, icon only, and link */}
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-2xl font-bold text-center">Disabled</h1>
|
||||||
|
<p className="text-lg text-center text-gray-500">
|
||||||
|
Button – icon only – link
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-10 items-center justify-center">
|
||||||
|
{renderDisabledButtons()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
{/* Badge */}
|
{/* Badge */}
|
||||||
@ -139,6 +176,49 @@ const Page = () => {
|
|||||||
|
|
||||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
|
{/* Switch */}
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Switch</h1>
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-center">
|
||||||
|
<Switch
|
||||||
|
label="Label"
|
||||||
|
checked={switchValue}
|
||||||
|
onCheckedChange={setSwitchValue}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label="Label"
|
||||||
|
description="Additional information or context"
|
||||||
|
checked={switchValue}
|
||||||
|
onCheckedChange={setSwitchValue}
|
||||||
|
/>
|
||||||
|
<Switch disabled label="Disabled unchecked" />
|
||||||
|
<Switch disabled checked label="Disabled checked" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
|
{/* Radio */}
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Radio</h1>
|
||||||
|
<div className="flex gap-20 items-start">
|
||||||
|
<Radio
|
||||||
|
options={RADIO_OPTIONS}
|
||||||
|
orientation="vertical"
|
||||||
|
value={selectedRadio}
|
||||||
|
onValueChange={setSelectedRadio}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
options={RADIO_OPTIONS}
|
||||||
|
orientation="horizontal"
|
||||||
|
value={selectedRadio}
|
||||||
|
onValueChange={setSelectedRadio}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
{/* Inline notification */}
|
{/* Inline notification */}
|
||||||
<div className="flex flex-col gap-10 items-center justify-between">
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
<h1 className="text-2xl font-bold">Inline Notification</h1>
|
<h1 className="text-2xl font-bold">Inline Notification</h1>
|
||||||
@ -149,16 +229,6 @@ const Page = () => {
|
|||||||
{renderInlineNotificationWithDescriptions()}
|
{renderInlineNotificationWithDescriptions()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
|
||||||
|
|
||||||
{/* Link */}
|
|
||||||
<div className="flex flex-col gap-10 items-center justify-between">
|
|
||||||
<h1 className="text-2xl font-bold">Link</h1>
|
|
||||||
<div className="flex gap-4 items-center justify-center">
|
|
||||||
{renderLinks()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,44 +2,45 @@ import { Button, ButtonTheme } from 'components/shared/Button';
|
|||||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
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 = () => {
|
export const renderButtons = () => {
|
||||||
return ['primary', 'secondary', 'tertiary', 'danger'].map(
|
return buttonVariants.map((variant, index) => (
|
||||||
(variant, index) => (
|
<div className="flex gap-5 flex-wrap" key={index}>
|
||||||
<div className="flex gap-5 flex-wrap" key={index}>
|
{buttonSizes.map((size) => (
|
||||||
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
|
<Button
|
||||||
<Button
|
leftIcon={<PlusIcon />}
|
||||||
leftIcon={<PlusIcon />}
|
rightIcon={<PlusIcon />}
|
||||||
rightIcon={<PlusIcon />}
|
variant={variant as ButtonTheme['variant']}
|
||||||
variant={variant as ButtonTheme['variant']}
|
size={size as ButtonTheme['size']}
|
||||||
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'}
|
key={`${variant}-${size}`}
|
||||||
key={`${variant}-${size}`}
|
>
|
||||||
disabled={size === 'disabled'}
|
Button
|
||||||
>
|
</Button>
|
||||||
Button
|
))}
|
||||||
</Button>
|
</div>
|
||||||
))}
|
));
|
||||||
</div>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderButtonIcons = () => {
|
export const renderButtonIcons = () => {
|
||||||
return [
|
return iconOnlyVariants.map((variant, index) => (
|
||||||
'primary',
|
|
||||||
'secondary',
|
|
||||||
'tertiary',
|
|
||||||
'ghost',
|
|
||||||
'danger',
|
|
||||||
'danger-ghost',
|
|
||||||
].map((variant, index) => (
|
|
||||||
<div className="flex gap-5 flex-wrap" key={index}>
|
<div className="flex gap-5 flex-wrap" key={index}>
|
||||||
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
|
{buttonSizes.map((size) => (
|
||||||
<Button
|
<Button
|
||||||
iconOnly
|
iconOnly
|
||||||
variant={variant as ButtonTheme['variant']}
|
variant={variant as ButtonTheme['variant']}
|
||||||
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'}
|
size={size as ButtonTheme['size']}
|
||||||
key={`${variant}-${size}`}
|
key={`${variant}-${size}`}
|
||||||
disabled={size === 'disabled'}
|
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@ -49,17 +50,38 @@ export const renderButtonIcons = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const renderLinks = () => {
|
export const renderLinks = () => {
|
||||||
return ['link', 'link-emphasized', 'disabled'].map((variant) => (
|
return linkVariants.map((variant) => (
|
||||||
<Button
|
<Button
|
||||||
variant={
|
variant={variant}
|
||||||
variant !== 'disabled' ? (variant as ButtonTheme['variant']) : 'link'
|
|
||||||
}
|
|
||||||
key={variant}
|
key={variant}
|
||||||
leftIcon={<PlusIcon />}
|
leftIcon={<PlusIcon />}
|
||||||
rightIcon={<PlusIcon />}
|
rightIcon={<PlusIcon />}
|
||||||
disabled={variant === 'disabled'}
|
|
||||||
>
|
>
|
||||||
Link
|
Link
|
||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const renderDisabledButtons = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Button variants */}
|
||||||
|
<Button leftIcon={<PlusIcon />} rightIcon={<PlusIcon />} disabled>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
{/* Icon only variants */}
|
||||||
|
<Button iconOnly disabled>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
{/* Link variantws */}
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
disabled
|
||||||
|
leftIcon={<PlusIcon />}
|
||||||
|
rightIcon={<PlusIcon />}
|
||||||
|
>
|
||||||
|
Link
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
16
packages/frontend/src/pages/components/renders/radio.ts
Normal file
16
packages/frontend/src/pages/components/renders/radio.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { RadioOption } from 'components/shared/Radio';
|
||||||
|
|
||||||
|
export const RADIO_OPTIONS: RadioOption[] = [
|
||||||
|
{
|
||||||
|
label: 'Label 1',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Label 2',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Label 3',
|
||||||
|
value: '3',
|
||||||
|
},
|
||||||
|
];
|
30
packages/frontend/src/pages/components/renders/tooltip.tsx
Normal file
30
packages/frontend/src/pages/components/renders/tooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { Tooltip } from 'components/shared/Tooltip';
|
||||||
|
import { ContentProps } from 'components/shared/Tooltip/TooltipContent';
|
||||||
|
|
||||||
|
const alignments: ContentProps['align'][] = ['start', 'center', 'end'];
|
||||||
|
const sides: ContentProps['side'][] = ['left', 'top', 'bottom', 'right'];
|
||||||
|
|
||||||
|
export const renderTooltips = () => {
|
||||||
|
const tooltips = sides.map((side) => {
|
||||||
|
return alignments.map((align) => {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={`${side}-${align}`}
|
||||||
|
content="tooltip content"
|
||||||
|
contentProps={{ align, side }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
key={`${side}-${align}`}
|
||||||
|
className="h-16 self-center"
|
||||||
|
>
|
||||||
|
Tooltip ({side} - {align})
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return tooltips;
|
||||||
|
};
|
@ -16,6 +16,7 @@ const DEFAULT_FILTER_VALUE: FilterValue = {
|
|||||||
searchedBranch: '',
|
searchedBranch: '',
|
||||||
status: StatusOptions.ALL_STATUS,
|
status: StatusOptions.ALL_STATUS,
|
||||||
};
|
};
|
||||||
|
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
|
||||||
|
|
||||||
const DeploymentsTabPanel = () => {
|
const DeploymentsTabPanel = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
@ -26,22 +27,30 @@ const DeploymentsTabPanel = () => {
|
|||||||
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
||||||
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
||||||
|
|
||||||
const fetchDeployments = async () => {
|
const fetchDeployments = useCallback(async () => {
|
||||||
const { deployments } = await client.getDeployments(project.id);
|
const { deployments } = await client.getDeployments(project.id);
|
||||||
setDeployments(deployments);
|
setDeployments(deployments);
|
||||||
};
|
}, [client, project]);
|
||||||
|
|
||||||
const fetchProductionBranchDomains = useCallback(async () => {
|
const fetchProductionBranchDomains = useCallback(async () => {
|
||||||
const { domains } = await client.getDomains(project.id, {
|
const { domains } = await client.getDomains(project.id, {
|
||||||
branch: project.prodBranch,
|
branch: project.prodBranch,
|
||||||
});
|
});
|
||||||
setProdBranchDomains(domains);
|
setProdBranchDomains(domains);
|
||||||
}, []);
|
}, [client, project]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDeployments();
|
|
||||||
fetchProductionBranchDomains();
|
fetchProductionBranchDomains();
|
||||||
}, []);
|
fetchDeployments();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchDeployments();
|
||||||
|
}, FETCH_DEPLOYMENTS_INTERVAL);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [fetchDeployments, fetchProductionBranchDomains]);
|
||||||
|
|
||||||
const currentDeployment = useMemo(() => {
|
const currentDeployment = useMemo(() => {
|
||||||
return deployments.find((deployment) => {
|
return deployments.find((deployment) => {
|
||||||
|
@ -8,10 +8,13 @@ export default withMT({
|
|||||||
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
|
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
|
||||||
sans: ['Inter', 'sans-serif'],
|
|
||||||
},
|
|
||||||
extend: {
|
extend: {
|
||||||
|
zIndex: {
|
||||||
|
tooltip: '52',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
'2xs': '0.625rem',
|
'2xs': '0.625rem',
|
||||||
'3xs': '0.5rem',
|
'3xs': '0.5rem',
|
||||||
@ -151,6 +154,7 @@ export default withMT({
|
|||||||
calendar:
|
calendar:
|
||||||
'0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)',
|
'0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)',
|
||||||
field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
|
field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
|
||||||
|
inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)',
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
2.5: '0.625rem',
|
2.5: '0.625rem',
|
||||||
|
115
yarn.lock
115
yarn.lock
@ -2032,6 +2032,13 @@
|
|||||||
link-module-alias "^1.2.0"
|
link-module-alias "^1.2.0"
|
||||||
shx "^0.3.4"
|
shx "^0.3.4"
|
||||||
|
|
||||||
|
"@floating-ui/core@^1.0.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
|
||||||
|
integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/utils" "^0.2.1"
|
||||||
|
|
||||||
"@floating-ui/core@^1.4.2":
|
"@floating-ui/core@^1.4.2":
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071"
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071"
|
||||||
@ -2047,6 +2054,14 @@
|
|||||||
"@floating-ui/core" "^1.4.2"
|
"@floating-ui/core" "^1.4.2"
|
||||||
"@floating-ui/utils" "^0.1.3"
|
"@floating-ui/utils" "^0.1.3"
|
||||||
|
|
||||||
|
"@floating-ui/dom@^1.6.1":
|
||||||
|
version "1.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
|
||||||
|
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core" "^1.0.0"
|
||||||
|
"@floating-ui/utils" "^0.2.0"
|
||||||
|
|
||||||
"@floating-ui/react-dom@^1.2.2":
|
"@floating-ui/react-dom@^1.2.2":
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3"
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3"
|
||||||
@ -2054,6 +2069,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/dom" "^1.2.1"
|
"@floating-ui/dom" "^1.2.1"
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@^2.0.0":
|
||||||
|
version "2.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d"
|
||||||
|
integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom" "^1.6.1"
|
||||||
|
|
||||||
"@floating-ui/react@0.19.0":
|
"@floating-ui/react@0.19.0":
|
||||||
version "0.19.0"
|
version "0.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.0.tgz#d8e19a3fcfaa0684d5ec3f335232b4e0ac0c87e1"
|
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.0.tgz#d8e19a3fcfaa0684d5ec3f335232b4e0ac0c87e1"
|
||||||
@ -2068,6 +2090,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
|
||||||
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
|
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
|
||||||
|
|
||||||
|
"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||||
|
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||||
|
|
||||||
"@fontsource/inter@^5.0.16":
|
"@fontsource/inter@^5.0.16":
|
||||||
version "5.0.16"
|
version "5.0.16"
|
||||||
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50"
|
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50"
|
||||||
@ -3284,6 +3311,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow@1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d"
|
||||||
|
integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/react-avatar@^1.0.4":
|
"@radix-ui/react-avatar@^1.0.4":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
|
||||||
@ -3362,6 +3397,23 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-popper@1.1.3":
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
|
||||||
|
integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@floating-ui/react-dom" "^2.0.0"
|
||||||
|
"@radix-ui/react-arrow" "1.0.3"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||||
|
"@radix-ui/react-use-rect" "1.0.1"
|
||||||
|
"@radix-ui/react-use-size" "1.0.1"
|
||||||
|
"@radix-ui/rect" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/react-portal@1.0.4":
|
"@radix-ui/react-portal@1.0.4":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
|
||||||
@ -3387,6 +3439,23 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-slot" "1.0.2"
|
"@radix-ui/react-slot" "1.0.2"
|
||||||
|
|
||||||
|
"@radix-ui/react-radio-group@^1.1.3":
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3"
|
||||||
|
integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-direction" "1.0.1"
|
||||||
|
"@radix-ui/react-presence" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-roving-focus" "1.0.4"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
"@radix-ui/react-use-previous" "1.0.1"
|
||||||
|
"@radix-ui/react-use-size" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus@1.0.4":
|
"@radix-ui/react-roving-focus@1.0.4":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
|
||||||
@ -3411,6 +3480,20 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-compose-refs" "1.0.1"
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-switch@^1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e"
|
||||||
|
integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
"@radix-ui/react-use-previous" "1.0.1"
|
||||||
|
"@radix-ui/react-use-size" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/react-tabs@^1.0.4":
|
"@radix-ui/react-tabs@^1.0.4":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"
|
||||||
@ -3443,6 +3526,23 @@
|
|||||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||||
|
"@radix-ui/react-tooltip@^1.0.7":
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e"
|
||||||
|
integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||||
|
"@radix-ui/react-id" "1.0.1"
|
||||||
|
"@radix-ui/react-popper" "1.1.3"
|
||||||
|
"@radix-ui/react-portal" "1.0.4"
|
||||||
|
"@radix-ui/react-presence" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-slot" "1.0.2"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
"@radix-ui/react-visually-hidden" "1.0.3"
|
"@radix-ui/react-visually-hidden" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref@1.0.1":
|
"@radix-ui/react-use-callback-ref@1.0.1":
|
||||||
@ -3482,6 +3582,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-use-rect@1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"
|
||||||
|
integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/rect" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/react-use-size@1.0.1":
|
"@radix-ui/react-use-size@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
|
||||||
@ -3498,6 +3606,13 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-primitive" "1.0.3"
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
|
||||||
|
"@radix-ui/rect@1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
|
||||||
|
integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@remix-run/router@1.13.1":
|
"@remix-run/router@1.13.1":
|
||||||
version "1.13.1"
|
version "1.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e"
|
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e"
|
||||||
|
Loading…
Reference in New Issue
Block a user