forked from cerc-io/snowballtools-base
Compare commits
162
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd4a51c830 | ||
|
|
4a87c100d6 | ||
|
|
3493d735b9 | ||
|
|
a324d32ebf | ||
|
|
4519494be8 | ||
|
|
319822c8e6 | ||
|
|
c5273ff530 | ||
|
|
2585418ed6 | ||
|
|
89306ddcc7 | ||
|
|
3b67396c43 | ||
|
|
3535bd3a58 | ||
|
|
282001c317 | ||
|
|
2f62d086de | ||
|
|
4c544e8d15 | ||
|
|
a02ee13f9d | ||
|
|
e32a51a771 | ||
|
|
513ca69d01 | ||
|
|
e34b8d6a2e | ||
|
|
f4d5b77784 | ||
|
|
399b80ce3b | ||
|
|
fdaec2da94 | ||
|
|
13fc92bf0e | ||
|
|
8a8ee1d6ae | ||
|
|
5680961964 | ||
|
|
aa0ca8dd21 | ||
|
|
8280f4c7f7 | ||
|
|
ab72bbb03f | ||
|
|
cc4e5fd627 | ||
|
|
195471b888 | ||
|
|
c731dd308c | ||
|
|
8ce531274d | ||
|
|
6c38094093 | ||
|
|
95285d30dd | ||
|
|
61c8846400 | ||
|
|
d73c3c7daf | ||
|
|
effa0bbf14 | ||
|
|
10a875d6e8 | ||
|
|
284eb1403a | ||
|
|
538e89e3c9 | ||
|
|
b88752da69 | ||
|
|
48c569c371 | ||
|
|
549dc5af10 | ||
|
|
012beaad57 | ||
|
|
81f27442fb | ||
|
|
bdb948f69e | ||
|
|
2442e92395 | ||
|
|
dcf271a01b | ||
|
|
48fd953c60 | ||
|
|
5eff273abb | ||
|
|
f9af066333 | ||
|
|
04460a24ac | ||
|
|
ccc4589033 | ||
|
|
cf2f9a88e1 | ||
|
|
50749055bf | ||
|
|
ef7c673698 | ||
|
|
7f11d8818d | ||
|
|
ece9a2ecec | ||
|
|
cb614c8d8f | ||
|
|
dbb928cd2f | ||
|
|
cfd129fb96 | ||
|
|
efc10d3d7e | ||
|
|
042aff2f87 | ||
|
|
b48afed24f | ||
|
|
0dfecd024d | ||
|
|
9db87e6be3 | ||
|
|
5a5756bcf2 | ||
|
|
22233d95f8 | ||
|
|
12fba5ee0c | ||
|
|
9b6c777f5f | ||
|
|
3718e260f5 | ||
|
|
05e1ac6b02 | ||
|
|
55848b1dd1 | ||
|
|
42bf1534fb | ||
|
|
7d1810ebd9 | ||
|
|
d2ca4df35a | ||
|
|
30bbe4d766 | ||
|
|
0e7343da6f | ||
|
|
8952393b31 | ||
|
|
32918b7930 | ||
|
|
596889b5e2 | ||
|
|
fb932eeb04 | ||
|
|
200ea3ca7b | ||
|
|
ba87c6f22a | ||
|
|
4d646e1ac0 | ||
|
|
5be29a9745 | ||
|
|
fffc370d40 | ||
|
|
1cd60d84dd | ||
|
|
c8a153ad27 | ||
|
|
bb7a88f7e9 | ||
|
|
675112079c | ||
|
|
cb928c3360 | ||
|
|
5836779403 | ||
|
|
028dd35db1 | ||
|
|
ef9f4df28a | ||
|
|
496404195c | ||
|
|
9dec48d67c | ||
|
|
267b52a352 | ||
|
|
fa96b6e734 | ||
|
|
22c581dd33 | ||
|
|
d3013719e6 | ||
|
|
98a4d7be07 | ||
|
|
3928e2610f | ||
|
|
075cc05db5 | ||
|
|
6df094bf2e | ||
|
|
2369f4498a | ||
|
|
eb6a727425 | ||
|
|
da2f7ede42 | ||
|
|
448d0ceb7c | ||
|
|
195d957251 | ||
|
|
807a4b1197 | ||
|
|
5f6723fad6 | ||
|
|
b663067035 | ||
|
|
b3a99475af | ||
|
|
ea44efa0f2 | ||
|
|
7d5963d776 | ||
|
|
33b6191539 | ||
|
|
636f68d7a4 | ||
|
|
237e9e5cb9 | ||
|
|
e70bb34190 | ||
|
|
b5eef95d15 | ||
|
|
f0121605c4 | ||
|
|
c8f1c58507 | ||
|
|
83508cd81d | ||
|
|
76c59e2c58 | ||
|
|
a5bce0cda2 | ||
|
|
3a085a79ae | ||
|
|
abfe6a523c | ||
|
|
f764bea6d1 | ||
|
|
cc9c24cca1 | ||
|
|
e482f998a1 | ||
|
|
b255078431 | ||
|
|
26260976fb | ||
|
|
5e3a6ad2b5 | ||
|
|
f31ee7349f | ||
|
|
5c5d759c10 | ||
|
|
dc6bf3794a | ||
|
|
af0d28e654 | ||
|
|
4f7f9cf914 | ||
|
|
ad7dd1920a | ||
|
|
cc97ddff9d | ||
|
|
0a5a1b8c55 | ||
|
|
4f2944a974 | ||
|
|
69198307fb | ||
|
|
0f7c6c73c9 | ||
|
|
2ac657c32e | ||
|
|
943d427db1 | ||
|
|
7e6b74208d | ||
|
|
2b78cab849 | ||
|
|
862862a9c5 | ||
|
|
6a3af491ca | ||
|
|
92f7d07f3f | ||
|
|
6fb94aeb11 | ||
|
|
1c848c5a89 | ||
|
|
17b5ff9a74 | ||
|
|
56adfc4806 | ||
|
|
b51d050ee6 | ||
|
|
8021ff7ff9 | ||
|
|
e850435c2d | ||
|
|
8a43698c43 | ||
|
|
499dba2c5e | ||
|
|
d498ba7e9a | ||
|
|
082353e9bc |
@@ -1 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
yarn-error.log
|
||||||
|
.yarnrc.yml
|
||||||
|
.yarn/
|
||||||
|
.yarnrc
|
||||||
|
|
||||||
|
packages/backend/environments/local.toml
|
||||||
|
|||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
// IntelliSense for taiwind variants
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -28,6 +28,12 @@
|
|||||||
cd packages/backend
|
cd packages/backend
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Rename backend config file from [environments/local.toml.example](packages/backend/environments/local.toml.example) to `local.toml`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv environments/local.toml.example environments/local.toml
|
||||||
|
```
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||||
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||||
- In "Homepage URL", type `http://localhost:3000`
|
- In "Homepage URL", type `http://localhost:3000`
|
||||||
@@ -181,17 +187,24 @@
|
|||||||
cd packages/frontend
|
cd packages/frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Rename [.env.example](packages/frontend/.env.example) to `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Set actual owner/name of the template repo that will be used for creating new repo
|
# Set actual owner/name of the template repo that will be used for creating new repo
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||||
|
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = cerc-io/image-upload-pwa-example
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend Production
|
### Frontend Production
|
||||||
|
|||||||
+10
-1
@@ -9,10 +9,19 @@ if [[ -d "$DEST_DIR" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$PKG_DIR/.env" ]]; then
|
||||||
|
echo "Using existing .env file"
|
||||||
|
else
|
||||||
|
mv "$PKG_DIR/.env.example" "$PKG_DIR/.env"
|
||||||
|
echo "Created .env file. Please populate with the correct values."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
cat > $PKG_DIR/.env <<EOF
|
cat > $PKG_DIR/.env <<EOF
|
||||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||||
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
|
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
|
||||||
|
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
|
||||||
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
|
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
"allowArgumentsExplicitlyTypedAsAny": true
|
"allowArgumentsExplicitlyTypedAsAny": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ "ignoreRestSiblings": true }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
db
|
db
|
||||||
dist
|
dist
|
||||||
|
environments/local.toml
|
||||||
@@ -24,7 +24,7 @@ export interface GitHubConfig {
|
|||||||
oAuth: {
|
oAuth: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegistryConfig {
|
export interface RegistryConfig {
|
||||||
@@ -39,7 +39,7 @@ export interface RegistryConfig {
|
|||||||
amount: string;
|
amount: string;
|
||||||
denom: string;
|
denom: string;
|
||||||
gas: string;
|
gas: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MiscConfig {
|
export interface MiscConfig {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
|
||||||
export const DEFAULT_CONFIG_FILE_PATH = process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
export const DEFAULT_CONFIG_FILE_PATH =
|
||||||
|
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||||
|
|
||||||
export const DEFAULT_GQL_PATH = '/graphql';
|
export const DEFAULT_GQL_PATH = '/graphql';
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -74,14 +80,18 @@ export class Database {
|
|||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizations (options: FindManyOptions<Organization>): Promise<Organization[]> {
|
async getOrganizations (
|
||||||
|
options: FindManyOptions<Organization>
|
||||||
|
): Promise<Organization[]> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
const organizations = await organizationRepository.find(options);
|
const organizations = await organizationRepository.find(options);
|
||||||
|
|
||||||
return organizations;
|
return organizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
|
async getOrganization (
|
||||||
|
options: FindOneOptions<Organization>
|
||||||
|
): Promise<Organization | null> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
const organization = await organizationRepository.findOne(options);
|
const organization = await organizationRepository.findOne(options);
|
||||||
|
|
||||||
@@ -123,7 +133,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')
|
||||||
@@ -136,19 +150,29 @@ export class Database {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
|
async getProjectsInOrganization (
|
||||||
|
userId: string,
|
||||||
|
organizationSlug: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
.leftJoinAndSelect(
|
||||||
|
'project.deployments',
|
||||||
|
'deployments',
|
||||||
|
'deployments.isCurrent = true'
|
||||||
|
)
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.leftJoin('project.organization', 'organization')
|
.leftJoin('project.organization', 'organization')
|
||||||
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
|
.where(
|
||||||
|
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
organizationSlug
|
organizationSlug
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
@@ -157,7 +181,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);
|
||||||
|
|
||||||
@@ -182,7 +208,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);
|
||||||
|
|
||||||
@@ -210,8 +238,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: {
|
||||||
@@ -228,8 +259,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: {
|
||||||
@@ -244,9 +279,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;
|
||||||
@@ -255,37 +293,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;
|
||||||
@@ -295,7 +359,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: {
|
||||||
@@ -307,8 +372,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');
|
||||||
@@ -317,34 +381,49 @@ export class Database {
|
|||||||
return projectMemberWithProject[0];
|
return projectMemberWithProject[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
|
async getProjectsBySearchText (
|
||||||
|
userId: string,
|
||||||
|
searchText: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
|
.where(
|
||||||
|
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
searchText: `%${searchText}%`
|
searchText: `%${searchText}%`
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentById (
|
||||||
|
deploymentId: string,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.updateDeployment({ id: deploymentId }, data);
|
return this.updateDeployment({ id: deploymentId }, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeployment (
|
||||||
|
criteria: FindOptionsWhere<Deployment>,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const updateResult = await deploymentRepository.update(criteria, data);
|
const updateResult = await deploymentRepository.update(criteria, data);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentsByProjectIds (
|
||||||
|
projectIds: string[],
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const updateResult = await deploymentRepository
|
const updateResult = await deploymentRepository
|
||||||
@@ -378,9 +457,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);
|
||||||
}
|
}
|
||||||
@@ -427,14 +512,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({
|
||||||
|
|||||||
@@ -27,26 +27,26 @@ export enum DeploymentStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationDeploymentRequest {
|
export interface ApplicationDeploymentRequest {
|
||||||
type: string
|
type: string;
|
||||||
version: string
|
version: string;
|
||||||
name: string
|
name: string;
|
||||||
application: string
|
application: string;
|
||||||
config: string,
|
config: string;
|
||||||
meta: 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()
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ export class Organization {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
@OneToMany(
|
||||||
|
() => UserOrganization,
|
||||||
|
(userOrganization) => userOrganization.organization,
|
||||||
|
{
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,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()
|
||||||
|
|||||||
@@ -39,13 +39,17 @@ export class User {
|
|||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
})
|
||||||
projectMembers!: ProjectMember[];
|
projectMembers!: ProjectMember[];
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
@OneToMany(
|
||||||
|
() => UserOrganization,
|
||||||
|
(UserOrganization) => UserOrganization.member,
|
||||||
|
{
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,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,7 +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 { ApplicationRecord, Deployment, ApplicationDeploymentRequest } from './entity/Deployment';
|
import {
|
||||||
|
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');
|
||||||
@@ -22,7 +26,11 @@ export class Registry {
|
|||||||
|
|
||||||
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 ({
|
||||||
@@ -32,24 +40,38 @@ export class Registry {
|
|||||||
appType,
|
appType,
|
||||||
repoUrl
|
repoUrl
|
||||||
}: {
|
}: {
|
||||||
appName: string,
|
appName: string;
|
||||||
packageJSON: PackageJSON
|
packageJSON: PackageJSON;
|
||||||
commitHash: string,
|
commitHash: string;
|
||||||
appType: string,
|
appType: string;
|
||||||
repoUrl: string
|
repoUrl: string;
|
||||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
}): Promise<{
|
||||||
|
applicationRecordId: string;
|
||||||
|
applicationRecordData: ApplicationRecord;
|
||||||
|
}> {
|
||||||
// Use laconic-sdk to publish record
|
// Use laconic-sdk to publish record
|
||||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||||
// Fetch previous records
|
// Fetch previous records
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords(
|
||||||
|
{
|
||||||
type: APP_RECORD_TYPE,
|
type: APP_RECORD_TYPE,
|
||||||
name: packageJSON.name
|
name: packageJSON.name
|
||||||
}, true);
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Get next version of record
|
// Get next version of record
|
||||||
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
|
const bondRecords = records.filter(
|
||||||
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
(record: any) => record.bondId === this.registryConfig.bondId
|
||||||
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
);
|
||||||
|
const [latestBondRecord] = bondRecords.sort(
|
||||||
|
(a: any, b: any) =>
|
||||||
|
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||||
|
);
|
||||||
|
const nextVersion = semverInc(
|
||||||
|
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||||
|
'patch'
|
||||||
|
);
|
||||||
|
|
||||||
assert(nextVersion, 'Application record version not valid');
|
assert(nextVersion, 'Application record version not valid');
|
||||||
|
|
||||||
@@ -64,7 +86,12 @@ export class Registry {
|
|||||||
...(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 })
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,11 +111,29 @@ export class Registry {
|
|||||||
const crn = this.getCrn(appName);
|
const crn = this.getCrn(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: {
|
||||||
@@ -98,8 +143,8 @@ export class Registry {
|
|||||||
repository: string,
|
repository: string,
|
||||||
environmentVariables: { [key: string]: 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.appName);
|
||||||
const records = await this.registry.resolveNames([crn]);
|
const records = await this.registry.resolveNames([crn]);
|
||||||
@@ -125,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.deployment.commitHash
|
repository_ref: data.deployment.commitHash
|
||||||
})
|
})
|
||||||
@@ -143,21 +190,34 @@ export class Registry {
|
|||||||
log(`Application deployment request record published: ${result.data.id}`);
|
log(`Application deployment request record published: ${result.data.id}`);
|
||||||
log('Application deployment request data:', applicationDeploymentRequest);
|
log('Application deployment request data:', applicationDeploymentRequest);
|
||||||
|
|
||||||
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
|
return {
|
||||||
|
applicationDeploymentRequestId: result.data.id,
|
||||||
|
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords for deployments
|
* Fetch ApplicationDeploymentRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
async getDeploymentRecords (
|
||||||
|
deployments: Deployment[]
|
||||||
|
): Promise<AppDeploymentRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords(
|
||||||
|
{
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||||
}, true);
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ids
|
// Filter records with ApplicationRecord ids
|
||||||
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
|
deployments.some(
|
||||||
|
(deployment) =>
|
||||||
|
deployment.applicationRecordId === record.attributes.application
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCrn (appName: string): string {
|
getCrn (appName: string): string {
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -45,14 +48,24 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.searchProjects(context.user, searchText);
|
return service.searchProjects(context.user, 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.user, projectMemberId);
|
return await service.removeProjectMember(context.user, projectMemberId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -61,12 +74,18 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProjectMember: async (_: any, { projectMemberId, data }: {
|
updateProjectMember: async (
|
||||||
projectMemberId: string,
|
_: any,
|
||||||
|
{
|
||||||
|
projectMemberId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
projectMemberId: string;
|
||||||
data: {
|
data: {
|
||||||
permissions: Permission[]
|
permissions: Permission[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProjectMember(projectMemberId, data);
|
return await service.updateProjectMember(projectMemberId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -75,13 +94,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addProjectMember: async (_: any, { projectId, data }: {
|
addProjectMember: async (
|
||||||
projectId: string,
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
data: {
|
data: {
|
||||||
email: string,
|
email: string;
|
||||||
permissions: Permission[]
|
permissions: Permission[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addProjectMember(projectId, data));
|
return Boolean(await service.addProjectMember(projectId, data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -90,25 +115,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,7 +168,11 @@ 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.user, deploymentId));
|
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -126,7 +181,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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.user, organizationSlug, data);
|
return await service.addProject(context.user, organizationSlug, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -135,7 +197,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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,7 +209,11 @@ 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.user, deploymentId));
|
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -157,7 +226,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 +240,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 +255,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 +267,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 +279,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.user);
|
return await service.authenticateGitHub(code, context.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
+152
-61
@@ -23,8 +23,8 @@ 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 {
|
||||||
@@ -72,7 +72,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);
|
||||||
@@ -92,10 +94,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: {
|
||||||
@@ -104,38 +108,46 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get project IDs of deployments that are in production environment
|
// Get project IDs of deployments that are in production environment
|
||||||
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
|
const productionDeploymentProjectIds = deployments.reduce(
|
||||||
|
(acc, deployment): Set<string> => {
|
||||||
if (deployment.environment === Environment.Production) {
|
if (deployment.environment === Environment.Production) {
|
||||||
acc.add(deployment.projectId);
|
acc.add(deployment.projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, new Set<string>());
|
},
|
||||||
|
new Set<string>()
|
||||||
|
);
|
||||||
|
|
||||||
// Set old deployments isCurrent to false
|
// Set old deployments isCurrent to false
|
||||||
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false });
|
await this.db.updateDeploymentsByProjectIds(
|
||||||
|
Array.from(productionDeploymentProjectIds),
|
||||||
|
{ isCurrent: false }
|
||||||
|
);
|
||||||
|
|
||||||
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => {
|
const recordToDeploymentsMap = deployments.reduce(
|
||||||
|
(acc: { [key: string]: Deployment }, deployment) => {
|
||||||
acc[deployment.applicationRecordId] = deployment;
|
acc[deployment.applicationRecordId] = deployment;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
// Update deployment data for ApplicationDeploymentRecords
|
// Update deployment data for ApplicationDeploymentRecords
|
||||||
const deploymentUpdatePromises = records.map(async (record) => {
|
const deploymentUpdatePromises = records.map(async (record) => {
|
||||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||||
|
|
||||||
await this.db.updateDeploymentById(
|
await this.db.updateDeploymentById(deployment.id, {
|
||||||
deployment.id,
|
|
||||||
{
|
|
||||||
applicationDeploymentRecordId: record.id,
|
applicationDeploymentRecordId: record.id,
|
||||||
applicationDeploymentRecordData: record.attributes,
|
applicationDeploymentRecordData: record.attributes,
|
||||||
url: record.attributes.url,
|
url: record.attributes.url,
|
||||||
status: DeploymentStatus.Ready,
|
status: DeploymentStatus.Ready,
|
||||||
isCurrent: deployment.environment === Environment.Production
|
isCurrent: deployment.environment === Environment.Production
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`);
|
log(
|
||||||
|
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(deploymentUpdatePromises);
|
await Promise.all(deploymentUpdatePromises);
|
||||||
@@ -181,7 +193,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 });
|
||||||
}
|
}
|
||||||
@@ -206,13 +221,19 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,20 +242,28 @@ export class Service {
|
|||||||
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: {
|
||||||
@@ -279,29 +308,41 @@ export class Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
|
async addEnvironmentVariables (
|
||||||
const formattedEnvironmentVariables = data.map((environmentVariable) => {
|
projectId: string,
|
||||||
|
data: { environments: string[]; key: string; value: string }[]
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const formattedEnvironmentVariables = data
|
||||||
|
.map((environmentVariable) => {
|
||||||
return environmentVariable.environments.map((environment) => {
|
return environmentVariable.environments.map((environment) => {
|
||||||
return ({
|
return {
|
||||||
key: environmentVariable.key,
|
key: environmentVariable.key,
|
||||||
value: environmentVariable.value,
|
value: environmentVariable.value,
|
||||||
environment: environment as Environment,
|
environment: environment as Environment,
|
||||||
project: Object.assign(new Project(), {
|
project: Object.assign(new Project(), {
|
||||||
id: projectId
|
id: projectId
|
||||||
})
|
})
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
}).flat();
|
.flat();
|
||||||
|
|
||||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
|
||||||
|
formattedEnvironmentVariables
|
||||||
|
);
|
||||||
return savedEnvironmentVariables;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
async updateEnvironmentVariable (
|
||||||
|
environmentVariableId: string,
|
||||||
|
data: DeepPartial<EnvironmentVariable>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
async removeEnvironmentVariable (
|
||||||
|
environmentVariableId: string
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +358,10 @@ 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(user.id);
|
const octokit = await this.getOctokit(user.id);
|
||||||
|
|
||||||
@@ -342,7 +386,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({
|
||||||
@@ -362,12 +408,16 @@ export class Service {
|
|||||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||||
|
|
||||||
if (!recordData.repoUrl) {
|
if (!recordData.repoUrl) {
|
||||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
const { data: repoDetails } = await octokit.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo
|
||||||
|
});
|
||||||
recordData.repoUrl = repoDetails.html_url;
|
recordData.repoUrl = repoDetails.html_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||||
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
|
const { applicationRecordId, applicationRecordData } =
|
||||||
|
await this.registry.createApplicationRecord({
|
||||||
appName: repo,
|
appName: repo,
|
||||||
packageJSON,
|
packageJSON,
|
||||||
appType: data.project!.template!,
|
appType: data.project!.template!,
|
||||||
@@ -439,7 +489,9 @@ export class Service {
|
|||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(user.id);
|
||||||
const [owner, repo] = project.repository.split('/');
|
const [owner, repo] = project.repository.split('/');
|
||||||
|
|
||||||
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,
|
||||||
@@ -476,7 +528,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']
|
||||||
@@ -484,9 +539,13 @@ export class Service {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
||||||
if (
|
if (
|
||||||
!(err instanceof RequestError &&
|
!(
|
||||||
|
err instanceof RequestError &&
|
||||||
err.status === 422 &&
|
err.status === 422 &&
|
||||||
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR))
|
(err.response?.data as any).errors.some(
|
||||||
|
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@@ -498,7 +557,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}`);
|
||||||
@@ -510,15 +571,18 @@ export class Service {
|
|||||||
|
|
||||||
for await (const project of projects) {
|
for await (const project of projects) {
|
||||||
const octokit = await this.getOctokit(project.ownerId);
|
const octokit = await this.getOctokit(project.ownerId);
|
||||||
const [domain] = await this.db.getDomainsByProjectId(project.id, { branch });
|
const [domain] = await this.db.getDomainsByProjectId(project.id, {
|
||||||
|
branch
|
||||||
|
});
|
||||||
|
|
||||||
// Create deployment with branch and latest commit in GitHub data
|
// Create deployment with branch and latest commit in GitHub data
|
||||||
await this.createDeployment(project.ownerId,
|
await this.createDeployment(project.ownerId, octokit, {
|
||||||
octokit,
|
|
||||||
{
|
|
||||||
project,
|
project,
|
||||||
branch,
|
branch,
|
||||||
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
environment:
|
||||||
|
project.prodBranch === branch
|
||||||
|
? Environment.Production
|
||||||
|
: Environment.Preview,
|
||||||
domain,
|
domain,
|
||||||
commitHash: headCommit.id,
|
commitHash: headCommit.id,
|
||||||
commitMessage: headCommit.message
|
commitMessage: headCommit.message
|
||||||
@@ -526,7 +590,10 @@ export class Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
async updateProject (
|
||||||
|
projectId: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateProjectById(projectId, data);
|
return this.db.updateProjectById(projectId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +610,9 @@ 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);
|
||||||
@@ -582,7 +651,10 @@ export class Service {
|
|||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
async rollbackDeployment (
|
||||||
|
projectId: string,
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<boolean> {
|
||||||
// TODO: Implement transactions
|
// TODO: Implement transactions
|
||||||
const oldCurrentDeployment = await this.db.getDeployment({
|
const oldCurrentDeployment = await this.db.getDeployment({
|
||||||
relations: {
|
relations: {
|
||||||
@@ -600,16 +672,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);
|
||||||
|
|
||||||
@@ -634,12 +715,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
|
||||||
@@ -680,7 +769,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;
|
||||||
|
|||||||
@@ -47,5 +47,5 @@ interface RegistryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
export interface AppDeploymentRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRecordAttributes
|
attributes: AppDeploymentRecordAttributes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ export const getEntities = async (filePath: string): Promise<any> => {
|
|||||||
return entities;
|
return entities;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, entities: any, relations?: any | undefined): Promise<Entity[]> => {
|
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 entityRepository = dataSource.getRepository(entityType);
|
||||||
|
|
||||||
const savedEntity: Entity[] = [];
|
const savedEntity: Entity[] = [];
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
+9
-31
@@ -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,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 { checkFileExists, getConfig, getEntities, loadAndSaveData } 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,19 +32,34 @@ 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 generateTestData = async (dataSource: DataSource) => {
|
const generateTestData = async (dataSource: DataSource) => {
|
||||||
const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH));
|
const userEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_DATA_PATH)
|
||||||
|
);
|
||||||
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
||||||
|
|
||||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
const orgEntities = await getEntities(
|
||||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities);
|
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 projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH));
|
const projectEntities = await getEntities(
|
||||||
const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations);
|
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedProjects = await loadAndSaveData(
|
||||||
|
Project,
|
||||||
|
dataSource,
|
||||||
|
projectEntities,
|
||||||
|
projectRelations
|
||||||
|
);
|
||||||
|
|
||||||
const domainRepository = dataSource.getRepository(Domain);
|
const domainRepository = dataSource.getRepository(Domain);
|
||||||
|
|
||||||
@@ -47,16 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH));
|
const primaryDomainsEntities = await getEntities(
|
||||||
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, domainPrimaryRelations);
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH));
|
const redirectDomainsEntities = await getEntities(
|
||||||
await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations);
|
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Domain,
|
||||||
|
dataSource,
|
||||||
|
redirectDomainsEntities,
|
||||||
|
domainRedirectedRelations
|
||||||
|
);
|
||||||
|
|
||||||
const savedDomains = await domainRepository.find();
|
const savedDomains = await domainRepository.find();
|
||||||
|
|
||||||
@@ -65,16 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH));
|
const userOrganizationsEntities = await getEntities(
|
||||||
await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations);
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH));
|
const projectMembersEntities = await getEntities(
|
||||||
await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations);
|
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
ProjectMember,
|
||||||
|
dataSource,
|
||||||
|
projectMembersEntities,
|
||||||
|
projectMemberRelations
|
||||||
|
);
|
||||||
|
|
||||||
const deploymentRelations = {
|
const deploymentRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
@@ -82,15 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
createdBy: savedUsers
|
createdBy: savedUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH));
|
const deploymentsEntities = await getEntities(
|
||||||
await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations);
|
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Deployment,
|
||||||
|
dataSource,
|
||||||
|
deploymentsEntities,
|
||||||
|
deploymentRelations
|
||||||
|
);
|
||||||
|
|
||||||
const environmentVariableRelations = {
|
const environmentVariableRelations = {
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH));
|
const environmentVariablesEntities = await getEntities(
|
||||||
await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations);
|
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
EnvironmentVariable,
|
||||||
|
dataSource,
|
||||||
|
environmentVariablesEntities,
|
||||||
|
environmentVariableRelations
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
|||||||
@@ -21,12 +21,20 @@ async function main () {
|
|||||||
|
|
||||||
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 authorityNames) {
|
for await (const name of authorityNames) {
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ const log = debug('snowball:publish-deploy-records');
|
|||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig, database, misc } = 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',
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# deployer
|
||||||
|
|
||||||
|
- Install dependencies
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
brew install jq # if you do not have jq installed already
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example of how to make the necessary deploy edits [here](https://github.com/snowball-tools/snowballtools-base/pull/131/files).
|
||||||
|
|
||||||
|
- Replace variables in the following files
|
||||||
|
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
- update the name & application version numbers
|
||||||
|
- `<CURRENT_DATE_TIME>`: Replace with current time which can be generated by command `date -u`
|
||||||
|
```yml
|
||||||
|
# Example
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
meta:
|
||||||
|
note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Update record version in [records/application-record.yml](records/application-record.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: <NEW_VERSION>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Update commit hash in the following places:
|
||||||
|
- [records/application-record.yml](records/application-record.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
repository_ref: <COMMIT_HASH>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
meta:
|
||||||
|
...
|
||||||
|
repository_ref: <COMMIT_HASH>
|
||||||
|
```
|
||||||
|
- [deploy-frontend.sh](deploy-frontend.sh)
|
||||||
|
Also be sure to update the app version
|
||||||
|
```bash
|
||||||
|
...
|
||||||
|
RCD_APP_VERSION="<NEW_VERSION>"
|
||||||
|
REPO_REF="<COMMIT_HASH>"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run script to deploy app
|
||||||
|
```
|
||||||
|
./deploy-frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
|
...
|
||||||
|
```
|
||||||
|
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
|
||||||
|
|
||||||
|
## Troubleshoot
|
||||||
|
|
||||||
|
- Check deployment status [here](https://console.laconic.com/deployer).
|
||||||
|
- Check records [here](https://console.laconic.com/#/registry).
|
||||||
|
|
||||||
|
- If deployment fails due to low bond balance
|
||||||
|
- Check balances
|
||||||
|
```bash
|
||||||
|
# Account balance
|
||||||
|
yarn laconic cns account get
|
||||||
|
|
||||||
|
# Bond balance
|
||||||
|
yarn laconic cns bond get --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||||
|
```
|
||||||
|
- Command to refill bond
|
||||||
|
```bash
|
||||||
|
yarn laconic cns bond refill --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac --type aphoton --quantity 10000000
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
cns:
|
||||||
|
restEndpoint: http://console.laconic.com:1317
|
||||||
|
gqlEndpoint: http://console.laconic.com:9473/api
|
||||||
|
chainId: laconic_9000-1
|
||||||
|
gas: 1000000
|
||||||
|
fees: 200000aphoton
|
||||||
|
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
|
||||||
|
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-record.yml
|
||||||
|
CONFIG_FILE=config.yml
|
||||||
|
RCD_APP_VERSION="0.1.3"
|
||||||
|
REPO_REF="513ca69d01bee857cf207a0605483205b384e218"
|
||||||
|
|
||||||
|
# Publish ApplicationRecord
|
||||||
|
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
|
echo "ApplicationRecord published"
|
||||||
|
echo $RECORD_ID
|
||||||
|
|
||||||
|
# Set name to record
|
||||||
|
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||||
|
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${RCD_APP_VERSION}" "$RECORD_ID"
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${REPO_REF}" "$RECORD_ID"
|
||||||
|
# Set name if latest release
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||||
|
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||||
|
|
||||||
|
# Check if record found for REGISTRY_APP_CRN
|
||||||
|
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||||
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
|
echo "No record found for $REGISTRY_APP_CRN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
|
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
|
echo "ApplicationDeploymentRequest published"
|
||||||
|
echo $DEPLOYMENT_REQUEST_ID
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "deployer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@cerc-io/laconic-registry-cli": "^0.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
record:
|
||||||
|
type: ApplicationDeploymentRequest
|
||||||
|
version: '1.0.0'
|
||||||
|
name: snowballtools-base-frontend@0.1.3
|
||||||
|
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.3
|
||||||
|
dns: dashboard
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
|
# If GitHub client ID is changed, same ID and corresponding secret has to be set in backend config
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||||
|
# New config env after changes for image upload PWA
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||||
|
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
|
meta:
|
||||||
|
# Set CURRENT_DATE_TIME; Use command date -u
|
||||||
|
note: Added by Snowball @ Tue Feb 27 17:24:06 UTC 2024
|
||||||
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
repository_ref: 513ca69d01bee857cf207a0605483205b384e218
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: 0.0.11
|
||||||
|
repository_ref: 513ca69d01bee857cf207a0605483205b384e218
|
||||||
|
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||||
|
app_type: webapp
|
||||||
|
# name is set to repo name
|
||||||
|
name: snowballtools-base-frontend
|
||||||
|
# app_version is set from package.json
|
||||||
|
app_version: 0.1.3
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||||
|
|
||||||
REACT_APP_GITHUB_CLIENT_ID =
|
REACT_APP_GITHUB_CLIENT_ID =
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||||
|
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||||
|
|
||||||
REACT_APP_WALLET_CONNECT_ID =
|
REACT_APP_WALLET_CONNECT_ID =
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||||
|
|
||||||
|
REACT_APP_GITHUB_CLIENT_ID =
|
||||||
|
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||||
|
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||||
|
|
||||||
|
REACT_APP_WALLET_CONNECT_ID =
|
||||||
@@ -16,5 +16,10 @@
|
|||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended"
|
||||||
]
|
],
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||||
|
"@fontsource/inter": "^5.0.16",
|
||||||
"@material-tailwind/react": "^2.1.7",
|
"@material-tailwind/react": "^2.1.7",
|
||||||
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@tanstack/react-query": "^5.22.2",
|
"@tanstack/react-query": "^5.22.2",
|
||||||
"@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",
|
||||||
@@ -16,13 +26,15 @@
|
|||||||
"@web3modal/wagmi": "^4.0.5",
|
"@web3modal/wagmi": "^4.0.5",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"date-fns": "^3.0.1",
|
"clsx": "^2.1.0",
|
||||||
"downshift": "^8.2.3",
|
"date-fns": "^3.3.1",
|
||||||
|
"downshift": "^8.3.2",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"gql-client": "^1.0.0",
|
"gql-client": "^1.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"octokit": "^3.1.2",
|
"octokit": "^3.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-calendar": "^4.8.0",
|
||||||
"react-code-blocks": "^0.1.6",
|
"react-code-blocks": "^0.1.6",
|
||||||
"react-day-picker": "^8.9.1",
|
"react-day-picker": "^8.9.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -34,6 +46,7 @@
|
|||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"siwe": "^2.1.4",
|
"siwe": "^2.1.4",
|
||||||
|
"tailwind-variants": "^0.2.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"usehooks-ts": "^2.10.0",
|
"usehooks-ts": "^2.10.0",
|
||||||
"vertical-stepper-nav": "^1.0.2",
|
"vertical-stepper-nav": "^1.0.2",
|
||||||
@@ -78,6 +91,6 @@
|
|||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"tailwindcss": "^3.3.6"
|
"tailwindcss": "^3.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,18 +3,29 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta name="description" content="snowball tools dashboard" />
|
||||||
name="description"
|
|
||||||
content="snowball tools dashboard"
|
|
||||||
/>
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
|
<link
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
rel="apple-touch-icon"
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
|
sizes="180x180"
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest">
|
href="%PUBLIC_URL%/apple-touch-icon.png"
|
||||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
/>
|
||||||
<meta name="theme-color" content="#ffffff">
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||||
|
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<title>Snowball</title>
|
<title>Snowball</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="333" height="5" viewBox="0 0 333 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 4L6.39555 1.30714C8.38078 0.47125 10.6192 0.47125 12.6045 1.30714L15.8955 2.69286C17.8808 3.52875 20.1192 3.52875 22.1045 2.69286L25.3955 1.30714C27.3808 0.47125 29.6192 0.47125 31.6045 1.30714L34.8955 2.69286C36.8808 3.52875 39.1192 3.52875 41.1045 2.69286L44.3955 1.30714C46.3808 0.47125 48.6192 0.47125 50.6045 1.30714L53.8955 2.69286C55.8808 3.52875 58.1192 3.52875 60.1045 2.69286L63.3955 1.30714C65.3808 0.47125 67.6192 0.47125 69.6045 1.30714L72.8955 2.69286C74.8808 3.52875 77.1192 3.52875 79.1045 2.69286L82.3955 1.30714C84.3808 0.47125 86.6192 0.47125 88.6045 1.30714L91.8955 2.69286C93.8808 3.52875 96.1192 3.52875 98.1045 2.69286L101.396 1.30714C103.381 0.47125 105.619 0.47125 107.604 1.30714L110.896 2.69286C112.881 3.52875 115.119 3.52875 117.104 2.69286L120.396 1.30714C122.381 0.47125 124.619 0.47125 126.604 1.30714L129.896 2.69286C131.881 3.52875 134.119 3.52875 136.104 2.69286L139.396 1.30714C141.381 0.47125 143.619 0.47125 145.604 1.30714L148.896 2.69286C150.881 3.52875 153.119 3.52875 155.104 2.69286L158.396 1.30714C160.381 0.47125 162.619 0.47125 164.604 1.30714L167.896 2.69286C169.881 3.52875 172.119 3.52875 174.104 2.69286L177.396 1.30714C179.381 0.47125 181.619 0.47125 183.604 1.30714L186.896 2.69286C188.881 3.52875 191.119 3.52875 193.104 2.69286L196.396 1.30714C198.381 0.47125 200.619 0.47125 202.604 1.30714L205.896 2.69286C207.881 3.52875 210.119 3.52875 212.104 2.69286L215.396 1.30714C217.381 0.47125 219.619 0.47125 221.604 1.30714L224.896 2.69286C226.881 3.52875 229.119 3.52875 231.104 2.69286L234.396 1.30714C236.381 0.47125 238.619 0.47125 240.604 1.30714L243.896 2.69286C245.881 3.52875 248.119 3.52875 250.104 2.69286L253.396 1.30714C255.381 0.47125 257.619 0.47125 259.604 1.30714L262.896 2.69286C264.881 3.52875 267.119 3.52875 269.104 2.69286L272.396 1.30714C274.381 0.47125 276.619 0.47125 278.604 1.30714L281.896 2.69286C283.881 3.52875 286.119 3.52875 288.104 2.69286L291.396 1.30714C293.381 0.47125 295.619 0.47125 297.604 1.30714L300.896 2.69286C302.881 3.52875 305.119 3.52875 307.104 2.69286L310.396 1.30714C312.381 0.47125 314.619 0.47125 316.604 1.30714L319.973 2.72566C321.913 3.54216 324.095 3.56183 326.049 2.78039L330.029 1.18845C331.936 0.425535 334.064 0.425535 335.971 1.18845L343 4" stroke="#DBEBF9"/>
|
||||||
|
<path d="M6.39555 1.30714L0 4H342.5L336.027 1.27434C334.087 0.457837 331.905 0.438174 329.951 1.21961L326.049 2.78039C324.095 3.56183 321.913 3.54216 319.973 2.72566L316.604 1.30714C314.619 0.47125 312.381 0.47125 310.396 1.30714L307.104 2.69286C305.119 3.52875 302.881 3.52875 300.896 2.69286L297.604 1.30714C295.619 0.47125 293.381 0.47125 291.396 1.30714L288.104 2.69286C286.119 3.52875 283.881 3.52875 281.896 2.69286L278.604 1.30714C276.619 0.47125 274.381 0.47125 272.396 1.30714L269.104 2.69286C267.119 3.52875 264.881 3.52875 262.896 2.69286L259.604 1.30714C257.619 0.47125 255.381 0.47125 253.396 1.30714L250.104 2.69286C248.119 3.52875 245.881 3.52875 243.896 2.69286L240.604 1.30714C238.619 0.47125 236.381 0.47125 234.396 1.30714L231.104 2.69286C229.119 3.52875 226.881 3.52875 224.896 2.69286L221.604 1.30714C219.619 0.47125 217.381 0.47125 215.396 1.30714L212.104 2.69286C210.119 3.52875 207.881 3.52875 205.896 2.69286L202.604 1.30714C200.619 0.47125 198.381 0.47125 196.396 1.30714L193.104 2.69286C191.119 3.52875 188.881 3.52875 186.896 2.69286L183.604 1.30714C181.619 0.47125 179.381 0.47125 177.396 1.30714L174.104 2.69286C172.119 3.52875 169.881 3.52875 167.896 2.69286L164.604 1.30714C162.619 0.47125 160.381 0.47125 158.396 1.30714L155.104 2.69286C153.119 3.52875 150.881 3.52875 148.896 2.69286L145.604 1.30714C143.619 0.47125 141.381 0.47125 139.396 1.30714L136.104 2.69286C134.119 3.52875 131.881 3.52875 129.896 2.69286L126.604 1.30714C124.619 0.47125 122.381 0.47125 120.396 1.30714L117.104 2.69286C115.119 3.52875 112.881 3.52875 110.896 2.69286L107.604 1.30714C105.619 0.47125 103.381 0.47125 101.396 1.30714L98.1045 2.69286C96.1192 3.52875 93.8808 3.52875 91.8955 2.69286L88.6045 1.30714C86.6192 0.47125 84.3808 0.47125 82.3955 1.30714L79.1045 2.69286C77.1192 3.52875 74.8808 3.52875 72.8955 2.69286L69.6045 1.30714C67.6192 0.47125 65.3808 0.47125 63.3955 1.30714L60.1045 2.69286C58.1192 3.52875 55.8808 3.52875 53.8955 2.69286L50.6045 1.30714C48.6192 0.47125 46.3808 0.47125 44.3955 1.30714L41.1045 2.69286C39.1192 3.52875 36.8808 3.52875 34.8955 2.69286L31.6045 1.30714C29.6192 0.47125 27.3808 0.47125 25.3955 1.30714L22.1045 2.69286C20.1192 3.52875 17.8808 3.52875 15.8955 2.69286L12.6045 1.30714C10.6192 0.47125 8.38078 0.47125 6.39555 1.30714Z" fill="url(#paint0_linear_1729_11298)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_1729_11298" x1="171.25" y1="0" x2="171.25" y2="4" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E6F4FF"/>
|
||||||
|
<stop offset="1" stop-color="#F9FCFF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -1,27 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "Progressive Web App (PWA)",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "Kotlin",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "React Native",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"name": "Swift",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5",
|
|
||||||
"name": "Web app",
|
|
||||||
"icon": "^"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Progressive Web App (PWA)',
|
||||||
|
icon: '^',
|
||||||
|
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Image Upload PWA',
|
||||||
|
icon: '^',
|
||||||
|
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Kotlin',
|
||||||
|
icon: '^',
|
||||||
|
repoFullName: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'React Native',
|
||||||
|
icon: '^',
|
||||||
|
repoFullName: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'Swift',
|
||||||
|
icon: '^',
|
||||||
|
repoFullName: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -126,7 +126,8 @@ const DatePicker = ({
|
|||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
/>
|
/>
|
||||||
</PopoverHandler>
|
</PopoverHandler>
|
||||||
<PopoverContent>
|
{/* TODO: Figure out what placeholder is for */}
|
||||||
|
<PopoverContent placeholder={''}>
|
||||||
{mode === 'single' && (
|
{mode === 'single' && (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
mode="single"
|
mode="single"
|
||||||
@@ -145,19 +146,23 @@ const DatePicker = ({
|
|||||||
/>
|
/>
|
||||||
<HorizontalLine />
|
<HorizontalLine />
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
{/* TODO: Figure out what placeholder is for */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full mr-2"
|
className="rounded-full mr-2"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* TODO: Figure out what placeholder is for */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
color="gray"
|
color="gray"
|
||||||
onClick={() => handleRangeSelect()}
|
onClick={() => handleRangeSelect()}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
Select
|
Select
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const HorizontalLine = () => {
|
const HorizontalLine = () => {
|
||||||
return <hr className="h-px bg-gray-300 border-0" />;
|
return <hr className="h-px bg-gray-100 border-0" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HorizontalLine;
|
export default HorizontalLine;
|
||||||
|
|||||||
@@ -1,33 +1,23 @@
|
|||||||
import React, { forwardRef, RefAttributes } from 'react';
|
import React, { forwardRef, RefAttributes } from 'react';
|
||||||
|
|
||||||
import { Input, InputProps } from '@material-tailwind/react';
|
import { SearchIcon } from './shared/CustomIcon';
|
||||||
|
import { Input, InputProps } from './shared/Input';
|
||||||
|
|
||||||
const SearchBar: React.ForwardRefRenderFunction<
|
const SearchBar: React.ForwardRefRenderFunction<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
InputProps & RefAttributes<HTMLInputElement>
|
InputProps & RefAttributes<HTMLInputElement>
|
||||||
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full gap-2">
|
<div className="relative flex w-full">
|
||||||
<Input
|
<Input
|
||||||
|
leftIcon={<SearchIcon />}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
containerProps={{
|
appearance={'borderless'}
|
||||||
className: 'min-w-[288px]',
|
|
||||||
}}
|
|
||||||
className="!border-t-blue-gray-300 pl-9 placeholder:text-blue-gray-300 focus:!border-blue-gray-300"
|
|
||||||
labelProps={{
|
|
||||||
className: 'before:content-none after:content-none',
|
|
||||||
}}
|
|
||||||
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
|
|
||||||
crossOrigin={undefined}
|
|
||||||
{...props}
|
{...props}
|
||||||
inputRef={ref}
|
|
||||||
/>
|
/>
|
||||||
<div className="!absolute left-3 top-[13px]">
|
|
||||||
<i>^</i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,20 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Organization } from 'gql-client';
|
import { Organization } from 'gql-client';
|
||||||
|
|
||||||
import { Typography, Option } from '@material-tailwind/react';
|
import { Option } from '@material-tailwind/react';
|
||||||
import { useDisconnect } from 'wagmi';
|
import { useDisconnect } from 'wagmi';
|
||||||
|
|
||||||
import { useGQLClient } from '../context/GQLClientContext';
|
import { useGQLClient } from '../context/GQLClientContext';
|
||||||
import AsyncSelect from './shared/AsyncSelect';
|
import AsyncSelect from './shared/AsyncSelect';
|
||||||
|
import {
|
||||||
|
ChevronGrabberHorizontal,
|
||||||
|
FolderIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
LifeBuoyIcon,
|
||||||
|
QuestionMarkRoundIcon,
|
||||||
|
SettingsSlidersIcon,
|
||||||
|
} from './shared/CustomIcon';
|
||||||
|
import { Tabs } from 'components/shared/Tabs';
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
@@ -33,61 +42,99 @@ const Sidebar = () => {
|
|||||||
}, [disconnect, navigate]);
|
}, [disconnect, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full p-4">
|
<div className="flex flex-col h-full p-4 pt-10">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div>
|
|
||||||
<Link to={`/${orgSlug}`}>
|
<Link to={`/${orgSlug}`}>
|
||||||
<h3 className="text-black text-2xl">Snowball</h3>
|
<div className="flex items-center space-x-3 mb-10 ml-2">
|
||||||
</Link>
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Snowball Logo"
|
||||||
|
className="h-8 w-8 rounded-lg"
|
||||||
|
/>
|
||||||
|
<span className="text-2xl font-bold text-snowball-900">
|
||||||
|
Snowball
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
className="bg-white py-2"
|
containerProps={{ className: 'h-14 border-none' }}
|
||||||
|
labelProps={{ className: 'before:border-none after:border-none' }}
|
||||||
|
className="bg-white rounded-lg shadow border-none"
|
||||||
value={selectedOrgSlug}
|
value={selectedOrgSlug}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedOrgSlug(value!);
|
setSelectedOrgSlug(value!);
|
||||||
navigate(`/${value}`);
|
navigate(`/${value}`);
|
||||||
}}
|
}}
|
||||||
selected={(_, index) => (
|
selected={(_, index) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center space-x-3">
|
||||||
<div>^</div>
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Application Logo"
|
||||||
|
className="h-8 w-8 rounded-lg"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span>{organizations[index!]?.name}</span>
|
<div className="text-sm font-semibold">
|
||||||
<Typography>Organization</Typography>
|
{organizations[index!]?.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">Organization</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
|
||||||
>
|
>
|
||||||
{/* TODO: Show label organization and manage in option */}
|
{/* TODO: Show label organization and manage in option */}
|
||||||
{organizations.map((org) => (
|
{organizations.map((org) => (
|
||||||
<Option key={org.id} value={org.slug}>
|
<Option key={org.id} value={org.slug}>
|
||||||
^ {org.name}
|
<div className="flex items-center space-x-3">
|
||||||
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Application Logo"
|
||||||
|
className="h-8 w-8 rounded-lg"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold">{org.name}</div>
|
||||||
|
<div className="text-xs text-gray-500">Organization</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</AsyncSelect>
|
</AsyncSelect>
|
||||||
<div>
|
<Tabs defaultValue="Projects" orientation="vertical" className="mt-10">
|
||||||
<NavLink
|
<Tabs.List>
|
||||||
to={`/${orgSlug}`}
|
{[
|
||||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
{ title: 'Projects', url: `/${orgSlug}/`, icon: <FolderIcon /> },
|
||||||
>
|
{
|
||||||
<Typography>Projects</Typography>
|
title: 'Settings',
|
||||||
|
url: `/${orgSlug}/settings`,
|
||||||
|
icon: <SettingsSlidersIcon />,
|
||||||
|
},
|
||||||
|
].map(({ title, icon, url }, index) => (
|
||||||
|
<NavLink to={url} key={index}>
|
||||||
|
<Tabs.Trigger icon={icon} value={title}>
|
||||||
|
{title}
|
||||||
|
</Tabs.Trigger>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="grow flex flex-col justify-end mb-8">
|
||||||
<NavLink
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
to={`/${orgSlug}/settings`}
|
{/* TODO: use proper link buttons */}
|
||||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
<Tabs.List>
|
||||||
>
|
<Tabs.Trigger icon={<GlobeIcon />} value="">
|
||||||
<Typography>Settings</Typography>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grow flex flex-col justify-end">
|
|
||||||
<a className="cursor-pointer" onClick={handleLogOut}>
|
<a className="cursor-pointer" onClick={handleLogOut}>
|
||||||
Log Out
|
Log Out
|
||||||
</a>
|
</a>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger icon={<QuestionMarkRoundIcon />} value="">
|
||||||
<a className="cursor-pointer">Documentation</a>
|
<a className="cursor-pointer">Documentation</a>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger icon={<LifeBuoyIcon />} value="">
|
||||||
<a className="cursor-pointer">Support</a>
|
<a className="cursor-pointer">Support</a>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Project } from 'gql-client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
MenuHandler,
|
|
||||||
MenuList,
|
|
||||||
MenuItem,
|
|
||||||
Typography,
|
|
||||||
Avatar,
|
|
||||||
} from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import { relativeTimeMs } from '../../utils/time';
|
|
||||||
|
|
||||||
interface ProjectCardProps {
|
|
||||||
project: Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg shadow">
|
|
||||||
<div className="flex gap-2 p-2 items-center">
|
|
||||||
<Avatar variant="rounded" src={project.icon || '/gray.png'} />
|
|
||||||
<div className="grow">
|
|
||||||
<Link to={`projects/${project.id}`}>
|
|
||||||
<Typography>{project.name}</Typography>
|
|
||||||
<Typography color="gray" variant="small">
|
|
||||||
{project.deployments[0]?.domain?.name ??
|
|
||||||
'No Production Deployment'}
|
|
||||||
</Typography>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Menu placement="bottom-end">
|
|
||||||
<MenuHandler>
|
|
||||||
<button>...</button>
|
|
||||||
</MenuHandler>
|
|
||||||
<MenuList>
|
|
||||||
<MenuItem>^ Project settings</MenuItem>
|
|
||||||
<MenuItem className="text-red-500">^ Delete project</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<div className="border-t-2 border-solid p-4 bg-gray-50">
|
|
||||||
{project.deployments.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<Typography variant="small" color="gray">
|
|
||||||
^ {project.deployments[0].commitMessage}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="small" color="gray">
|
|
||||||
{relativeTimeMs(project.deployments[0].createdAt)} on ^
|
|
||||||
{project.deployments[0].branch}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Typography variant="small" color="gray">
|
|
||||||
No Production deployment
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectCard;
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const projectCardTheme = tv({
|
||||||
|
slots: {
|
||||||
|
wrapper: [
|
||||||
|
'bg-surface-card',
|
||||||
|
'shadow-card',
|
||||||
|
'rounded-2xl',
|
||||||
|
'flex',
|
||||||
|
'flex-col',
|
||||||
|
],
|
||||||
|
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
|
||||||
|
content: ['flex', 'flex-col', 'gap-1', 'flex-1'],
|
||||||
|
title: [
|
||||||
|
'text-sm',
|
||||||
|
'font-medium',
|
||||||
|
'text-elements-high-em',
|
||||||
|
'tracking-[-0.006em]',
|
||||||
|
],
|
||||||
|
description: ['text-xs', 'text-elements-low-em'],
|
||||||
|
icons: ['flex', 'items-center', 'gap-1'],
|
||||||
|
lowerContent: [
|
||||||
|
'bg-surface-card-hovered',
|
||||||
|
'px-4',
|
||||||
|
'py-4',
|
||||||
|
'flex',
|
||||||
|
'flex-col',
|
||||||
|
'gap-2',
|
||||||
|
'rounded-b-2xl',
|
||||||
|
],
|
||||||
|
latestDeployment: ['flex', 'items-center', 'gap-2'],
|
||||||
|
deploymentStatusContainer: [
|
||||||
|
'h-3',
|
||||||
|
'w-3',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'justify-center',
|
||||||
|
],
|
||||||
|
deploymentStatus: ['w-1', 'h-1', 'rounded-full'],
|
||||||
|
deploymentName: ['text-xs', 'text-elements-low-em'],
|
||||||
|
deploymentText: [
|
||||||
|
'text-xs',
|
||||||
|
'text-elements-low-em',
|
||||||
|
'font-mono',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'gap-2',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
status: {
|
||||||
|
success: {
|
||||||
|
deploymentStatus: ['bg-emerald-500'],
|
||||||
|
},
|
||||||
|
'in-progress': {
|
||||||
|
deploymentStatus: ['bg-orange-400'],
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
deploymentStatus: ['bg-rose-500'],
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
deploymentStatus: ['bg-gray-500'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProjectCardTheme = VariantProps<typeof projectCardTheme>;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import React, { ComponentPropsWithoutRef, MouseEvent } from 'react';
|
||||||
|
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||||
|
import { Project } from 'gql-client';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
|
import {
|
||||||
|
BranchIcon,
|
||||||
|
ClockIcon,
|
||||||
|
GitHubLogo,
|
||||||
|
HorizontalDotIcon,
|
||||||
|
WarningDiamondIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { relativeTimeMs } from 'utils/time';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
|
import { getInitials } from 'utils/geInitials';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
|
export interface ProjectCardProps
|
||||||
|
extends ComponentPropsWithoutRef<'div'>,
|
||||||
|
ProjectCardTheme {
|
||||||
|
project: Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectCard = ({
|
||||||
|
className,
|
||||||
|
project,
|
||||||
|
status = 'failure',
|
||||||
|
...props
|
||||||
|
}: ProjectCardProps) => {
|
||||||
|
const theme = projectCardTheme();
|
||||||
|
const hasDeployment = project.deployments.length > 0;
|
||||||
|
// TODO: Update this to use the actual status from the API
|
||||||
|
const hasError = status === 'failure';
|
||||||
|
|
||||||
|
const handleOptionsClick = (
|
||||||
|
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
|
||||||
|
) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props} className={theme.wrapper({ className })}>
|
||||||
|
{/* Upper content */}
|
||||||
|
<div className={theme.upperContent()}>
|
||||||
|
{/* Icon container */}
|
||||||
|
<Avatar
|
||||||
|
size={48}
|
||||||
|
imageSrc={project.icon}
|
||||||
|
initials={getInitials(project.name)}
|
||||||
|
/>
|
||||||
|
{/* </div> */}
|
||||||
|
{/* Title and website */}
|
||||||
|
<Link to={`projects/${project.id}`} className={theme.content()}>
|
||||||
|
<p className={theme.title()}>{project.name}</p>
|
||||||
|
<p className={theme.description()}>
|
||||||
|
{project.deployments[0]?.domain?.name ?? 'No domain'}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
{/* Icons */}
|
||||||
|
<div className={theme.icons()}>
|
||||||
|
{hasError && <WarningDiamondIcon className="text-elements-danger" />}
|
||||||
|
<Menu placement="bottom-end">
|
||||||
|
<MenuHandler>
|
||||||
|
<Button
|
||||||
|
shape="default"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
iconOnly
|
||||||
|
onClick={handleOptionsClick}
|
||||||
|
>
|
||||||
|
<HorizontalDotIcon />
|
||||||
|
</Button>
|
||||||
|
</MenuHandler>
|
||||||
|
<MenuList placeholder={''}>
|
||||||
|
<MenuItem placeholder={''}>Project settings</MenuItem>
|
||||||
|
<MenuItem className="text-red-500" placeholder={''}>
|
||||||
|
Delete project
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Wave */}
|
||||||
|
<WavyBorder />
|
||||||
|
{/* Lower content */}
|
||||||
|
<div className={theme.lowerContent()}>
|
||||||
|
{/* Latest deployment */}
|
||||||
|
<div className={theme.latestDeployment()}>
|
||||||
|
{/* Dot icon */}
|
||||||
|
<div className={theme.deploymentStatusContainer()}>
|
||||||
|
<div className={theme.deploymentStatus({ status })} />
|
||||||
|
</div>
|
||||||
|
<p className={theme.deploymentText()}>
|
||||||
|
{hasDeployment
|
||||||
|
? project.deployments[0]?.commitMessage
|
||||||
|
: 'No production deployment'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Deployment and branch name */}
|
||||||
|
<div className={theme.deploymentText()}>
|
||||||
|
{hasDeployment ? (
|
||||||
|
<>
|
||||||
|
<GitHubLogo />
|
||||||
|
<span>{relativeTimeMs(project.deployments[0].createdAt)} on</span>
|
||||||
|
<BranchIcon />
|
||||||
|
<span>{project.deployments[0].branch}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ClockIcon />
|
||||||
|
<span>Created {relativeTimeMs(project.createdAt)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './ProjectCard';
|
||||||
@@ -71,12 +71,13 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
|||||||
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
|
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
|
||||||
(!inputValue || !isOpen) && 'hidden'
|
(!inputValue || !isOpen) && 'hidden'
|
||||||
}`}
|
}`}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
<List {...getMenuProps()}>
|
<List {...getMenuProps()}>
|
||||||
{items.length ? (
|
{items.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray" placeholder={''}>
|
||||||
Suggestions
|
Suggestions
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,19 +85,25 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
|||||||
<ListItem
|
<ListItem
|
||||||
selected={highlightedIndex === index || selectedItem === item}
|
selected={highlightedIndex === index || selectedItem === item}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
placeholder={''}
|
||||||
{...getItemProps({ item, index })}
|
{...getItemProps({ item, index })}
|
||||||
>
|
>
|
||||||
<ListItemPrefix>
|
<ListItemPrefix placeholder={''}>
|
||||||
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
|
<Avatar
|
||||||
|
src={item.icon || '/gray.png'}
|
||||||
|
variant="rounded"
|
||||||
|
placeholder={''}
|
||||||
|
/>
|
||||||
</ListItemPrefix>
|
</ListItemPrefix>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h6" color="blue-gray">
|
<Typography variant="h6" color="blue-gray" placeholder={''}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="small"
|
variant="small"
|
||||||
color="gray"
|
color="gray"
|
||||||
className="font-normal"
|
className="font-normal"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{item.organization.name}
|
{item.organization.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -106,7 +113,9 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<Typography>^ No projects matching this name</Typography>
|
<Typography placeholder={''}>
|
||||||
|
^ No projects matching this name
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { Button } from '@material-tailwind/react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import OauthPopup from 'react-oauth-popup';
|
import OauthPopup from 'react-oauth-popup';
|
||||||
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
import { Button } from 'components/shared/Button';
|
||||||
|
import {
|
||||||
|
GitIcon,
|
||||||
|
EllipsesIcon,
|
||||||
|
SnowballIcon,
|
||||||
|
GithubIcon,
|
||||||
|
GitTeaIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { IconWithFrame } from 'components/shared/IconWithFrame';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
|
||||||
const SCOPES = 'repo user';
|
const SCOPES = 'repo user';
|
||||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||||
@@ -14,30 +23,49 @@ interface ConnectAccountInterface {
|
|||||||
onAuth: (token: string) => void;
|
onAuth: (token: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||||
|
onAuth: onToken,
|
||||||
|
}: ConnectAccountInterface) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
|
||||||
const handleCode = async (code: string) => {
|
const handleCode = async (code: string) => {
|
||||||
// Pass code to backend and get access token
|
// Pass code to backend and get access token
|
||||||
const {
|
const {
|
||||||
authenticateGitHub: { token },
|
authenticateGitHub: { token },
|
||||||
} = await client.authenticateGitHub(code);
|
} = await client.authenticateGitHub(code);
|
||||||
onToken(token);
|
onToken(token);
|
||||||
|
toast({
|
||||||
|
onDismiss: dismiss,
|
||||||
|
id: 'connected-to-github',
|
||||||
|
title: 'The Git account is connected.',
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Use correct height
|
// TODO: Use correct height
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
|
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||||
<div>^</div>
|
<div className="flex flex-col items-center max-w-[420px]">
|
||||||
<div>
|
{/** Icons */}
|
||||||
<p>Connect to your git account</p>
|
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
|
||||||
<p>
|
<IconWithFrame icon={<GitIcon />} />
|
||||||
Once connected, you can import a repository from your
|
<EllipsesIcon className="items-center gap-1.5 flex" />
|
||||||
<br />
|
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
|
||||||
account or start with one of our templates.
|
</div>
|
||||||
|
{/** Text */}
|
||||||
|
<div className="flex flex-col gap-1.5 mb-6">
|
||||||
|
<Heading className="text-xl font-medium">
|
||||||
|
Connect to your Git account
|
||||||
|
</Heading>
|
||||||
|
<p className="text-center text-elements-mid-em">
|
||||||
|
Once connected, you can import a repository from your account or
|
||||||
|
start with one of our templates.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex">
|
{/** CTA Buttons */}
|
||||||
|
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
|
||||||
<OauthPopup
|
<OauthPopup
|
||||||
url={GITHUB_OAUTH_URL}
|
url={GITHUB_OAUTH_URL}
|
||||||
onCode={handleCode}
|
onCode={handleCode}
|
||||||
@@ -46,11 +74,28 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
|||||||
width={1000}
|
width={1000}
|
||||||
height={1000}
|
height={1000}
|
||||||
>
|
>
|
||||||
<Button className="rounded-full mx-2">Connect to Github</Button>
|
<Button
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
leftIcon={<GithubIcon />}
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Connect to GitHub
|
||||||
|
</Button>
|
||||||
</OauthPopup>
|
</OauthPopup>
|
||||||
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
<Button
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
leftIcon={<GitTeaIcon />}
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Connect to GitTea
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TODO: Add ConnectAccountTabPanel */}
|
||||||
|
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||||
<ConnectAccountTabPanel />
|
<ConnectAccountTabPanel />
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Tabs, TabsHeader, Tab } from '@material-tailwind/react';
|
import { Tabs } from 'components/shared/Tabs';
|
||||||
|
|
||||||
const ConnectAccountTabPanel = () => {
|
const ConnectAccountTabPanel: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
|
<Tabs
|
||||||
<TabsHeader className="grid grid-cols-2">
|
defaultValue="Connect Accounts Tab Panel"
|
||||||
<Tab className="row-span-1" value="import">
|
orientation="horizontal"
|
||||||
Import a repository
|
className="mt-10"
|
||||||
</Tab>
|
>
|
||||||
<Tab className="row-span-2" value="template">
|
<Tabs.List>
|
||||||
Start with a template
|
{[
|
||||||
</Tab>
|
{ title: 'Import a repository' },
|
||||||
</TabsHeader>
|
{ title: 'Start with a template' },
|
||||||
{/* <TabsBody> */}
|
].map(({ title }, index) => (
|
||||||
{/* TODO: Add content */}
|
<Tabs.Trigger value={title} key={index}>
|
||||||
{/* </TabsBody> */}
|
{title}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
))}
|
||||||
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { Button, Typography } from '@material-tailwind/react';
|
|||||||
|
|
||||||
import { DeployStep, DeployStatus } from './DeployStep';
|
import { DeployStep, DeployStatus } from './DeployStep';
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
import ConfirmDialog from '../../shared/ConfirmDialog';
|
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||||
|
|
||||||
const INTERVAL_DURATION = 5000;
|
const TIMEOUT_DURATION = 5000;
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const projectId = searchParams.get('projectId');
|
const projectId = searchParams.get('projectId');
|
||||||
@@ -25,7 +25,7 @@ const Deploy = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timerID = setTimeout(() => {
|
const timerID = setTimeout(() => {
|
||||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||||
}, INTERVAL_DURATION);
|
}, TIMEOUT_DURATION);
|
||||||
|
|
||||||
return () => clearInterval(timerID);
|
return () => clearInterval(timerID);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -43,7 +43,12 @@ const Deploy = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleOpen} variant="outlined" size="sm">
|
<Button
|
||||||
|
onClick={handleOpen}
|
||||||
|
variant="outlined"
|
||||||
|
size="sm"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
^ Cancel
|
^ Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +60,7 @@ const Deploy = () => {
|
|||||||
handleConfirm={handleCancel}
|
handleConfirm={handleCancel}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
This will halt the deployment and you will have to start the process
|
This will halt the deployment and you will have to start the process
|
||||||
from scratch.
|
from scratch.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ const DeployStep = ({
|
|||||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
||||||
{processLogs.map((log, key) => {
|
{processLogs.map((log, key) => {
|
||||||
return (
|
return (
|
||||||
<Typography variant="small" color="gray" key={key}>
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="gray"
|
||||||
|
key={key}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
{log}
|
{log}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@@ -75,6 +80,7 @@ const DeployStep = ({
|
|||||||
toast.success('Logs copied');
|
toast.success('Logs copied');
|
||||||
}}
|
}}
|
||||||
color="blue"
|
color="blue"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Copy log
|
^ Copy log
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
|||||||
import { relativeTimeISO } from '../../../utils/time';
|
import { relativeTimeISO } from '../../../utils/time';
|
||||||
import { GitRepositoryDetails } from '../../../types';
|
import { GitRepositoryDetails } from '../../../types';
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
|
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
interface ProjectRepoCardProps {
|
interface ProjectRepoCardProps {
|
||||||
repository: GitRepositoryDetails;
|
repository: GitRepositoryDetails;
|
||||||
@@ -47,16 +48,18 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
|||||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
||||||
onClick={createProject}
|
onClick={createProject}
|
||||||
>
|
>
|
||||||
<div>^</div>
|
<div className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
|
||||||
|
<GithubIcon />
|
||||||
|
</div>
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-black">{repository.full_name}</span>
|
<span className="text-black">{repository.full_name}</span>
|
||||||
{repository.visibility === 'private' && (
|
{repository.visibility === 'private' && (
|
||||||
<Chip
|
<Chip
|
||||||
className="normal-case inline ml-6 font-normal"
|
className="normal-case inline ml-6 font-normal text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
|
||||||
size="sm"
|
size="sm"
|
||||||
value="Private"
|
value="Private"
|
||||||
icon={'^'}
|
icon={<LockIcon />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +69,9 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
|||||||
<Spinner className="h-4 w-4" />
|
<Spinner className="h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
<div className="hidden group-hover:block">
|
<div className="hidden group-hover:block">
|
||||||
<IconButton size="sm">{'>'}</IconButton>
|
<IconButton size="sm" placeholder={''}>
|
||||||
|
{'>'}
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SearchBar from '../../SearchBar';
|
|||||||
import ProjectRepoCard from './ProjectRepoCard';
|
import ProjectRepoCard from './ProjectRepoCard';
|
||||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
||||||
import AsyncSelect from '../../shared/AsyncSelect';
|
import AsyncSelect from '../../shared/AsyncSelect';
|
||||||
|
import { GithubIcon } from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
const DEFAULT_SEARCHED_REPO = '';
|
const DEFAULT_SEARCHED_REPO = '';
|
||||||
const REPOS_PER_PAGE = 5;
|
const REPOS_PER_PAGE = 5;
|
||||||
@@ -108,7 +109,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2 items-center">
|
||||||
<div className="basis-1/3">
|
<div className="basis-1/3">
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
@@ -116,12 +117,14 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
>
|
>
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
<Option key={account.id} value={account.login}>
|
<Option key={account.id} value={account.login}>
|
||||||
^ {account.login}
|
<div className="flex items-center gap-2 justify-start">
|
||||||
|
<GithubIcon /> {account.login}
|
||||||
|
</div>
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</AsyncSelect>
|
</AsyncSelect>
|
||||||
</div>
|
</div>
|
||||||
<div className="basis-2/3">
|
<div className="basis-2/3 flex-grow flex items-center">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
value={searchedRepo}
|
value={searchedRepo}
|
||||||
onChange={(event) => setSearchedRepo(event.target.value)}
|
onChange={(event) => setSearchedRepo(event.target.value)}
|
||||||
@@ -136,11 +139,12 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="mt-4 p-6 flex items-center justify-center">
|
<div className="mt-4 p-6 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Typography>No repository found</Typography>
|
<Typography placeholder={''}>No repository found</Typography>
|
||||||
<Button
|
<Button
|
||||||
className="rounded-full mt-5"
|
className="rounded-full mt-5"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleResetFilters}
|
onClick={handleResetFilters}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Reset filters
|
^ Reset filters
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ interface TemplateCardProps {
|
|||||||
const CardDetails = ({ template }: { template: TemplateDetails }) => {
|
const CardDetails = ({ template }: { template: TemplateDetails }) => {
|
||||||
return (
|
return (
|
||||||
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
|
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
|
||||||
<Typography className="grow">
|
<Typography className="grow" placeholder={''}>
|
||||||
{template.icon} {template.name}
|
{template.icon} {template.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div>
|
<div>
|
||||||
<IconButton size="sm" className="rounded-full hidden group-hover:block">
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full hidden group-hover:block"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
{'>'}
|
{'>'}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,20 +13,29 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
|
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
|
||||||
<div className="w-8">
|
<div className="w-8">
|
||||||
<Avatar src={activity.author?.avatar_url} variant="rounded" size="sm" />
|
<Avatar
|
||||||
|
src={activity.author?.avatar_url}
|
||||||
|
variant="rounded"
|
||||||
|
size="sm"
|
||||||
|
placeholder={''}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Typography>{activity.commit.author?.name}</Typography>
|
<Typography placeholder={''}>{activity.commit.author?.name}</Typography>
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray" placeholder={''}>
|
||||||
{relativeTimeISO(activity.commit.author!.date!)} ^{' '}
|
{relativeTimeISO(activity.commit.author!.date!)} ^{' '}
|
||||||
{activity.branch.name}
|
{activity.branch.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray" placeholder={''}>
|
||||||
{activity.commit.message}
|
{activity.commit.message}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2 self-center hidden group-hover:block">
|
<div className="mr-2 self-center hidden group-hover:block">
|
||||||
<IconButton size="sm" className="rounded-full bg-gray-600">
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full bg-gray-600"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
{'>'}
|
{'>'}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-4
@@ -17,9 +17,9 @@ interface AssignDomainProps {
|
|||||||
|
|
||||||
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} handler={handleOpen}>
|
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||||
<DialogHeader>Assign Domain</DialogHeader>
|
<DialogHeader placeholder={''}>Assign Domain</DialogHeader>
|
||||||
<DialogBody>
|
<DialogBody placeholder={''}>
|
||||||
In order to assign a domain to your production deployments, configure it
|
In order to assign a domain to your production deployments, configure it
|
||||||
in the{' '}
|
in the{' '}
|
||||||
{/* TODO: Fix selection of project settings tab on navigation to domains */}
|
{/* TODO: Fix selection of project settings tab on navigation to domains */}
|
||||||
@@ -36,12 +36,13 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
|||||||
theme={atomOneLight}
|
theme={atomOneLight}
|
||||||
/>
|
/>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter className="flex justify-start">
|
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||||
<Button
|
<Button
|
||||||
className="rounded-3xl"
|
className="rounded-3xl"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
color="blue"
|
color="blue"
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
<span>Okay</span>
|
<span>Okay</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+31
-16
@@ -93,10 +93,12 @@ const DeploymentDetailsCard = ({
|
|||||||
<div className="col-span-3">
|
<div className="col-span-3">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{deployment.url && (
|
{deployment.url && (
|
||||||
<Typography className=" basis-3/4">{deployment.url}</Typography>
|
<Typography className="basis-3/4" placeholder={''}>
|
||||||
|
{deployment.url}
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Typography color="gray">
|
<Typography color="gray" placeholder={''}>
|
||||||
{deployment.environment === Environment.Production
|
{deployment.environment === Environment.Production
|
||||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||||
: 'Preview'}
|
: 'Preview'}
|
||||||
@@ -111,14 +113,16 @@ const DeploymentDetailsCard = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Typography color="gray">^ {deployment.branch}</Typography>
|
<Typography color="gray" placeholder={''}>
|
||||||
<Typography color="gray">
|
^ {deployment.branch}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="gray" placeholder={''}>
|
||||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||||
{deployment.commitMessage}
|
{deployment.commitMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-center">
|
<div className="col-span-2 flex items-center">
|
||||||
<Typography color="gray" className="grow">
|
<Typography color="gray" className="grow" placeholder={''}>
|
||||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||||
<Tooltip content={deployment.createdBy.name}>
|
<Tooltip content={deployment.createdBy.name}>
|
||||||
{formatAddress(deployment.createdBy.name ?? '')}
|
{formatAddress(deployment.createdBy.name ?? '')}
|
||||||
@@ -128,18 +132,22 @@ const DeploymentDetailsCard = ({
|
|||||||
<MenuHandler>
|
<MenuHandler>
|
||||||
<button className="self-start">...</button>
|
<button className="self-start">...</button>
|
||||||
</MenuHandler>
|
</MenuHandler>
|
||||||
<MenuList>
|
<MenuList placeholder={''}>
|
||||||
<a href={deployment.url} target="_blank" rel="noreferrer">
|
<a href={deployment.url} target="_blank" rel="noreferrer">
|
||||||
<MenuItem disabled={!Boolean(deployment.url)}>^ Visit</MenuItem>
|
<MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
|
||||||
|
^ Visit
|
||||||
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Assign domain
|
^ Assign domain
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||||
disabled={!(deployment.environment !== Environment.Production)}
|
disabled={!(deployment.environment !== Environment.Production)}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Change to production
|
^ Change to production
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -152,6 +160,7 @@ const DeploymentDetailsCard = ({
|
|||||||
deployment.isCurrent
|
deployment.isCurrent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Redeploy to production
|
^ Redeploy to production
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -162,6 +171,7 @@ const DeploymentDetailsCard = ({
|
|||||||
deployment.environment !== Environment.Production ||
|
deployment.environment !== Environment.Production ||
|
||||||
!Boolean(currentDeployment)
|
!Boolean(currentDeployment)
|
||||||
}
|
}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Rollback to this version
|
^ Rollback to this version
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -180,17 +190,22 @@ const DeploymentDetailsCard = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Upon confirmation, this deployment will be changed to production.
|
Upon confirmation, this deployment will be changed to production.
|
||||||
</Typography>
|
</Typography>
|
||||||
<DeploymentDialogBodyCard deployment={deployment} />
|
<DeploymentDialogBodyCard deployment={deployment} />
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
The new deployment will be associated with these domains:
|
The new deployment will be associated with these domains:
|
||||||
</Typography>
|
</Typography>
|
||||||
{prodBranchDomains.length > 0 &&
|
{prodBranchDomains.length > 0 &&
|
||||||
prodBranchDomains.map((value) => {
|
prodBranchDomains.map((value) => {
|
||||||
return (
|
return (
|
||||||
<Typography variant="small" color="blue" key={value.id}>
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue"
|
||||||
|
key={value.id}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
^ {value.name}
|
^ {value.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@@ -209,16 +224,16 @@ const DeploymentDetailsCard = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Upon confirmation, new deployment will be created with the same
|
Upon confirmation, new deployment will be created with the same
|
||||||
source code as current deployment.
|
source code as current deployment.
|
||||||
</Typography>
|
</Typography>
|
||||||
<DeploymentDialogBodyCard deployment={deployment} />
|
<DeploymentDialogBodyCard deployment={deployment} />
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
These domains will point to your new deployment:
|
These domains will point to your new deployment:
|
||||||
</Typography>
|
</Typography>
|
||||||
{deployment.domain?.name && (
|
{deployment.domain?.name && (
|
||||||
<Typography variant="small" color="blue">
|
<Typography variant="small" color="blue" placeholder={''}>
|
||||||
{deployment.domain?.name}
|
{deployment.domain?.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -237,7 +252,7 @@ const DeploymentDetailsCard = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Upon confirmation, this deployment will replace your current
|
Upon confirmation, this deployment will replace your current
|
||||||
deployment
|
deployment
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -255,10 +270,10 @@ const DeploymentDetailsCard = ({
|
|||||||
color: 'orange',
|
color: 'orange',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
These domains will point to your new deployment:
|
These domains will point to your new deployment:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small" color="blue">
|
<Typography variant="small" color="blue" placeholder={''}>
|
||||||
^ {currentDeployment.domain?.name}
|
^ {currentDeployment.domain?.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+4
-4
@@ -20,7 +20,7 @@ const DeploymentDialogBodyCard = ({
|
|||||||
deployment,
|
deployment,
|
||||||
}: DeploymentDialogBodyCardProps) => {
|
}: DeploymentDialogBodyCardProps) => {
|
||||||
return (
|
return (
|
||||||
<Card className="p-2 shadow-none">
|
<Card className="p-2 shadow-none" placeholder={''}>
|
||||||
{chip && (
|
{chip && (
|
||||||
<Chip
|
<Chip
|
||||||
className={`w-fit normal-case font-normal`}
|
className={`w-fit normal-case font-normal`}
|
||||||
@@ -30,16 +30,16 @@ const DeploymentDialogBodyCard = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{deployment.url && (
|
{deployment.url && (
|
||||||
<Typography variant="small" className="text-black">
|
<Typography variant="small" className="text-black" placeholder={''}>
|
||||||
{deployment.url}
|
{deployment.url}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
^ {deployment.branch} ^{' '}
|
^ {deployment.branch} ^{' '}
|
||||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||||
{deployment.commitMessage}
|
{deployment.commitMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||||
{formatAddress(deployment.createdBy.name ?? '')}
|
{formatAddress(deployment.createdBy.name ?? '')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
|||||||
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
|
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
X
|
X
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
+7
-2
@@ -21,7 +21,9 @@ const AddEnvironmentVariableRow = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-1 p-2">
|
<div className="flex gap-1 p-2">
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="small">Key</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Key
|
||||||
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
{...register(`variables.${index}.key`, {
|
{...register(`variables.${index}.key`, {
|
||||||
@@ -30,7 +32,9 @@ const AddEnvironmentVariableRow = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="small">Value</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Value
|
||||||
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
{...register(`variables.${index}.value`, {
|
{...register(`variables.${index}.value`, {
|
||||||
@@ -43,6 +47,7 @@ const AddEnvironmentVariableRow = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onDelete()}
|
onClick={() => onDelete()}
|
||||||
disabled={isDeleteDisabled}
|
disabled={isDeleteDisabled}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{'>'}
|
{'>'}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -61,23 +61,26 @@ const AddMemberDialog = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} handler={handleOpen}>
|
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||||
<DialogHeader className="flex justify-between">
|
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||||
<div>Add member</div>
|
<div>Add member</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
className="mr-1 rounded-3xl"
|
className="mr-1 rounded-3xl"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
X
|
X
|
||||||
</Button>
|
</Button>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit(submitHandler)}>
|
<form onSubmit={handleSubmit(submitHandler)}>
|
||||||
<DialogBody className="flex flex-col gap-2 p-4">
|
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
We will send an invitation link to this email address.
|
We will send an invitation link to this email address.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small">Email address</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Email address
|
||||||
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
@@ -85,8 +88,10 @@ const AddMemberDialog = ({
|
|||||||
required: 'email field cannot be empty',
|
required: 'email field cannot be empty',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Typography variant="small">Permissions</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
<Typography variant="small">
|
Permissions
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
You can change this later if required.
|
You can change this later if required.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -102,8 +107,13 @@ const AddMemberDialog = ({
|
|||||||
color="blue"
|
color="blue"
|
||||||
/>
|
/>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter className="flex justify-start">
|
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleOpen}
|
||||||
|
className="mr-1"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -111,6 +121,7 @@ const AddMemberDialog = ({
|
|||||||
color="blue"
|
color="blue"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
Send invite
|
Send invite
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -53,20 +53,21 @@ const DeleteProjectDialog = ({
|
|||||||
}, [client, project, handleOpen]);
|
}, [client, project, handleOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} handler={handleOpen}>
|
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||||
<DialogHeader className="flex justify-between">
|
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||||
<div>Delete project?</div>
|
<div>Delete project?</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
className="mr-1 rounded-3xl"
|
className="mr-1 rounded-3xl"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
X
|
X
|
||||||
</Button>
|
</Button>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||||
<DialogBody className="flex flex-col gap-2">
|
<DialogBody className="flex flex-col gap-2" placeholder={''}>
|
||||||
<Typography variant="paragraph">
|
<Typography variant="paragraph" placeholder={''}>
|
||||||
Deleting your project is irreversible. Enter your project’s
|
Deleting your project is irreversible. Enter your project’s
|
||||||
name
|
name
|
||||||
<span className="bg-blue-100 text-blue-700">({project.name})</span>
|
<span className="bg-blue-100 text-blue-700">({project.name})</span>
|
||||||
@@ -80,12 +81,17 @@ const DeleteProjectDialog = ({
|
|||||||
validate: (value) => value === project.name,
|
validate: (value) => value === project.name,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Typography variant="small" color="red">
|
<Typography variant="small" color="red" placeholder={''}>
|
||||||
^ Deleting your project is irreversible.
|
^ Deleting your project is irreversible.
|
||||||
</Typography>
|
</Typography>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter className="flex justify-start">
|
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleOpen}
|
||||||
|
className="mr-1"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -93,6 +99,7 @@ const DeleteProjectDialog = ({
|
|||||||
color="red"
|
color="red"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
Yes, Delete project
|
Yes, Delete project
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+4
-5
@@ -1,9 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
|
|
||||||
|
|
||||||
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
||||||
|
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||||
|
|
||||||
interface DisplayEnvironmentVariablesProps {
|
interface DisplayEnvironmentVariablesProps {
|
||||||
environment: Environment;
|
environment: Environment;
|
||||||
@@ -30,11 +29,11 @@ const DisplayEnvironmentVariables = ({
|
|||||||
</div>
|
</div>
|
||||||
<Collapse open={openCollapse}>
|
<Collapse open={openCollapse}>
|
||||||
{variables.length === 0 ? (
|
{variables.length === 0 ? (
|
||||||
<Card className="bg-gray-300 flex items-center p-4">
|
<Card className="bg-gray-300 flex items-center p-4" placeholder={''}>
|
||||||
<Typography variant="small" className="text-black">
|
<Typography variant="small" className="text-black" placeholder={''}>
|
||||||
No environment variables added yet.
|
No environment variables added yet.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Once you add them, they’ll show up here.
|
Once you add them, they’ll show up here.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const DomainCard = ({
|
|||||||
<>
|
<>
|
||||||
<div className="flex justify-between py-3">
|
<div className="flex justify-between py-3">
|
||||||
<div className="flex justify-start gap-1">
|
<div className="flex justify-start gap-1">
|
||||||
<Typography variant="h6">
|
<Typography variant="h6" placeholder={''}>
|
||||||
<i>^</i> {domain.name}
|
<i>^</i> {domain.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
@@ -97,18 +97,20 @@ const DomainCard = ({
|
|||||||
<MenuHandler>
|
<MenuHandler>
|
||||||
<button className="border-2 rounded-full w-8 h-8">...</button>
|
<button className="border-2 rounded-full w-8 h-8">...</button>
|
||||||
</MenuHandler>
|
</MenuHandler>
|
||||||
<MenuList>
|
<MenuList placeholder={''}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="text-black"
|
className="text-black"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditDialogOpen((preVal) => !preVal);
|
setEditDialogOpen((preVal) => !preVal);
|
||||||
}}
|
}}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Edit domain
|
^ Edit domain
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="text-red-500"
|
className="text-red-500"
|
||||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Delete domain
|
^ Delete domain
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -127,7 +129,7 @@ const DomainCard = ({
|
|||||||
}}
|
}}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Once deleted, the project{' '}
|
Once deleted, the project{' '}
|
||||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||||
{project.name}
|
{project.name}
|
||||||
@@ -140,15 +142,21 @@ const DomainCard = ({
|
|||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Typography variant="small">Production</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Production
|
||||||
|
</Typography>
|
||||||
{domain.status === DomainStatus.Pending && (
|
{domain.status === DomainStatus.Pending && (
|
||||||
<Card className="bg-gray-200 p-4 text-sm">
|
<Card className="bg-gray-200 p-4 text-sm" placeholder={''}>
|
||||||
{refreshStatus === RefreshStatus.IDLE ? (
|
{refreshStatus === RefreshStatus.IDLE ? (
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
^ Add these records to your domain and refresh to check
|
^ Add these records to your domain and refresh to check
|
||||||
</Typography>
|
</Typography>
|
||||||
) : refreshStatus === RefreshStatus.CHECKING ? (
|
) : refreshStatus === RefreshStatus.CHECKING ? (
|
||||||
<Typography variant="small" className="text-blue-500">
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="text-blue-500"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
^ Checking records for {domain.name}
|
^ Checking records for {domain.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -122,27 +122,32 @@ const EditDomainDialog = ({
|
|||||||
}, [domain]);
|
}, [domain]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} handler={handleOpen}>
|
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||||
<DialogHeader className="flex justify-between">
|
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||||
<div>Edit domain</div>
|
<div>Edit domain</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
className="mr-1 rounded-3xl"
|
className="mr-1 rounded-3xl"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
X
|
X
|
||||||
</Button>
|
</Button>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||||
<DialogBody className="flex flex-col gap-2 p-4">
|
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||||
<Typography variant="small">Domain name</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Domain name
|
||||||
|
</Typography>
|
||||||
<Input crossOrigin={undefined} {...register('name')} />
|
<Input crossOrigin={undefined} {...register('name')} />
|
||||||
<Typography variant="small">Redirect to</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Redirect to
|
||||||
|
</Typography>
|
||||||
<Controller
|
<Controller
|
||||||
name="redirectedTo"
|
name="redirectedTo"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Select {...field} disabled={isDisableDropdown}>
|
<Select {...field} disabled={isDisableDropdown} placeholder={''}>
|
||||||
{redirectOptions.map((option, key) => (
|
{redirectOptions.map((option, key) => (
|
||||||
<Option key={key} value={option}>
|
<Option key={key} value={option}>
|
||||||
^ {option}
|
^ {option}
|
||||||
@@ -154,14 +159,16 @@ const EditDomainDialog = ({
|
|||||||
{isDisableDropdown && (
|
{isDisableDropdown && (
|
||||||
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
||||||
<div>^</div>
|
<div>^</div>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Domain “{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
Domain “{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
||||||
redirects to this domain so you can not redirect this doman
|
redirects to this domain so you can not redirect this doman
|
||||||
further.
|
further.
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Typography variant="small">Git branch</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Git branch
|
||||||
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
{...register('branch', {
|
{...register('branch', {
|
||||||
@@ -174,13 +181,22 @@ const EditDomainDialog = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{!isValid && (
|
{!isValid && (
|
||||||
<Typography variant="small" className="text-red-500">
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="text-red-500"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
We couldn't find this branch in the connected Git repository.
|
We couldn't find this branch in the connected Git repository.
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter className="flex justify-start">
|
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleOpen}
|
||||||
|
className="mr-1"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -188,6 +204,7 @@ const EditDomainDialog = ({
|
|||||||
color="blue"
|
color="blue"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty}
|
disabled={!isDirty}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
Save changes
|
Save changes
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+11
-3
@@ -84,7 +84,9 @@ const EditEnvironmentVariableRow = ({
|
|||||||
<>
|
<>
|
||||||
<div className="flex gap-1 p-2">
|
<div className="flex gap-1 p-2">
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="small">Key</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Key
|
||||||
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
disabled={!edit}
|
disabled={!edit}
|
||||||
@@ -92,7 +94,9 @@ const EditEnvironmentVariableRow = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="small">Value</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Value
|
||||||
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
disabled={!edit}
|
disabled={!edit}
|
||||||
@@ -114,6 +118,7 @@ const EditEnvironmentVariableRow = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleSubmit(updateEnvironmentVariableHandler)}
|
onClick={handleSubmit(updateEnvironmentVariableHandler)}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{'S'}
|
{'S'}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -125,6 +130,7 @@ const EditEnvironmentVariableRow = ({
|
|||||||
reset();
|
reset();
|
||||||
setEdit((preVal) => !preVal);
|
setEdit((preVal) => !preVal);
|
||||||
}}
|
}}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{'C'}
|
{'C'}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -138,6 +144,7 @@ const EditEnvironmentVariableRow = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEdit((preVal) => !preVal);
|
setEdit((preVal) => !preVal);
|
||||||
}}
|
}}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{'E'}
|
{'E'}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -146,6 +153,7 @@ const EditEnvironmentVariableRow = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{'D'}
|
{'D'}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -162,7 +170,7 @@ const EditEnvironmentVariableRow = ({
|
|||||||
handleConfirm={removeEnvironmentVariableHandler}
|
handleConfirm={removeEnvironmentVariableHandler}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Are you sure you want to delete the variable
|
Are you sure you want to delete the variable
|
||||||
<span className="bg-blue-100">{variable.key}</span>?
|
<span className="bg-blue-100">{variable.key}</span>?
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ const MemberCard = ({
|
|||||||
selected={(_, index) => (
|
selected={(_, index) => (
|
||||||
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
||||||
)}
|
)}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{DROPDOWN_OPTIONS.map((permission, key) => (
|
{DROPDOWN_OPTIONS.map((permission, key) => (
|
||||||
<Option key={key} value={permission.value}>
|
<Option key={key} value={permission.value}>
|
||||||
@@ -132,6 +133,7 @@ const MemberCard = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
||||||
}}
|
}}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
D
|
D
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -152,7 +154,7 @@ const MemberCard = ({
|
|||||||
}}
|
}}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Once removed, {formatAddress(member.name ?? '')} (
|
Once removed, {formatAddress(member.name ?? '')} (
|
||||||
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
||||||
this project.
|
this project.
|
||||||
|
|||||||
@@ -17,14 +17,19 @@ const RepoConnectedSection = ({
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div>^</div>
|
<div>^</div>
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Typography variant="small">{linkedRepo.full_name}</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
<Typography variant="small">Connected just now</Typography>
|
{linkedRepo.full_name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Connected just now
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setDisconnectRepoDialogOpen(true)}
|
onClick={() => setDisconnectRepoDialogOpen(true)}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
^ Disconnect
|
^ Disconnect
|
||||||
</Button>
|
</Button>
|
||||||
@@ -39,7 +44,7 @@ const RepoConnectedSection = ({
|
|||||||
}}
|
}}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Any data tied to your Git project may become misconfigured. Are you
|
Any data tied to your Git project may become misconfigured. Are you
|
||||||
sure you want to continue?
|
sure you want to continue?
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -54,14 +54,18 @@ const SetupDomain = () => {
|
|||||||
className="flex flex-col gap-6 w-full"
|
className="flex flex-col gap-6 w-full"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h5">Setup domain name</Typography>
|
<Typography variant="h5" placeholder={''}>
|
||||||
<Typography variant="small">
|
Setup domain name
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
Add your domain and setup redirects
|
Add your domain and setup redirects
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-auto">
|
<div className="w-auto">
|
||||||
<Typography variant="small">Domain name</Typography>
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Domain name
|
||||||
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -76,7 +80,7 @@ const SetupDomain = () => {
|
|||||||
|
|
||||||
{isValid && (
|
{isValid && (
|
||||||
<div>
|
<div>
|
||||||
<Typography>Primary domain</Typography>
|
<Typography placeholder={''}>Primary domain</Typography>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Radio
|
<Radio
|
||||||
label={domainStr}
|
label={domainStr}
|
||||||
@@ -108,6 +112,7 @@ const SetupDomain = () => {
|
|||||||
className="w-fit"
|
className="w-fit"
|
||||||
color={isValid ? 'blue' : 'gray'}
|
color={isValid ? 'blue' : 'gray'}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
<i>^</i> Next
|
<i>^</i> Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
|||||||
navigator.clipboard.writeText(webhookUrl);
|
navigator.clipboard.writeText(webhookUrl);
|
||||||
toast.success('Copied to clipboard');
|
toast.success('Copied to clipboard');
|
||||||
}}
|
}}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
C
|
C
|
||||||
</Button>
|
</Button>
|
||||||
@@ -32,6 +33,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
X
|
X
|
||||||
</Button>
|
</Button>
|
||||||
@@ -48,7 +50,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
|||||||
}}
|
}}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
Are you sure you want to delete the variable{' '}
|
Are you sure you want to delete the variable{' '}
|
||||||
<span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
|
<span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const AsyncSelect = React.forwardRef((props: SelectProps, ref: any) => {
|
|||||||
|
|
||||||
useEffect(() => setKey((preVal) => preVal + 1), [props]);
|
useEffect(() => setKey((preVal) => preVal + 1), [props]);
|
||||||
|
|
||||||
return <Select key={key} ref={ref} {...props} />;
|
return <Select key={key} ref={ref} {...props} placeholder={''} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
AsyncSelect.displayName = 'AsyncSelect';
|
AsyncSelect.displayName = 'AsyncSelect';
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const avatarTheme = tv(
|
||||||
|
{
|
||||||
|
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||||
|
slots: {
|
||||||
|
image: [
|
||||||
|
'h-full',
|
||||||
|
'w-full',
|
||||||
|
'rounded-[inherit]',
|
||||||
|
'object-cover',
|
||||||
|
'object-center',
|
||||||
|
],
|
||||||
|
fallback: [
|
||||||
|
'grid',
|
||||||
|
'select-none',
|
||||||
|
'place-content-center',
|
||||||
|
'h-full',
|
||||||
|
'w-full',
|
||||||
|
'rounded-[inherit]',
|
||||||
|
'font-medium',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
type: {
|
||||||
|
gray: {
|
||||||
|
fallback: ['text-elements-highEm', 'bg-base-bg-emphasized'],
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'],
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
fallback: ['text-elements-info', 'bg-base-bg-emphasized-info'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
18: {
|
||||||
|
base: ['rounded-md', 'h-[18px]', 'w-[18px]', 'text-[0.625rem]'],
|
||||||
|
},
|
||||||
|
20: {
|
||||||
|
base: ['rounded-md', 'h-5', 'w-5', 'text-[0.625rem]'],
|
||||||
|
},
|
||||||
|
24: {
|
||||||
|
base: ['rounded-md', 'h-6', 'w-6', 'text-[0.625rem]'],
|
||||||
|
},
|
||||||
|
28: {
|
||||||
|
base: ['rounded-lg', 'h-[28px]', 'w-[28px]', 'text-[0.625rem]'],
|
||||||
|
},
|
||||||
|
32: {
|
||||||
|
base: ['rounded-lg', 'h-8', 'w-8', 'text-xs'],
|
||||||
|
},
|
||||||
|
36: {
|
||||||
|
base: ['rounded-xl', 'h-[36px]', 'w-[36px]', 'text-xs'],
|
||||||
|
},
|
||||||
|
40: {
|
||||||
|
base: ['rounded-xl', 'h-10', 'w-10', 'text-sm'],
|
||||||
|
},
|
||||||
|
44: {
|
||||||
|
base: ['rounded-xl', 'h-[44px]', 'w-[44px]', 'text-sm'],
|
||||||
|
},
|
||||||
|
48: {
|
||||||
|
base: ['rounded-xl', 'h-[48px]', 'w-[48px]', 'text-sm'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 24,
|
||||||
|
type: 'gray',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ responsiveVariants: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
export type AvatarVariants = VariantProps<typeof avatarTheme>;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { type ComponentPropsWithoutRef, type ComponentProps } from 'react';
|
||||||
|
import { avatarTheme, type AvatarVariants } from './Avatar.theme';
|
||||||
|
import * as PrimitiveAvatar from '@radix-ui/react-avatar';
|
||||||
|
|
||||||
|
export type AvatarProps = ComponentPropsWithoutRef<'div'> & {
|
||||||
|
imageSrc?: string | null;
|
||||||
|
initials?: string;
|
||||||
|
imageProps?: ComponentProps<typeof PrimitiveAvatar.Image>;
|
||||||
|
fallbackProps?: ComponentProps<typeof PrimitiveAvatar.Fallback>;
|
||||||
|
} & AvatarVariants;
|
||||||
|
|
||||||
|
export const Avatar = ({
|
||||||
|
className,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
imageSrc,
|
||||||
|
imageProps,
|
||||||
|
fallbackProps,
|
||||||
|
initials,
|
||||||
|
}: AvatarProps) => {
|
||||||
|
const { base, image, fallback } = avatarTheme({ size, type });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrimitiveAvatar.Root className={base({ className })}>
|
||||||
|
{imageSrc && (
|
||||||
|
<PrimitiveAvatar.Image
|
||||||
|
{...imageProps}
|
||||||
|
className={image({ className: imageProps?.className })}
|
||||||
|
src={imageSrc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<PrimitiveAvatar.Fallback asChild {...fallbackProps}>
|
||||||
|
<div className={fallback({ className: fallbackProps?.className })}>
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
</PrimitiveAvatar.Fallback>
|
||||||
|
</PrimitiveAvatar.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './Avatar';
|
||||||
|
export * from './Avatar.theme';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const badgeTheme = tv({
|
||||||
|
slots: {
|
||||||
|
wrapper: ['rounded-full', 'grid', 'place-content-center'],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
primary: {
|
||||||
|
wrapper: ['bg-controls-primary', 'text-elements-on-primary'],
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
wrapper: ['bg-controls-secondary', 'text-elements-on-secondary'],
|
||||||
|
},
|
||||||
|
tertiary: {
|
||||||
|
wrapper: [
|
||||||
|
'bg-controls-tertiary',
|
||||||
|
'border',
|
||||||
|
'border-border-interactive/10',
|
||||||
|
'text-elements-high-em',
|
||||||
|
'shadow-button',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
inset: {
|
||||||
|
wrapper: ['bg-controls-inset', 'text-elements-high-em'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: {
|
||||||
|
wrapper: ['h-5', 'w-5', 'text-xs'],
|
||||||
|
},
|
||||||
|
xs: {
|
||||||
|
wrapper: ['h-4', 'w-4', 'text-2xs', 'font-medium'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeTheme = VariantProps<typeof badgeTheme>;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { BadgeTheme, badgeTheme } from './Badge.theme';
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends ComponentPropsWithoutRef<'div'>,
|
||||||
|
BadgeTheme {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A badge is a small status descriptor for UI elements.
|
||||||
|
* It can be used to indicate a status, a count, or a category.
|
||||||
|
* It is typically used in lists, tables, or navigation elements.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <Badge variant="primary" size="sm">1</Badge
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const Badge = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: BadgeProps) => {
|
||||||
|
const { wrapper } = badgeTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props} className={wrapper({ className, variant, size })}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './Badge';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user