forked from cerc-io/snowballtools-base
Compare commits
352
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f25059c3c | ||
|
|
f38dfb5604 | ||
|
|
351db16336 | ||
|
|
78f04c3669 | ||
|
|
44a6a8902f | ||
|
|
4fcf8e92b7 | ||
|
|
193dbb058a | ||
|
|
8834de893e | ||
|
|
9b3ac4654f | ||
|
|
e4fdae3329 | ||
|
|
4c936b1eb7 | ||
|
|
f5807c1126 | ||
|
|
c1696fbf48 | ||
|
|
45e8e9a7f4 | ||
|
|
28740ffbee | ||
|
|
97289d85a3 | ||
|
|
f3ce0d0621 | ||
|
|
c2417a9daa | ||
|
|
d50a318e16 | ||
|
|
3674750011 | ||
|
|
102c861617 | ||
|
|
56e9be59ad | ||
|
|
300b8e4b5e | ||
|
|
296e149391 | ||
|
|
cbda1cc652 | ||
|
|
16e7b22507 | ||
|
|
93fb696d5c | ||
|
|
32305cedbc | ||
|
|
519618b456 | ||
|
|
4057480df2 | ||
|
|
de197759de | ||
|
|
65b66fe383 | ||
|
|
c6ea3a8f53 | ||
|
|
6d861c71cc | ||
|
|
1d96f6430f | ||
|
|
b922e632bf | ||
|
|
b1f318bbbd | ||
|
|
667deb78fb | ||
|
|
095008867f | ||
|
|
41033c5241 | ||
|
|
cdb995205a | ||
|
|
3a84832da5 | ||
|
|
3fdc0b2dff | ||
|
|
89da03cec6 | ||
|
|
9b6cd9baae | ||
|
|
658cc0b6b2 | ||
|
|
be8f0bc4ad | ||
|
|
2a8fc30979 | ||
|
|
4074dd4001 | ||
|
|
028831f806 | ||
|
|
646099707c | ||
|
|
6b0548ec47 | ||
|
|
2ed07a6987 | ||
|
|
e9ab034625 | ||
|
|
02500b3cba | ||
|
|
4c8d0e2436 | ||
|
|
efa74898af | ||
|
|
532732943e | ||
|
|
be4900e63f | ||
|
|
9e4e203f5f | ||
|
|
d812cdf05f | ||
|
|
b605c1bc5b | ||
|
|
8d3ef369bb | ||
|
|
7aac8cdcef | ||
|
|
6c31113bca | ||
|
|
30ecd41975 | ||
|
|
410375f8c7 | ||
|
|
226d02ea59 | ||
|
|
ca64d00355 | ||
|
|
287fe360d3 | ||
|
|
f23c757de9 | ||
|
|
3737550f7f | ||
|
|
274762cbfe | ||
|
|
90295fd620 | ||
|
|
b75957929a | ||
|
|
daffcf1203 | ||
|
|
eccf78afdb | ||
|
|
0901036f71 | ||
|
|
8d3ef3bafc | ||
|
|
b6e02fb19d | ||
|
|
462d247a86 | ||
|
|
80097a32ac | ||
|
|
a80d40156f | ||
|
|
ced50bf7f2 | ||
|
|
90b583aa19 | ||
|
|
48e3581322 | ||
|
|
1ddc3b81c7 | ||
|
|
5a86abd133 | ||
|
|
6ddf44cddd | ||
|
|
b03200c256 | ||
|
|
1648deb64f | ||
|
|
621ca8926e | ||
|
|
8c0162f9f3 | ||
|
|
e05e2c63c2 | ||
|
|
3b1f03bcb6 | ||
|
|
80fee1f585 | ||
|
|
d50f559a07 | ||
|
|
46d44378c5 | ||
|
|
3e42899f2e | ||
|
|
75a048c7a9 | ||
|
|
b5f50fe16f | ||
|
|
aa4094669d | ||
|
|
33fbdf60ae | ||
|
|
abe1401a6d | ||
|
|
2182beca22 | ||
|
|
96f519d5e3 | ||
|
|
f6db1e119b | ||
|
|
c079a9c336 | ||
|
|
f9ac778e47 | ||
|
|
e8ae417772 | ||
|
|
18dde52c9a | ||
|
|
d9bae720d7 | ||
|
|
e9f32ff668 | ||
|
|
8a1e84386a | ||
|
|
3335c26f82 | ||
|
|
0a2686d5ec | ||
|
|
c1da92bc99 | ||
|
|
add2559860 | ||
|
|
460f8a80b8 | ||
|
|
f5296e9b8d | ||
|
|
0aeea36dbd | ||
|
|
8f0c39022d | ||
|
|
6c42a22972 | ||
|
|
adb64f2db1 | ||
|
|
64e3aa5b25 | ||
|
|
4fc654f763 | ||
|
|
be0ea904ee | ||
|
|
8114b10932 | ||
|
|
0ee27df25e | ||
|
|
893a499be5 | ||
|
|
596d8eb326 | ||
|
|
e9d38e0d3b | ||
|
|
ab77ce681a | ||
|
|
5be6e14db9 | ||
|
|
409b654f9b | ||
|
|
4a86952810 | ||
|
|
207b32ce65 | ||
|
|
6e653774be | ||
|
|
0b64ccc364 | ||
|
|
a8ad6c6eec | ||
|
|
65f64a3dcd | ||
|
|
d64cf46d89 | ||
|
|
1ff5ab3dfd | ||
|
|
d9392c095d | ||
|
|
2cd1865d19 | ||
|
|
a7810a34c9 | ||
|
|
ff9b08e91d | ||
|
|
94a9bf88a9 | ||
|
|
dd5f93050d | ||
|
|
11aeda9453 | ||
|
|
dfa38a4cba | ||
|
|
638e31f7fe | ||
|
|
5946755749 | ||
|
|
4532a962d8 | ||
|
|
83fec1d273 | ||
|
|
6db5871868 | ||
|
|
e0c5895e9c | ||
|
|
630af612a2 | ||
|
|
87307381cb | ||
|
|
6a108c1a1b | ||
|
|
aa5507f309 | ||
|
|
91a80cac8a | ||
|
|
ee397d0e6e | ||
|
|
98ec1915ca | ||
|
|
f08081932c | ||
|
|
fe427a9fb8 | ||
|
|
a5b4103515 | ||
|
|
f77f7c120a | ||
|
|
22f3cbafb7 | ||
|
|
30c50a3eec | ||
|
|
0381fdbbed | ||
|
|
2f466d4fbb | ||
|
|
ee35e64f69 | ||
|
|
0fb378386a | ||
|
|
563059e887 | ||
|
|
075cec58e5 | ||
|
|
d52a34da35 | ||
|
|
eb4ccfbc9c | ||
|
|
769593913e | ||
|
|
eda2cc76ed | ||
|
|
99eb514306 | ||
|
|
f34eb48d1e | ||
|
|
acf5aab26f | ||
|
|
6b6582c287 | ||
|
|
b43ee3b7bb | ||
|
|
b1bf47d104 | ||
|
|
8ee61c0c85 | ||
|
|
33aa75e341 | ||
|
|
a238610522 | ||
|
|
bd4a51c830 | ||
|
|
4a87c100d6 | ||
|
|
3493d735b9 | ||
|
|
a324d32ebf | ||
|
|
4519494be8 | ||
|
|
319822c8e6 | ||
|
|
c5273ff530 | ||
|
|
2585418ed6 | ||
|
|
62734308fc | ||
|
|
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/
|
||||
yarn-error.log
|
||||
.yarnrc.yml
|
||||
.yarn/
|
||||
.yarnrc
|
||||
|
||||
packages/backend/environments/local.toml
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
]
|
||||
}
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
```bash
|
||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
||||
cd snowballtools-base
|
||||
```
|
||||
|
||||
- In root of the repo, install depedencies
|
||||
- Install dependencies in root
|
||||
|
||||
```bash
|
||||
yarn
|
||||
@@ -20,237 +21,212 @@
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
## Backend
|
||||
|
||||
- Change directory to `packages/backend`
|
||||
|
||||
```bash
|
||||
cd packages/backend
|
||||
```
|
||||
|
||||
- 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)
|
||||
- In "Homepage URL", type `http://localhost:3000`
|
||||
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
### Backend Production
|
||||
|
||||
- Let us assume the following domains for backend and frontend
|
||||
Let us assume the following domains for backend and frontend
|
||||
- Backend server: `api.snowballtools.com`
|
||||
- Frontend app: `dashboard.snowballtools.com`
|
||||
|
||||
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
||||
- Configuration Files
|
||||
|
||||
- For Backend:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
...
|
||||
[server.session]
|
||||
# Secret should be changed to a different random string
|
||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
|
||||
# Set URL of the frontend app
|
||||
appOriginUrl = "https://dashboard.snowballtools.com"
|
||||
# Set to true for session cookies to work behind proxy
|
||||
trustProxy = true
|
||||
# Set empty domain when using secure connection
|
||||
domain = ""
|
||||
```bash
|
||||
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
|
||||
```
|
||||
|
||||
- Production
|
||||
|
||||
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
[server]
|
||||
...
|
||||
[server.session]
|
||||
# Secret should be changed to a different random string
|
||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
|
||||
# Set URL of the frontend app
|
||||
appOriginUrl = "https://dashboard.snowballtools.com"
|
||||
# Set to true for session cookies to work behind proxy
|
||||
trustProxy = true
|
||||
# Set empty domain when using secure connection
|
||||
domain = ""
|
||||
```
|
||||
|
||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
|
||||
- Homepage URL: `https://dashboard.snowballtools.com`
|
||||
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
[gitHub]
|
||||
webhookUrl = "https://api.snowballtools.com"
|
||||
```
|
||||
|
||||
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
# Use actual port for REST endpoint
|
||||
restEndpoint = "http://api.laconic.com:1317"
|
||||
# Use actual port for GQL endpoint
|
||||
gqlEndpoint = "http://api.laconic.com:9473/api"
|
||||
# Set private key of account to be used in Laconicd
|
||||
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
|
||||
# Set Bond ID to be used for publishing records
|
||||
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
|
||||
chainId = "laconic_9000-1"
|
||||
# Set authority that is existing in the chain
|
||||
authority = "laconic"
|
||||
[registryConfig.fee]
|
||||
amount = "200000"
|
||||
denom = "aphoton"
|
||||
gas = "750000"
|
||||
```
|
||||
|
||||
- Development
|
||||
|
||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
|
||||
- Homepage URL: `http://localhost:3000`
|
||||
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
- Setup Laconicd
|
||||
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```bash
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
||||
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
||||
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
||||
```
|
||||
|
||||
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```bash
|
||||
# For registryConfig.restEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
||||
# 0.0.0.0:32777
|
||||
|
||||
# For registryConfig.gqlEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
||||
# 0.0.0.0:32771
|
||||
```
|
||||
|
||||
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
- Run the script to create bond, reserve the authority and set authority bond
|
||||
|
||||
```bash
|
||||
yarn test:registry:init
|
||||
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
||||
```
|
||||
|
||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
- Setup ngrok for GitHub webhooks
|
||||
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
|
||||
- Start ngrok and point to backend server endpoint
|
||||
|
||||
```bash
|
||||
ngrok http http://localhost:8000
|
||||
```
|
||||
|
||||
- Look for the forwarding URL in ngrok
|
||||
|
||||
```bash
|
||||
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
|
||||
```
|
||||
|
||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
[gitHub]
|
||||
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
|
||||
```
|
||||
|
||||
- For Frontend:
|
||||
|
||||
```bash
|
||||
cp packages/frontend/.env.example packages/frontend/.env
|
||||
```
|
||||
|
||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||
```
|
||||
|
||||
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
|
||||
```
|
||||
|
||||
- Production
|
||||
|
||||
- Set the following values in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
|
||||
```
|
||||
|
||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
||||
- Create a project and add information to use wallet connect SDK
|
||||
- Add project name and select project type as `App`
|
||||
- Set project home page URL to `https://dashboard.snowballtools.com`
|
||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
|
||||
```
|
||||
|
||||
- Build the React application
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
- Use a web server for hosting static built files
|
||||
|
||||
```bash
|
||||
python3 -m http.server -d build 3000
|
||||
```
|
||||
|
||||
- Development
|
||||
|
||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
||||
|
||||
```env
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
```
|
||||
|
||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
|
||||
- Create a project and add information to use wallet connect SDK
|
||||
- Add project name and select project type as `App`
|
||||
- Project home page URL is not required to be set
|
||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_WALLET_CONNECT_ID = <Project_ID>
|
||||
```
|
||||
|
||||
- The React application will be running in `http://localhost:3000/`
|
||||
|
||||
## Development
|
||||
|
||||
- Start the backend server
|
||||
|
||||
```bash
|
||||
yarn start:backend
|
||||
```
|
||||
|
||||
- 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)
|
||||
- In "Homepage URL", type `https://dashboard.snowballtools.com`
|
||||
- In "Authorization callback URL", type `https://dashboard.snowballtools.com/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://api.snowballtools.com"
|
||||
...
|
||||
```
|
||||
|
||||
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
...
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
# Use actual port for REST endpoint
|
||||
restEndpoint = "http://api.laconic.com:1317"
|
||||
# Use actual port for GQL endpoint
|
||||
gqlEndpoint = "http://api.laconic.com:9473/api"
|
||||
# Set private key of account to be used in Laconicd
|
||||
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
|
||||
# Set Bond ID to be used for publishing records
|
||||
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
|
||||
chainId = "laconic_9000-1"
|
||||
# Set authority that is existing in the chain
|
||||
authority = "laconic"
|
||||
[registryConfig.fee]
|
||||
amount = "200000"
|
||||
denom = "aphoton"
|
||||
gas = "750000"
|
||||
...
|
||||
```
|
||||
|
||||
- Start the server in `packages/backend`
|
||||
- Start the frontend
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Backend Development
|
||||
|
||||
- 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)
|
||||
- In "Homepage URL", type `http://localhost:3000`
|
||||
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
- Setup Laconicd
|
||||
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```bash
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
||||
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
||||
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
||||
```
|
||||
|
||||
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```bash
|
||||
# For registryConfig.restEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
||||
# 0.0.0.0:32777
|
||||
|
||||
# For registryConfig.gqlEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
||||
# 0.0.0.0:32771
|
||||
```
|
||||
|
||||
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
- Run the script to create bond, reserve the authority and set authority bond
|
||||
|
||||
```bash
|
||||
yarn test:registry:init
|
||||
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
||||
```
|
||||
|
||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
- Setup ngrok for GitHub webhooks
|
||||
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
|
||||
- Start ngrok and point to backend server endpoint
|
||||
|
||||
```bash
|
||||
ngrok http http://localhost:8000
|
||||
```
|
||||
|
||||
- Look for the forwarding URL in ngrok
|
||||
|
||||
```bash
|
||||
...
|
||||
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
|
||||
...
|
||||
```
|
||||
|
||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
|
||||
...
|
||||
```
|
||||
|
||||
- Start the server in `packages/backend`
|
||||
|
||||
```bash
|
||||
yarn start:dev
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
- Change directory to `packages/frontend` in a new terminal
|
||||
|
||||
```bash
|
||||
cd packages/frontend
|
||||
```
|
||||
|
||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||
```
|
||||
|
||||
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
# 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
|
||||
```
|
||||
|
||||
### Frontend Production
|
||||
|
||||
- Let us assume the following domains for backend and frontend
|
||||
- Backend server: `api.snowballtools.com`
|
||||
- Frontend app: `dashboard.snowballtools.com`
|
||||
|
||||
- Set the following values in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
# Backend server endpoint
|
||||
REACT_APP_SERVER_URL = 'https://api.snowballtools.com'
|
||||
```
|
||||
|
||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
||||
- Create a project and add information to use wallet connect SDK
|
||||
- Add project name and select project type as `App`
|
||||
- Set project home page URL to `https://dashboard.snowballtools.com`
|
||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
|
||||
```
|
||||
|
||||
- Build the React application
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
- Use a web server for hosting static built files
|
||||
|
||||
```bash
|
||||
python3 -m http.server -d build 3000
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
|
||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
||||
|
||||
```env
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
```
|
||||
|
||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
|
||||
- Create a project and add information to use wallet connect SDK
|
||||
- Add project name and select project type as `App`
|
||||
- Project home page URL is not required to be set
|
||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_WALLET_CONNECT_ID = <Project_ID>
|
||||
```
|
||||
|
||||
- Start the React application
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
- The React application will be running in `http://localhost:3000/`
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ fi
|
||||
cat > $PKG_DIR/.env <<EOF
|
||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||
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'
|
||||
EOF
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"allowArgumentsExplicitlyTypedAsAny": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "ignoreRestSiblings": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
db
|
||||
dist
|
||||
environments/local.toml
|
||||
-5
@@ -17,11 +17,6 @@
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[gitea]
|
||||
[gitea.oAuth]
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@cerc-io/laconic-sdk": "^0.1.14",
|
||||
"@cerc-io/laconic-sdk": "^0.1.16",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
@@ -21,7 +21,6 @@
|
||||
"luxon": "^3.4.4",
|
||||
"nanoid": "3",
|
||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||
"node-fetch": "2",
|
||||
"octokit": "^3.1.2",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"semver": "^7.6.0",
|
||||
@@ -47,7 +46,6 @@
|
||||
"devDependencies": {
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface GitHubConfig {
|
||||
oAuth: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface RegistryConfig {
|
||||
@@ -39,7 +39,7 @@ export interface RegistryConfig {
|
||||
amount: string;
|
||||
denom: string;
|
||||
gas: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface MiscConfig {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
|
||||
import {
|
||||
DataSource,
|
||||
DeepPartial,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
FindOptionsWhere
|
||||
} from 'typeorm';
|
||||
import path from 'path';
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
@@ -74,14 +80,18 @@ export class Database {
|
||||
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 organizations = await organizationRepository.find(options);
|
||||
|
||||
return organizations;
|
||||
}
|
||||
|
||||
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
|
||||
async getOrganization (
|
||||
options: FindOneOptions<Organization>
|
||||
): Promise<Organization | null> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
const organization = await organizationRepository.findOne(options);
|
||||
|
||||
@@ -123,7 +133,11 @@ export class Database {
|
||||
|
||||
const project = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoinAndSelect('project.owner', 'owner')
|
||||
@@ -136,19 +150,29 @@ export class Database {
|
||||
return project;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
|
||||
async getProjectsInOrganization (
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
const projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.leftJoin('project.organization', 'organization')
|
||||
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
|
||||
userId,
|
||||
organizationSlug
|
||||
})
|
||||
.where(
|
||||
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
|
||||
{
|
||||
userId,
|
||||
organizationSlug
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
return projects;
|
||||
@@ -157,7 +181,9 @@ export class Database {
|
||||
/**
|
||||
* Get deployments with specified filter
|
||||
*/
|
||||
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
|
||||
async getDeployments (
|
||||
options: FindManyOptions<Deployment>
|
||||
): Promise<Deployment[]> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const deployments = await deploymentRepository.find(options);
|
||||
|
||||
@@ -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 deployment = await deploymentRepository.findOne(options);
|
||||
|
||||
@@ -210,8 +238,11 @@ export class Database {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
async getProjectMembersByProjectId (
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const projectMembers = await projectMemberRepository.find({
|
||||
relations: {
|
||||
@@ -228,8 +259,12 @@ export class Database {
|
||||
return projectMembers;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
|
||||
const environmentVariables = await environmentVariableRepository.find({
|
||||
where: {
|
||||
@@ -244,9 +279,12 @@ export class Database {
|
||||
}
|
||||
|
||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const deleteResult = await projectMemberRepository.delete({ id: projectMemberId });
|
||||
const deleteResult = await projectMemberRepository.delete({
|
||||
id: projectMemberId
|
||||
});
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0;
|
||||
@@ -255,37 +293,63 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
|
||||
async updateProjectMemberById (
|
||||
projectMemberId: string,
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<boolean> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
const updateResult = await projectMemberRepository.update(
|
||||
{ id: projectMemberId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
async addProjectMember (
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<ProjectMember> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
const newProjectMember = await projectMemberRepository.save(data);
|
||||
|
||||
return newProjectMember;
|
||||
}
|
||||
|
||||
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
|
||||
async addEnvironmentVariables (
|
||||
data: DeepPartial<EnvironmentVariable>[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const savedEnvironmentVariables =
|
||||
await environmentVariableRepository.save(data);
|
||||
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
|
||||
async updateEnvironmentVariable (
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const updateResult = await environmentVariableRepository.update(
|
||||
{ id: environmentVariableId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
|
||||
async deleteEnvironmentVariable (
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const deleteResult = await environmentVariableRepository.delete({
|
||||
id: environmentVariableId
|
||||
});
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0;
|
||||
@@ -295,7 +359,8 @@ export class Database {
|
||||
}
|
||||
|
||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const projectMemberWithProject = await projectMemberRepository.find({
|
||||
relations: {
|
||||
@@ -307,8 +372,7 @@ export class Database {
|
||||
where: {
|
||||
id: projectMemberId
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (projectMemberWithProject.length === 0) {
|
||||
throw new Error('Member does not exist');
|
||||
@@ -317,34 +381,49 @@ export class Database {
|
||||
return projectMemberWithProject[0];
|
||||
}
|
||||
|
||||
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
|
||||
async getProjectsBySearchText (
|
||||
userId: string,
|
||||
searchText: string
|
||||
): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
const projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
|
||||
userId,
|
||||
searchText: `%${searchText}%`
|
||||
})
|
||||
.where(
|
||||
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
|
||||
{
|
||||
userId,
|
||||
searchText: `%${searchText}%`
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
|
||||
async updateDeploymentById (
|
||||
deploymentId: string,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
return this.updateDeployment({ id: deploymentId }, data);
|
||||
}
|
||||
|
||||
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
|
||||
async updateDeployment (
|
||||
criteria: FindOptionsWhere<Deployment>,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const updateResult = await deploymentRepository.update(criteria, data);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
|
||||
async updateDeploymentsByProjectIds (
|
||||
projectIds: string[],
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
|
||||
const updateResult = await deploymentRepository
|
||||
@@ -378,9 +457,15 @@ export class Database {
|
||||
return projectRepository.save(newProject);
|
||||
}
|
||||
|
||||
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
||||
async updateProjectById (
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<boolean> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const updateResult = await projectRepository.update({ id: projectId }, data);
|
||||
const updateResult = await projectRepository.update(
|
||||
{ id: projectId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
@@ -427,14 +512,20 @@ export class Database {
|
||||
return domain;
|
||||
}
|
||||
|
||||
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
||||
async updateDomainById (
|
||||
domainId: string,
|
||||
data: DeepPartial<Domain>
|
||||
): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const updateResult = await domainRepository.update({ id: domainId }, data);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
||||
async getDomainsByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<Domain>
|
||||
): Promise<Domain[]> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
|
||||
const domains = await domainRepository.find({
|
||||
|
||||
@@ -27,26 +27,26 @@ export enum DeploymentStatus {
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRequest {
|
||||
type: string
|
||||
version: string
|
||||
name: string
|
||||
application: string
|
||||
config: string,
|
||||
meta: string
|
||||
type: string;
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
config: string;
|
||||
meta: string;
|
||||
}
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string;
|
||||
version:string
|
||||
name: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
author?: string
|
||||
repository?: string[],
|
||||
app_version?: string
|
||||
repository_ref: string
|
||||
app_type: string
|
||||
version: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
license?: string;
|
||||
author?: string;
|
||||
repository?: string[];
|
||||
app_version?: string;
|
||||
repository_ref: string;
|
||||
app_type: string;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Domain {
|
||||
|
||||
@ManyToOne(() => Domain)
|
||||
@JoinColumn({ name: 'redirectToId' })
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
redirectTo!: Domain | null;
|
||||
|
||||
@Column({
|
||||
|
||||
@@ -27,8 +27,12 @@ export class Organization {
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(userOrganization) => userOrganization.organization,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class Project {
|
||||
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||
deployments!: Deployment[];
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
|
||||
@@ -15,7 +15,7 @@ import { User } from './User';
|
||||
|
||||
export enum Permission {
|
||||
View = 'View',
|
||||
Edit = 'Edit'
|
||||
Edit = 'Edit',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -39,13 +39,17 @@ export class User {
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
|
||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(UserOrganization) => UserOrganization.member,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
||||
@@ -31,9 +31,16 @@ export const main = async (): Promise<void> => {
|
||||
await db.init();
|
||||
|
||||
const registry = new Registry(registryConfig);
|
||||
const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
|
||||
const service = new Service(
|
||||
{ gitHubConfig: gitHub, registryConfig },
|
||||
db,
|
||||
app,
|
||||
registry
|
||||
);
|
||||
|
||||
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
|
||||
const typeDefs = fs
|
||||
.readFileSync(path.join(__dirname, 'schema.gql'))
|
||||
.toString();
|
||||
const resolvers = await createResolvers(service);
|
||||
|
||||
await createAndStartServer(server, typeDefs, resolvers, service);
|
||||
|
||||
@@ -6,23 +6,33 @@ import { DateTime } from 'luxon';
|
||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import { ApplicationRecord, Deployment, ApplicationDeploymentRequest } from './entity/Deployment';
|
||||
import {
|
||||
ApplicationRecord,
|
||||
Deployment,
|
||||
ApplicationDeploymentRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||
import { sleep } from './utils';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||
const SLEEP_DURATION = 1000;
|
||||
|
||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||
export class Registry {
|
||||
private registry: LaconicRegistry;
|
||||
private registryConfig: RegistryConfig;
|
||||
|
||||
constructor (registryConfig : RegistryConfig) {
|
||||
constructor (registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig;
|
||||
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
registryConfig.chainId
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationRecord ({
|
||||
@@ -32,24 +42,38 @@ export class Registry {
|
||||
appType,
|
||||
repoUrl
|
||||
}: {
|
||||
appName: string,
|
||||
packageJSON: PackageJSON
|
||||
commitHash: string,
|
||||
appType: string,
|
||||
repoUrl: string
|
||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
||||
appName: string;
|
||||
packageJSON: PackageJSON;
|
||||
commitHash: string;
|
||||
appType: string;
|
||||
repoUrl: string;
|
||||
}): Promise<{
|
||||
applicationRecordId: string;
|
||||
applicationRecordData: ApplicationRecord;
|
||||
}> {
|
||||
// Use laconic-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords({
|
||||
type: APP_RECORD_TYPE,
|
||||
name: packageJSON.name
|
||||
}, true);
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_RECORD_TYPE,
|
||||
name: packageJSON.name
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Get next version of record
|
||||
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
|
||||
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
||||
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
||||
const bondRecords = records.filter(
|
||||
(record: any) => record.bondId === this.registryConfig.bondId
|
||||
);
|
||||
const [latestBondRecord] = bondRecords.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
);
|
||||
const nextVersion = semverInc(
|
||||
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||
'patch'
|
||||
);
|
||||
|
||||
assert(nextVersion, 'Application record version not valid');
|
||||
|
||||
@@ -64,7 +88,12 @@ export class Registry {
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }),
|
||||
...(packageJSON.author && {
|
||||
author:
|
||||
typeof packageJSON.author === 'object'
|
||||
? JSON.stringify(packageJSON.author)
|
||||
: packageJSON.author
|
||||
}),
|
||||
...(packageJSON.version && { app_version: packageJSON.version })
|
||||
};
|
||||
|
||||
@@ -84,22 +113,45 @@ export class Registry {
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
|
||||
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
packageJsonName: string,
|
||||
repository: string,
|
||||
environmentVariables: { [key: string]: string }
|
||||
environmentVariables: { [key: string]: string },
|
||||
dns: string,
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string,
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
}> {
|
||||
const crn = this.getCrn(data.appName);
|
||||
const records = await this.registry.resolveNames([crn]);
|
||||
@@ -115,7 +167,7 @@ export class Registry {
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: `${data.deployment.project.name}-${data.deployment.id}`,
|
||||
dns: data.dns,
|
||||
|
||||
// TODO: Not set in test-progressive-web-app CI
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||
@@ -125,12 +177,15 @@ export class Registry {
|
||||
env: data.environmentVariables
|
||||
}),
|
||||
meta: JSON.stringify({
|
||||
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
|
||||
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
||||
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.deployment.commitHash
|
||||
})
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
@@ -143,21 +198,35 @@ export class Registry {
|
||||
log(`Application deployment request record published: ${result.data.id}`);
|
||||
log('Application deployment request data:', applicationDeploymentRequest);
|
||||
|
||||
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
|
||||
return {
|
||||
applicationDeploymentRequestId: result.data.id,
|
||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords for deployments
|
||||
*/
|
||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
||||
async getDeploymentRecords (
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRecord[]> {
|
||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
||||
const records = await this.registry.queryRecords({
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||
}, true);
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationRecord ids
|
||||
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
|
||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationRecordId === record.attributes.application &&
|
||||
record.attributes.url.includes(deployment.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getCrn (appName: string): string {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Permission } from './entity/ProjectMember';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { Project } from './entity/Project';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { GitType } from './types';
|
||||
|
||||
const log = debug('snowball:resolver');
|
||||
|
||||
@@ -34,7 +33,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getDeploymentsByProjectId(projectId);
|
||||
},
|
||||
|
||||
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
||||
environmentVariables: async (
|
||||
_: any,
|
||||
{ projectId }: { projectId: string }
|
||||
) => {
|
||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||
},
|
||||
|
||||
@@ -46,14 +48,24 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
Mutation: {
|
||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
||||
removeProjectMember: async (
|
||||
_: any,
|
||||
{ projectMemberId }: { projectMemberId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.removeProjectMember(context.user, projectMemberId);
|
||||
} catch (err) {
|
||||
@@ -62,12 +74,18 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
updateProjectMember: async (_: any, { projectMemberId, data }: {
|
||||
projectMemberId: string,
|
||||
data: {
|
||||
permissions: Permission[]
|
||||
updateProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectMemberId,
|
||||
data
|
||||
}: {
|
||||
projectMemberId: string;
|
||||
data: {
|
||||
permissions: Permission[];
|
||||
};
|
||||
}
|
||||
}) => {
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProjectMember(projectMemberId, data);
|
||||
} catch (err) {
|
||||
@@ -76,13 +94,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
addProjectMember: async (_: any, { projectId, data }: {
|
||||
projectId: string,
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
addProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data
|
||||
}: {
|
||||
projectId: string;
|
||||
data: {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
}
|
||||
}) => {
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addProjectMember(projectId, data));
|
||||
} catch (err) {
|
||||
@@ -91,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 {
|
||||
return Boolean(await service.addEnvironmentVariables(projectId, data));
|
||||
return Boolean(
|
||||
await service.addEnvironmentVariables(projectId, data)
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
|
||||
updateEnvironmentVariable: async (
|
||||
_: any,
|
||||
{
|
||||
environmentVariableId,
|
||||
data
|
||||
}: {
|
||||
environmentVariableId: string;
|
||||
data: DeepPartial<EnvironmentVariable>;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await service.updateEnvironmentVariable(environmentVariableId, data);
|
||||
return await service.updateEnvironmentVariable(
|
||||
environmentVariableId,
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
||||
removeEnvironmentVariable: async (
|
||||
_: any,
|
||||
{ environmentVariableId }: { environmentVariableId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||
} catch (err) {
|
||||
@@ -118,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 {
|
||||
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
@@ -127,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 {
|
||||
return await service.addProject(context.user, organizationSlug, data);
|
||||
} catch (err) {
|
||||
@@ -136,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 {
|
||||
return await service.updateProject(projectId, data);
|
||||
} catch (err) {
|
||||
@@ -145,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 {
|
||||
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
@@ -158,7 +226,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
try {
|
||||
return await service.deleteProject(projectId);
|
||||
} catch (err) {
|
||||
log(err); return false;
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -171,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 {
|
||||
return await service.rollbackDeployment(projectId, deploymentId);
|
||||
} catch (err) {
|
||||
@@ -180,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 {
|
||||
return Boolean(await service.addDomain(projectId, data));
|
||||
} catch (err) {
|
||||
@@ -189,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 {
|
||||
return await service.updateDomain(domainId, data);
|
||||
} catch (err) {
|
||||
@@ -198,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 {
|
||||
return await service.authenticateGitHub(code, context.user);
|
||||
} catch (err) {
|
||||
@@ -207,15 +292,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
authenticateGit: async (_: any, { type, code }: { type: GitType, code: string }, context: any) => {
|
||||
try {
|
||||
return await service.authenticateGit(type, code, context.user);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||
try {
|
||||
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
||||
|
||||
@@ -26,11 +26,6 @@ enum DomainStatus {
|
||||
Pending
|
||||
}
|
||||
|
||||
enum GitType {
|
||||
GitHub
|
||||
Gitea
|
||||
}
|
||||
|
||||
type User {
|
||||
id: String!
|
||||
name: String
|
||||
@@ -193,10 +188,19 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
||||
updateProjectMember(
|
||||
projectMemberId: String!
|
||||
data: UpdateProjectMemberInput
|
||||
): Boolean!
|
||||
removeProjectMember(projectMemberId: String!): Boolean!
|
||||
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
|
||||
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
|
||||
addEnvironmentVariables(
|
||||
projectId: String!
|
||||
data: [AddEnvironmentVariableInput!]
|
||||
): Boolean!
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: String!
|
||||
data: UpdateEnvironmentVariableInput!
|
||||
): Boolean!
|
||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||
@@ -208,6 +212,5 @@ type Mutation {
|
||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||
authenticateGitHub(code: String!): AuthResult!
|
||||
authenticateGit(type: GitType!, code: String!): AuthResult!
|
||||
unauthenticateGitHub: Boolean!
|
||||
}
|
||||
|
||||
+208
-152
@@ -2,7 +2,6 @@ import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||
import { Octokit, RequestError } from 'octokit';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { OAuthApp } from '@octokit/oauth-app';
|
||||
|
||||
@@ -16,17 +15,16 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
||||
import { User } from './entity/User';
|
||||
import { Registry } from './registry';
|
||||
import { GitHubConfig, RegistryConfig } from './config';
|
||||
import { AppDeploymentRecord, GitPushEventPayload, GitType, PackageJSON } from './types';
|
||||
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||
import { Role } from './entity/UserOrganization';
|
||||
|
||||
const log = debug('snowball:service');
|
||||
|
||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
||||
const GITEA_ACCESS_TOKEN_ENDPOINT = 'https://git.vdb.to/login/oauth/access_token';
|
||||
|
||||
interface Config {
|
||||
gitHubConfig: GitHubConfig
|
||||
registryConfig: RegistryConfig
|
||||
gitHubConfig: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
}
|
||||
|
||||
export class Service {
|
||||
@@ -74,7 +72,9 @@ export class Service {
|
||||
});
|
||||
|
||||
if (deployments.length) {
|
||||
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`);
|
||||
log(
|
||||
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
||||
);
|
||||
|
||||
// Fetch ApplicationDeploymentRecord for deployments
|
||||
const records = await this.registry.getDeploymentRecords(deployments);
|
||||
@@ -94,10 +94,12 @@ export class Service {
|
||||
/**
|
||||
* Update deployments with ApplicationDeploymentRecord data
|
||||
*/
|
||||
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
|
||||
async updateDeploymentsWithRecordData (
|
||||
records: AppDeploymentRecord[]
|
||||
): Promise<void> {
|
||||
// Get deployments for ApplicationDeploymentRecords
|
||||
const deployments = await this.db.getDeployments({
|
||||
where: records.map(record => ({
|
||||
where: records.map((record) => ({
|
||||
applicationRecordId: record.attributes.application
|
||||
})),
|
||||
order: {
|
||||
@@ -106,38 +108,46 @@ export class Service {
|
||||
});
|
||||
|
||||
// Get project IDs of deployments that are in production environment
|
||||
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
|
||||
if (deployment.environment === Environment.Production) {
|
||||
acc.add(deployment.projectId);
|
||||
}
|
||||
const productionDeploymentProjectIds = deployments.reduce(
|
||||
(acc, deployment): Set<string> => {
|
||||
if (deployment.environment === Environment.Production) {
|
||||
acc.add(deployment.projectId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
return acc;
|
||||
},
|
||||
new Set<string>()
|
||||
);
|
||||
|
||||
// Set old deployments isCurrent to false
|
||||
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false });
|
||||
await this.db.updateDeploymentsByProjectIds(
|
||||
Array.from(productionDeploymentProjectIds),
|
||||
{ isCurrent: false }
|
||||
);
|
||||
|
||||
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => {
|
||||
acc[deployment.applicationRecordId] = deployment;
|
||||
return acc;
|
||||
}, {});
|
||||
const recordToDeploymentsMap = deployments.reduce(
|
||||
(acc: { [key: string]: Deployment }, deployment) => {
|
||||
acc[deployment.applicationRecordId] = deployment;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// Update deployment data for ApplicationDeploymentRecords
|
||||
const deploymentUpdatePromises = records.map(async (record) => {
|
||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||
|
||||
await this.db.updateDeploymentById(
|
||||
deployment.id,
|
||||
{
|
||||
applicationDeploymentRecordId: record.id,
|
||||
applicationDeploymentRecordData: record.attributes,
|
||||
url: record.attributes.url,
|
||||
status: DeploymentStatus.Ready,
|
||||
isCurrent: deployment.environment === Environment.Production
|
||||
}
|
||||
);
|
||||
await this.db.updateDeploymentById(deployment.id, {
|
||||
applicationDeploymentRecordId: record.id,
|
||||
applicationDeploymentRecordData: record.attributes,
|
||||
url: record.attributes.url,
|
||||
status: DeploymentStatus.Ready,
|
||||
isCurrent: deployment.environment === Environment.Production
|
||||
});
|
||||
|
||||
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`);
|
||||
log(
|
||||
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(deploymentUpdatePromises);
|
||||
@@ -183,7 +193,10 @@ export class Service {
|
||||
|
||||
async getOctokit (userId: string): Promise<Octokit> {
|
||||
const user = await this.db.getUser({ where: { id: userId } });
|
||||
assert(user && user.gitHubToken, 'User needs to be authenticated with GitHub token');
|
||||
assert(
|
||||
user && user.gitHubToken,
|
||||
'User needs to be authenticated with GitHub token'
|
||||
);
|
||||
|
||||
return new Octokit({ auth: user.gitHubToken });
|
||||
}
|
||||
@@ -208,13 +221,19 @@ export class Service {
|
||||
return dbDeployments;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> {
|
||||
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
projectId: string
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const dbEnvironmentVariables =
|
||||
await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||
return dbEnvironmentVariables;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
||||
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId);
|
||||
async getProjectMembersByProjectId (
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const dbProjectMembers =
|
||||
await this.db.getProjectMembersByProjectId(projectId);
|
||||
return dbProjectMembers;
|
||||
}
|
||||
|
||||
@@ -223,20 +242,28 @@ export class Service {
|
||||
return dbProjects;
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
||||
async getDomainsByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<Domain>
|
||||
): Promise<Domain[]> {
|
||||
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
||||
return dbDomains;
|
||||
}
|
||||
|
||||
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
|
||||
async updateProjectMember (
|
||||
projectMemberId: string,
|
||||
data: { permissions: Permission[] }
|
||||
): Promise<boolean> {
|
||||
return this.db.updateProjectMemberById(projectMemberId, data);
|
||||
}
|
||||
|
||||
async addProjectMember (projectId: string,
|
||||
async addProjectMember (
|
||||
projectId: string,
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
}): Promise<ProjectMember> {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
): Promise<ProjectMember> {
|
||||
// TODO: Send invitation
|
||||
let user = await this.db.getUser({
|
||||
where: {
|
||||
@@ -281,29 +308,41 @@ export class Service {
|
||||
}
|
||||
}
|
||||
|
||||
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
|
||||
const formattedEnvironmentVariables = data.map((environmentVariable) => {
|
||||
return environmentVariable.environments.map((environment) => {
|
||||
return ({
|
||||
key: environmentVariable.key,
|
||||
value: environmentVariable.value,
|
||||
environment: environment as Environment,
|
||||
project: Object.assign(new Project(), {
|
||||
id: projectId
|
||||
})
|
||||
async addEnvironmentVariables (
|
||||
projectId: string,
|
||||
data: { environments: string[]; key: string; value: string }[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const formattedEnvironmentVariables = data
|
||||
.map((environmentVariable) => {
|
||||
return environmentVariable.environments.map((environment) => {
|
||||
return {
|
||||
key: environmentVariable.key,
|
||||
value: environmentVariable.value,
|
||||
environment: environment as Environment,
|
||||
project: Object.assign(new Project(), {
|
||||
id: projectId
|
||||
})
|
||||
};
|
||||
});
|
||||
});
|
||||
}).flat();
|
||||
})
|
||||
.flat();
|
||||
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
|
||||
formattedEnvironmentVariables
|
||||
);
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
||||
async updateEnvironmentVariable (
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
||||
}
|
||||
|
||||
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
async removeEnvironmentVariable (
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||
}
|
||||
|
||||
@@ -319,7 +358,10 @@ export class Service {
|
||||
throw new Error('Deployment does not exist');
|
||||
}
|
||||
|
||||
const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch });
|
||||
const prodBranchDomains = await this.db.getDomainsByProjectId(
|
||||
oldDeployment.project.id,
|
||||
{ branch: oldDeployment.project.prodBranch }
|
||||
);
|
||||
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
|
||||
@@ -340,11 +382,12 @@ export class Service {
|
||||
async createDeployment (
|
||||
userId: string,
|
||||
octokit: Octokit,
|
||||
data: DeepPartial<Deployment>,
|
||||
recordData: { repoUrl?: string } = {}
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<Deployment> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
log(`Creating deployment in project ${data.project.name} from branch ${data.branch}`);
|
||||
log(
|
||||
`Creating deployment in project ${data.project.name} from branch ${data.branch}`
|
||||
);
|
||||
const [owner, repo] = data.project.repository.split('/');
|
||||
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
@@ -363,19 +406,20 @@ export class Service {
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
if (!recordData.repoUrl) {
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||
recordData.repoUrl = repoDetails.html_url;
|
||||
}
|
||||
const repoUrl = (await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo
|
||||
})).data.html_url;
|
||||
|
||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
|
||||
appName: repo,
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl: recordData.repoUrl
|
||||
});
|
||||
const { applicationRecordId, applicationRecordData } =
|
||||
await this.registry.createApplicationRecord({
|
||||
appName: repo,
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl
|
||||
});
|
||||
|
||||
// Update previous deployment with prod branch domain
|
||||
// TODO: Fix unique constraint error for domain
|
||||
@@ -416,11 +460,23 @@ export class Service {
|
||||
{
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
packageJsonName: packageJSON.name,
|
||||
repository: recordData.repoUrl,
|
||||
environmentVariables: environmentVariablesObj
|
||||
repository: repoUrl,
|
||||
environmentVariables: environmentVariablesObj,
|
||||
dns: `${newDeployment.project.name}-${newDeployment.id}`
|
||||
});
|
||||
|
||||
// To set project DNS
|
||||
if (data.environment === Environment.Production) {
|
||||
await this.registry.createApplicationDeploymentRequest(
|
||||
{
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
repository: repoUrl,
|
||||
environmentVariables: environmentVariablesObj,
|
||||
dns: `${newDeployment.project.name}`
|
||||
});
|
||||
}
|
||||
|
||||
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
||||
|
||||
return newDeployment;
|
||||
@@ -441,15 +497,15 @@ export class Service {
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
const [owner, repo] = project.repository.split('/');
|
||||
|
||||
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({
|
||||
const {
|
||||
data: [latestCommit]
|
||||
} = await octokit.rest.repos.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
sha: project.prodBranch,
|
||||
per_page: 1
|
||||
});
|
||||
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||
|
||||
// Create deployment with prod branch and latest commit
|
||||
await this.createDeployment(user.id,
|
||||
octokit,
|
||||
@@ -460,9 +516,6 @@ export class Service {
|
||||
domain: null,
|
||||
commitHash: latestCommit.sha,
|
||||
commitMessage: latestCommit.commit.message
|
||||
},
|
||||
{
|
||||
repoUrl: repoDetails.html_url
|
||||
}
|
||||
);
|
||||
|
||||
@@ -478,7 +531,10 @@ export class Service {
|
||||
owner,
|
||||
repo,
|
||||
config: {
|
||||
url: new URL('api/github/webhook', this.config.gitHubConfig.webhookUrl).href,
|
||||
url: new URL(
|
||||
'api/github/webhook',
|
||||
this.config.gitHubConfig.webhookUrl
|
||||
).href,
|
||||
content_type: 'json'
|
||||
},
|
||||
events: ['push']
|
||||
@@ -486,9 +542,13 @@ export class Service {
|
||||
} catch (err) {
|
||||
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
||||
if (
|
||||
!(err instanceof RequestError &&
|
||||
err.status === 422 &&
|
||||
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR))
|
||||
!(
|
||||
err instanceof RequestError &&
|
||||
err.status === 422 &&
|
||||
(err.response?.data as any).errors.some(
|
||||
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
@@ -498,9 +558,17 @@ export class Service {
|
||||
}
|
||||
|
||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||
const { repository, ref, head_commit: headCommit } = data;
|
||||
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
||||
const projects = await this.db.getProjects({ where: { repository: repository.full_name } });
|
||||
const { repository, ref, head_commit: headCommit, deleted } = data;
|
||||
|
||||
if (deleted) {
|
||||
log(`Branch ${ref} deleted for project ${repository.full_name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`);
|
||||
const projects = await this.db.getProjects({
|
||||
where: { repository: repository.full_name }
|
||||
});
|
||||
|
||||
if (!projects.length) {
|
||||
log(`No projects found for repository ${repository.full_name}`);
|
||||
@@ -512,23 +580,29 @@ export class Service {
|
||||
|
||||
for await (const project of projects) {
|
||||
const octokit = await this.getOctokit(project.ownerId);
|
||||
const [domain] = await this.db.getDomainsByProjectId(project.id, { branch });
|
||||
const [domain] = await this.db.getDomainsByProjectId(project.id, {
|
||||
branch
|
||||
});
|
||||
|
||||
// Create deployment with branch and latest commit in GitHub data
|
||||
await this.createDeployment(project.ownerId,
|
||||
octokit,
|
||||
{
|
||||
project,
|
||||
branch,
|
||||
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message
|
||||
});
|
||||
await this.createDeployment(project.ownerId, octokit, {
|
||||
project,
|
||||
branch,
|
||||
environment:
|
||||
project.prodBranch === branch
|
||||
? Environment.Production
|
||||
: Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
||||
async updateProject (
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<boolean> {
|
||||
return this.db.updateProjectById(projectId, data);
|
||||
}
|
||||
|
||||
@@ -545,7 +619,9 @@ export class Service {
|
||||
});
|
||||
|
||||
if (domainsRedirectedFrom.length > 0) {
|
||||
throw new Error('Cannot delete domain since it has redirects from other domains');
|
||||
throw new Error(
|
||||
'Cannot delete domain since it has redirects from other domains'
|
||||
);
|
||||
}
|
||||
|
||||
return this.db.deleteDomainById(domainId);
|
||||
@@ -584,7 +660,10 @@ export class Service {
|
||||
return newDeployment;
|
||||
}
|
||||
|
||||
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
||||
async rollbackDeployment (
|
||||
projectId: string,
|
||||
deploymentId: string
|
||||
): Promise<boolean> {
|
||||
// TODO: Implement transactions
|
||||
const oldCurrentDeployment = await this.db.getDeployment({
|
||||
relations: {
|
||||
@@ -602,16 +681,25 @@ export class Service {
|
||||
throw new Error('Current deployment doesnot exist');
|
||||
}
|
||||
|
||||
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
|
||||
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||
oldCurrentDeployment.id,
|
||||
{ isCurrent: false, domain: null }
|
||||
);
|
||||
|
||||
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain });
|
||||
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||
deploymentId,
|
||||
{ isCurrent: true, domain: oldCurrentDeployment?.domain }
|
||||
);
|
||||
|
||||
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
||||
}
|
||||
|
||||
async addDomain (projectId: string, data: { name: string }): Promise<{
|
||||
primaryDomain: Domain,
|
||||
redirectedDomain: Domain
|
||||
async addDomain (
|
||||
projectId: string,
|
||||
data: { name: string }
|
||||
): Promise<{
|
||||
primaryDomain: Domain;
|
||||
redirectedDomain: Domain;
|
||||
}> {
|
||||
const currentProject = await this.db.getProjectById(projectId);
|
||||
|
||||
@@ -636,12 +724,20 @@ export class Service {
|
||||
redirectTo: savedPrimaryDomain
|
||||
};
|
||||
|
||||
const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails);
|
||||
const savedRedirectedDomain = await this.db.addDomain(
|
||||
redirectedDomainDetails
|
||||
);
|
||||
|
||||
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain };
|
||||
return {
|
||||
primaryDomain: savedPrimaryDomain,
|
||||
redirectedDomain: savedRedirectedDomain
|
||||
};
|
||||
}
|
||||
|
||||
async updateDomain (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
||||
async updateDomain (
|
||||
domainId: string,
|
||||
data: DeepPartial<Domain>
|
||||
): Promise<boolean> {
|
||||
const domain = await this.db.getDomain({
|
||||
where: {
|
||||
id: domainId
|
||||
@@ -682,7 +778,9 @@ export class Service {
|
||||
}
|
||||
|
||||
if (redirectedDomain.redirectToId) {
|
||||
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
|
||||
throw new Error(
|
||||
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
|
||||
);
|
||||
}
|
||||
|
||||
newDomain.redirectTo = redirectedDomain;
|
||||
@@ -703,48 +801,6 @@ export class Service {
|
||||
return { token };
|
||||
}
|
||||
|
||||
async authenticateGit (type: GitType, code:string, user: User): Promise<{token: string}> {
|
||||
let token: string;
|
||||
|
||||
switch (type) {
|
||||
case GitType.GitHub:
|
||||
({ authentication: { token } } = await this.oauthApp.createToken({
|
||||
code
|
||||
}));
|
||||
|
||||
break;
|
||||
|
||||
case GitType.Gitea: {
|
||||
const response = await fetch(GITEA_ACCESS_TOKEN_ENDPOINT, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
// TODO: Fetch from config
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
// TODO: Get frontend app URL from config
|
||||
redirect_uri: 'http://localhost:3000/organization/projects/create'
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
assert(response.ok, `HTTP Error Response: ${response.status} ${response.statusText}`);
|
||||
const data: any = await response.json();
|
||||
({ access_token: token } = data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: throw new Error(`Type ${type} not handled for Git authentication`);
|
||||
}
|
||||
|
||||
assert(token, `Access token is not set for type ${type}`);
|
||||
await this.db.updateUser(user, { gitHubToken: token });
|
||||
|
||||
return { token };
|
||||
}
|
||||
|
||||
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||
return this.db.updateUser(user, data);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface GitPushEventPayload {
|
||||
id: string;
|
||||
message: string;
|
||||
};
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
@@ -47,10 +48,5 @@ interface RegistryRecord {
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRecordAttributes
|
||||
}
|
||||
|
||||
export enum GitType {
|
||||
GitHub = 'GitHub',
|
||||
Gitea = 'Gitea',
|
||||
attributes: AppDeploymentRecordAttributes;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,15 @@ export const getEntities = async (filePath: string): Promise<any> => {
|
||||
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 savedEntity:Entity[] = [];
|
||||
const savedEntity: Entity[] = [];
|
||||
|
||||
for (const entityData of entities) {
|
||||
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
||||
@@ -61,3 +66,5 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType:
|
||||
|
||||
return savedEntity;
|
||||
};
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
@@ -18,4 +18,4 @@ const main = async () => {
|
||||
deleteFile(config.database.dbPath);
|
||||
};
|
||||
|
||||
main().catch(err => log(err));
|
||||
main().catch((err) => log(err));
|
||||
|
||||
+20
-20
@@ -1,9 +1,9 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex":0,
|
||||
"domainIndex": 0,
|
||||
"createdByIndex": 0,
|
||||
"id":"ffhae3zq",
|
||||
"id": "ffhae3zq",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
@@ -18,9 +18,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex":1,
|
||||
"domainIndex": 1,
|
||||
"createdByIndex": 0,
|
||||
"id":"vehagei8",
|
||||
"id": "vehagei8",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
@@ -35,9 +35,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex":2,
|
||||
"domainIndex": 2,
|
||||
"createdByIndex": 0,
|
||||
"id":"qmgekyte",
|
||||
"id": "qmgekyte",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
@@ -54,7 +54,7 @@
|
||||
"projectIndex": 0,
|
||||
"domainIndex": null,
|
||||
"createdByIndex": 0,
|
||||
"id":"f8wsyim6",
|
||||
"id": "f8wsyim6",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": false,
|
||||
@@ -69,9 +69,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex":3,
|
||||
"domainIndex": 3,
|
||||
"createdByIndex": 1,
|
||||
"id":"eO8cckxk",
|
||||
"id": "eO8cckxk",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
@@ -86,9 +86,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex":4,
|
||||
"domainIndex": 4,
|
||||
"createdByIndex": 1,
|
||||
"id":"yaq0t5yw",
|
||||
"id": "yaq0t5yw",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
@@ -103,9 +103,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex":5,
|
||||
"domainIndex": 5,
|
||||
"createdByIndex": 1,
|
||||
"id":"hwwr6sbx",
|
||||
"id": "hwwr6sbx",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
@@ -120,9 +120,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex":9,
|
||||
"domainIndex": 9,
|
||||
"createdByIndex": 2,
|
||||
"id":"ndxje48a",
|
||||
"id": "ndxje48a",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
@@ -137,9 +137,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex":7,
|
||||
"domainIndex": 7,
|
||||
"createdByIndex": 2,
|
||||
"id":"gtgpgvei",
|
||||
"id": "gtgpgvei",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
@@ -154,9 +154,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex":8,
|
||||
"domainIndex": 8,
|
||||
"createdByIndex": 2,
|
||||
"id":"b4bpthjr",
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
@@ -173,7 +173,7 @@
|
||||
"projectIndex": 3,
|
||||
"domainIndex": 6,
|
||||
"createdByIndex": 2,
|
||||
"id":"b4bpthjr",
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
|
||||
+9
-31
@@ -2,77 +2,55 @@
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 0,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 0,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 1,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 2,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 2,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 3,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 3,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 4,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 4,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
|
||||
import { Domain } from '../src/entity/Domain';
|
||||
import { ProjectMember } from '../src/entity/ProjectMember';
|
||||
import { Deployment } from '../src/entity/Deployment';
|
||||
import { checkFileExists, getConfig, getEntities, loadAndSaveData } from '../src/utils';
|
||||
import {
|
||||
checkFileExists,
|
||||
getConfig,
|
||||
getEntities,
|
||||
loadAndSaveData
|
||||
} from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
@@ -27,19 +32,34 @@ const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
||||
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
||||
|
||||
const generateTestData = async (dataSource: DataSource) => {
|
||||
const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH));
|
||||
const userEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_DATA_PATH)
|
||||
);
|
||||
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
||||
|
||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities);
|
||||
const orgEntities = await getEntities(
|
||||
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||
);
|
||||
const savedOrgs = await loadAndSaveData(
|
||||
Organization,
|
||||
dataSource,
|
||||
orgEntities
|
||||
);
|
||||
|
||||
const projectRelations = {
|
||||
owner: savedUsers,
|
||||
organization: savedOrgs
|
||||
};
|
||||
|
||||
const projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH));
|
||||
const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations);
|
||||
const projectEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||
);
|
||||
const savedProjects = await loadAndSaveData(
|
||||
Project,
|
||||
dataSource,
|
||||
projectEntities,
|
||||
projectRelations
|
||||
);
|
||||
|
||||
const domainRepository = dataSource.getRepository(Domain);
|
||||
|
||||
@@ -47,16 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
project: savedProjects
|
||||
};
|
||||
|
||||
const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH));
|
||||
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, domainPrimaryRelations);
|
||||
const primaryDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
|
||||
);
|
||||
const savedPrimaryDomains = await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
primaryDomainsEntities,
|
||||
domainPrimaryRelations
|
||||
);
|
||||
|
||||
const domainRedirectedRelations = {
|
||||
project: savedProjects,
|
||||
redirectTo: savedPrimaryDomains
|
||||
};
|
||||
|
||||
const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH));
|
||||
await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations);
|
||||
const redirectDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
redirectDomainsEntities,
|
||||
domainRedirectedRelations
|
||||
);
|
||||
|
||||
const savedDomains = await domainRepository.find();
|
||||
|
||||
@@ -65,16 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
organization: savedOrgs
|
||||
};
|
||||
|
||||
const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH));
|
||||
await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations);
|
||||
const userOrganizationsEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
UserOrganization,
|
||||
dataSource,
|
||||
userOrganizationsEntities,
|
||||
userOrganizationRelations
|
||||
);
|
||||
|
||||
const projectMemberRelations = {
|
||||
member: savedUsers,
|
||||
project: savedProjects
|
||||
};
|
||||
|
||||
const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH));
|
||||
await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations);
|
||||
const projectMembersEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
ProjectMember,
|
||||
dataSource,
|
||||
projectMembersEntities,
|
||||
projectMemberRelations
|
||||
);
|
||||
|
||||
const deploymentRelations = {
|
||||
project: savedProjects,
|
||||
@@ -82,15 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
createdBy: savedUsers
|
||||
};
|
||||
|
||||
const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH));
|
||||
await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations);
|
||||
const deploymentsEntities = await getEntities(
|
||||
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
Deployment,
|
||||
dataSource,
|
||||
deploymentsEntities,
|
||||
deploymentRelations
|
||||
);
|
||||
|
||||
const environmentVariableRelations = {
|
||||
project: savedProjects
|
||||
};
|
||||
|
||||
const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH));
|
||||
await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations);
|
||||
const environmentVariablesEntities = await getEntities(
|
||||
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
EnvironmentVariable,
|
||||
dataSource,
|
||||
environmentVariablesEntities,
|
||||
environmentVariableRelations
|
||||
);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
|
||||
@@ -21,12 +21,20 @@ async function main () {
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||
log('bondId:', bondId);
|
||||
await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee);
|
||||
await registry.createBond(
|
||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||
log('Reserved authority name:', name);
|
||||
await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee);
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
);
|
||||
log(`Bond ${bondId} set for authority ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ const log = debug('snowball:publish-deploy-records');
|
||||
async function main () {
|
||||
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({
|
||||
type: 'better-sqlite3',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# deployer
|
||||
|
||||
- Install dependencies
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
```bash
|
||||
brew install jq # if you do not have jq installed already
|
||||
```
|
||||
|
||||
- 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 in [web-app deployer](https://console.laconic.com/deployer).
|
||||
- Check records in [registry console app](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 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
```
|
||||
- Command to refill bond
|
||||
```bash
|
||||
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
cns:
|
||||
restEndpoint: http://console.laconic.com:1317
|
||||
gqlEndpoint: http://console.laconic.com:9473/api
|
||||
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
||||
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
chainId: laconic_9000-1
|
||||
gas: 1200000
|
||||
fees: 200000aphoton
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat > ./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
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:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat > ./records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: snowballtools-base-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully."
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
|
||||
# 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"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
# 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
|
||||
|
||||
sleep 2
|
||||
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,18 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@0.1.8
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
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:
|
||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
@@ -0,0 +1,8 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.2
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
name: snowballtools-base-frontend
|
||||
app_version: 0.1.8
|
||||
@@ -1,8 +0,0 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_GITEA_CLIENT_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:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
]
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@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",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
@@ -16,13 +27,17 @@
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"date-fns": "^3.0.1",
|
||||
"downshift": "^8.2.3",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"downshift": "^8.3.2",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"framer-motion": "^11.0.8",
|
||||
"gql-client": "^1.0.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"luxon": "^3.4.4",
|
||||
"octokit": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-calendar": "^4.8.0",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -34,8 +49,9 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
@@ -78,6 +94,6 @@
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"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>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="snowball tools dashboard"
|
||||
/>
|
||||
<meta name="description" content="snowball tools dashboard" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<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 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="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="%PUBLIC_URL%/apple-touch-icon.png"
|
||||
/>
|
||||
<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" />
|
||||
<title>Snowball</title>
|
||||
</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 |
File diff suppressed because one or more lines are too long
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "Snowball Tools Dashboard",
|
||||
"short_name": "snowball tools",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"name": "Snowball Tools Dashboard",
|
||||
"short_name": "snowball tools",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="currentColor"/>
|
||||
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1177_19315)">
|
||||
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="url(#paint0_linear_1177_19315)"/>
|
||||
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="#DBEBF9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1177_19315" x1="9.25" y1="0.650024" x2="9.25" y2="3.9698" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EBF6FF"/>
|
||||
<stop offset="1" stop-color="#F7FCFF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1177_19315">
|
||||
<rect width="19" height="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1175_19292)">
|
||||
<path d="M-9.5 0L-3.10445 2.69286C-1.11922 3.52875 1.11922 3.52875 3.10446 2.69286L6.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.69286L28.5 0" stroke="#082F56" stroke-opacity="0.1"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1175_19292">
|
||||
<rect width="19" height="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import OrgSlug from './pages/OrgSlug';
|
||||
import Projects from './pages/org-slug';
|
||||
import Settings from './pages/org-slug/Settings';
|
||||
import {
|
||||
@@ -11,11 +10,12 @@ import {
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import Index from './pages';
|
||||
import Login from './pages/Login';
|
||||
import { DashboardLayout } from 'pages/org-slug/layout';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: ':orgSlug',
|
||||
element: <OrgSlug />,
|
||||
element: <DashboardLayout />,
|
||||
children: [
|
||||
{
|
||||
element: <ProjectSearchLayout />,
|
||||
|
||||
@@ -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,37 @@
|
||||
export default [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: 'pwa',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Image Upload PWA',
|
||||
icon: 'pwa',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Swift',
|
||||
icon: 'swift',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
];
|
||||
@@ -126,7 +126,8 @@ const DatePicker = ({
|
||||
crossOrigin={undefined}
|
||||
/>
|
||||
</PopoverHandler>
|
||||
<PopoverContent>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<PopoverContent placeholder={''}>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
@@ -145,19 +146,23 @@ const DatePicker = ({
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<div className="flex justify-end">
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full mr-2"
|
||||
variant="outlined"
|
||||
onClick={() => setIsOpen(false)}
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
placeholder={''}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { Duration } from 'luxon';
|
||||
import React from 'react';
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
const FormatMillisecond = ({ time }: { time: number }) => {
|
||||
export interface FormatMilliSecondProps
|
||||
extends ComponentPropsWithoutRef<'div'> {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
|
||||
const formatTime = Duration.fromMillis(time)
|
||||
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
||||
.toObject();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
{...props}
|
||||
className={cn('text-sm text-elements-mid-em', props?.className)}
|
||||
>
|
||||
{formatTime.days !== 0 && <span>{formatTime.days}d </span>}
|
||||
{formatTime.hours !== 0 && <span>{formatTime.hours}h </span>}
|
||||
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m </span>}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Heading } from './shared/Heading';
|
||||
|
||||
interface LogoProps {
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
export const Logo = ({ orgSlug }: LogoProps) => {
|
||||
return (
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<div className="flex items-center gap-3 px-0 lg:px-2">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Snowball Logo"
|
||||
className="lg:h-10 lg:w-10 h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<Heading className="lg:text-[24px] text-[19px] font-semibold">
|
||||
Snowball
|
||||
</Heading>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +1,24 @@
|
||||
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<
|
||||
HTMLInputElement,
|
||||
InputProps & RefAttributes<HTMLInputElement>
|
||||
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
||||
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
|
||||
return (
|
||||
<div className="relative flex w-full gap-2">
|
||||
<div className="relative flex w-full">
|
||||
<Input
|
||||
leftIcon={<SearchIcon />}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
containerProps={{
|
||||
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}
|
||||
appearance="borderless"
|
||||
className="w-full lg:w-[459px]"
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
/>
|
||||
<div className="!absolute left-3 top-[13px]">
|
||||
<i>^</i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Organization } from 'gql-client';
|
||||
|
||||
import { Typography, Option } from '@material-tailwind/react';
|
||||
import { useDisconnect } from 'wagmi';
|
||||
|
||||
import { useGQLClient } from '../context/GQLClientContext';
|
||||
import AsyncSelect from './shared/AsyncSelect';
|
||||
|
||||
const Sidebar = () => {
|
||||
const { orgSlug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
|
||||
const fetchUserOrganizations = useCallback(async () => {
|
||||
const { organizations } = await client.getOrganizations();
|
||||
setOrganizations(organizations);
|
||||
}, [orgSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserOrganizations();
|
||||
setSelectedOrgSlug(orgSlug);
|
||||
}, [orgSlug]);
|
||||
|
||||
const handleLogOut = useCallback(() => {
|
||||
disconnect();
|
||||
navigate('/login');
|
||||
}, [disconnect, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4">
|
||||
<div className="grow">
|
||||
<div>
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<h3 className="text-black text-2xl">Snowball</h3>
|
||||
</Link>
|
||||
</div>
|
||||
<AsyncSelect
|
||||
className="bg-white py-2"
|
||||
value={selectedOrgSlug}
|
||||
onChange={(value) => {
|
||||
setSelectedOrgSlug(value!);
|
||||
navigate(`/${value}`);
|
||||
}}
|
||||
selected={(_, index) => (
|
||||
<div className="flex gap-2">
|
||||
<div>^</div>
|
||||
<div>
|
||||
<span>{organizations[index!]?.name}</span>
|
||||
<Typography>Organization</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{/* TODO: Show label organization and manage in option */}
|
||||
{organizations.map((org) => (
|
||||
<Option key={org.id} value={org.slug}>
|
||||
^ {org.name}
|
||||
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
<div>
|
||||
<NavLink
|
||||
to={`/${orgSlug}`}
|
||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
||||
>
|
||||
<Typography>Projects</Typography>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div>
|
||||
<NavLink
|
||||
to={`/${orgSlug}/settings`}
|
||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
||||
>
|
||||
<Typography>Settings</Typography>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow flex flex-col justify-end">
|
||||
<a className="cursor-pointer" onClick={handleLogOut}>
|
||||
Log Out
|
||||
</a>
|
||||
<a className="cursor-pointer">Documentation</a>
|
||||
<a className="cursor-pointer">Support</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useStopwatch } from 'react-timer-hook';
|
||||
|
||||
import FormatMillisecond from './FormatMilliSecond';
|
||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
|
||||
const setStopWatchOffset = (time: string) => {
|
||||
const providedTime = new Date(time);
|
||||
@@ -11,13 +11,17 @@ const setStopWatchOffset = (time: string) => {
|
||||
return currentTime;
|
||||
};
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => {
|
||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds } = useStopwatch({
|
||||
autoStart: true,
|
||||
offsetTimestamp: offsetTimestamp,
|
||||
});
|
||||
|
||||
return <FormatMillisecond time={totalSeconds * 1000} />;
|
||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||
};
|
||||
|
||||
export { Stopwatch, setStopWatchOffset };
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface CancelDeploymentDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
export const CancelDeploymentDialog = ({
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: CancelDeploymentDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Cancel deployment?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, cancel deployment"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
This will halt the deployment and you'll have to start the process
|
||||
from scratch.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import { Deployment, Domain } from 'gql-client';
|
||||
import React from 'react';
|
||||
import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ChevronDoubleDownIcon,
|
||||
LinkChainIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { TagProps } from 'components/shared/Tag';
|
||||
|
||||
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
||||
deployment: Deployment;
|
||||
newDeployment?: Deployment;
|
||||
domains: Domain[];
|
||||
}
|
||||
|
||||
export const ChangeStateToProductionDialog = ({
|
||||
deployment,
|
||||
newDeployment,
|
||||
domains,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: ChangeStateToProductionDialogProps) => {
|
||||
const currentChip = {
|
||||
value: 'Live Deployment',
|
||||
type: 'positive' as TagProps['type'],
|
||||
};
|
||||
const newChip = {
|
||||
value: 'New Deployment',
|
||||
type: 'attention' as TagProps['type'],
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
handleConfirm={handleConfirm}
|
||||
>
|
||||
<div className="flex flex-col gap-7">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</p>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={deployment}
|
||||
chip={newDeployment ? currentChip : undefined}
|
||||
/>
|
||||
{newDeployment && (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full text-elements-info">
|
||||
{Array.from({ length: 7 }).map((_, index) => (
|
||||
<ChevronDoubleDownIcon key={index} />
|
||||
))}
|
||||
</div>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={newDeployment}
|
||||
chip={newChip}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
The new deployment will be associated with these domains:
|
||||
</p>
|
||||
{domains.length > 0 &&
|
||||
domains.map((value) => {
|
||||
return (
|
||||
<Button
|
||||
as="a"
|
||||
href={value.name}
|
||||
leftIcon={<LinkChainIcon size={18} />}
|
||||
variant="link"
|
||||
key={value.id}
|
||||
>
|
||||
{value.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DeleteDomainDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
domainName: string;
|
||||
}
|
||||
|
||||
export const DeleteDomainDialog = ({
|
||||
projectName,
|
||||
domainName,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DeleteDomainDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Delete domain?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, delete domain"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Once deleted, the project{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{projectName}
|
||||
</span>{' '}
|
||||
will not be accessible from the domain{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{domainName}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DeleteVariableDialogProps extends ConfirmDialogProps {
|
||||
variableKey: string;
|
||||
}
|
||||
|
||||
export const DeleteVariableDialog = ({
|
||||
variableKey,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DeleteVariableDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Delete variable"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm delete"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-mid-em">
|
||||
Are you sure you want to delete the variable{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{variableKey}
|
||||
</span>
|
||||
?
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DeleteWebhookDialogProps extends ConfirmDialogProps {
|
||||
webhookUrl: string;
|
||||
}
|
||||
|
||||
export const DeleteWebhookDialog = ({
|
||||
webhookUrl,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DeleteWebhookDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Delete webhook?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm delete"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-mid-em">
|
||||
Are you sure you want to delete{' '}
|
||||
<span className="text-sm font-mono text-elements-high-em px-0.5">
|
||||
{webhookUrl}
|
||||
</span>
|
||||
?
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
export const DisconnectRepositoryDialog = ({
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DisconnectRepositoryDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Disconnect repository?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm disconnect"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Any data tied to your Git project may become misconfigured. Are you sure
|
||||
you want to continue?
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
import { formatAddress } from 'utils/format';
|
||||
|
||||
interface RemoveMemberDialogProps extends ConfirmDialogProps {
|
||||
memberName: string;
|
||||
ethAddress: string;
|
||||
emailDomain: string;
|
||||
}
|
||||
|
||||
export const RemoveMemberDialog = ({
|
||||
memberName,
|
||||
ethAddress,
|
||||
emailDomain,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: RemoveMemberDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Remove member?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, remove member"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@
|
||||
{emailDomain}) will not be able to access this project.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface TransferProjectDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export const TransferProjectDialog = ({
|
||||
projectName,
|
||||
from,
|
||||
to,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: TransferProjectDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Transfer project?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm transfer"
|
||||
handleConfirm={handleConfirm}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Upon confirmation, your project{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{projectName}
|
||||
</span>{' '}
|
||||
will be transferred from{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{from}
|
||||
</span>{' '}
|
||||
to{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{to}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -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,82 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const projectCardTheme = tv({
|
||||
slots: {
|
||||
wrapper: [
|
||||
'bg-surface-card',
|
||||
'shadow-card',
|
||||
'rounded-2xl',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'group',
|
||||
'cursor-pointer',
|
||||
],
|
||||
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
|
||||
content: ['flex', 'flex-col', 'gap-1', 'flex-1', 'overflow-hidden'],
|
||||
title: [
|
||||
'text-sm',
|
||||
'font-medium',
|
||||
'text-elements-high-em',
|
||||
'tracking-[-0.006em]',
|
||||
'truncate',
|
||||
],
|
||||
description: ['text-xs', 'text-elements-low-em', 'truncate'],
|
||||
icons: ['flex', 'items-center', 'gap-1'],
|
||||
lowerContent: [
|
||||
'transition-colors',
|
||||
'duration-150',
|
||||
'px-4',
|
||||
'py-4',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-2',
|
||||
'rounded-b-2xl',
|
||||
'group-hover:bg-surface-card-hovered',
|
||||
],
|
||||
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',
|
||||
],
|
||||
wavyBorder: [
|
||||
'bg-surface-card',
|
||||
'transition-colors',
|
||||
'duration-150',
|
||||
'group-hover:bg-surface-card-hovered',
|
||||
],
|
||||
},
|
||||
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,142 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Project } from 'gql-client';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
BranchIcon,
|
||||
ClockIcon,
|
||||
GitHubLogo,
|
||||
HorizontalDotIcon,
|
||||
WarningDiamondIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tooltip } from 'components/shared/Tooltip';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
|
||||
export interface ProjectCardProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
ProjectCardTheme {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
// TODO: Update the whole component to use `Link` from `react-router-dom` and remove the `useNavigate` hook,
|
||||
// currently it's not possible to use `Link` because the dot menu is not a direct child of the `Link` component
|
||||
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 navigate = useNavigate();
|
||||
|
||||
const handleOptionsClick = (
|
||||
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
navigate(`projects/${project.id}`);
|
||||
}, [project.id, navigate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={theme.wrapper({ className })}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Upper content */}
|
||||
<div className={theme.upperContent()}>
|
||||
{/* Icon container */}
|
||||
<Avatar
|
||||
size={48}
|
||||
imageSrc={project.icon}
|
||||
initials={getInitials(project.name)}
|
||||
/>
|
||||
{/* Title and website */}
|
||||
<div className={theme.content()}>
|
||||
<Tooltip content={project.name}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
</Tooltip>
|
||||
<p className={theme.description()}>
|
||||
{project.deployments[0]?.domain?.name ?? 'No domain'}
|
||||
</p>
|
||||
</div>
|
||||
{/* 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 variant="stroke-and-fill" className={theme.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';
|
||||
@@ -1,118 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemPrefix,
|
||||
Card,
|
||||
Typography,
|
||||
Avatar,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../SearchBar';
|
||||
import { useGQLClient } from '../../context/GQLClientContext';
|
||||
|
||||
interface ProjectsSearchProps {
|
||||
onChange?: (data: Project) => void;
|
||||
}
|
||||
|
||||
const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
} = useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
|
||||
if (onChange) {
|
||||
onChange(newSelectedItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedInputValue = useDebounce<string>(inputValue, 500);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchBar {...getInputProps()} />
|
||||
<Card
|
||||
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
|
||||
(!inputValue || !isOpen) && 'hidden'
|
||||
}`}
|
||||
>
|
||||
<List {...getMenuProps()}>
|
||||
{items.length ? (
|
||||
<>
|
||||
<div className="p-3">
|
||||
<Typography variant="small" color="gray">
|
||||
Suggestions
|
||||
</Typography>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<ListItem
|
||||
selected={highlightedIndex === index || selectedItem === item}
|
||||
key={item.id}
|
||||
{...getItemProps({ item, index })}
|
||||
>
|
||||
<ListItemPrefix>
|
||||
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
|
||||
</ListItemPrefix>
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray">
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
className="font-normal"
|
||||
>
|
||||
{item.organization.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<Typography>^ No projects matching this name</Typography>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSearchBar;
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import SearchBar from 'components/SearchBar';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
|
||||
interface ProjectSearchBarProps {
|
||||
onChange?: (data: Project) => void;
|
||||
}
|
||||
|
||||
export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
} = useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
|
||||
if (onChange) {
|
||||
onChange(newSelectedItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedInputValue = useDebounce<string>(inputValue, 300);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full lg:w-fit">
|
||||
<SearchBar {...getInputProps()} />
|
||||
<div
|
||||
{...getMenuProps()}
|
||||
className={cn(
|
||||
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
|
||||
{ hidden: !inputValue || !isOpen },
|
||||
)}
|
||||
>
|
||||
{items.length ? (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-elements-mid-em text-xs font-medium">
|
||||
Suggestions
|
||||
</p>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<ProjectSearchBarItem
|
||||
{...getItemProps({ item, index })}
|
||||
key={item.id}
|
||||
item={item}
|
||||
active={highlightedIndex === index || selectedItem === item}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<ProjectSearchBarEmpty />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCombobox } from 'downshift';
|
||||
|
||||
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
onClickItem?: (data: Project) => void;
|
||||
}
|
||||
|
||||
export const ProjectSearchBarDialog = ({
|
||||
onClose,
|
||||
onClickItem,
|
||||
...props
|
||||
}: ProjectSearchBarDialogProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { getInputProps, getItemProps, inputValue, setInputValue } =
|
||||
useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
onClickItem?.(newSelectedItem);
|
||||
navigate(
|
||||
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedInputValue = useDebounce<string>(inputValue, 300);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
const handleClose = () => {
|
||||
setInputValue('');
|
||||
setItems([]);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root {...props}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="bg-base-bg fixed inset-0 md:hidden overflow-y-auto" />
|
||||
<Dialog.Content>
|
||||
<div className="h-full flex flex-col fixed top-0 inset-0">
|
||||
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
||||
<Input
|
||||
{...getInputProps()}
|
||||
leftIcon={<SearchIcon />}
|
||||
placeholder="Search"
|
||||
appearance="borderless"
|
||||
autoFocus
|
||||
/>
|
||||
<Button iconOnly variant="ghost" onClick={handleClose}>
|
||||
<CrossIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-col gap-1 px-2 py-2">
|
||||
{items.length > 0
|
||||
? items.map((item, index) => (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-elements-mid-em text-xs font-medium">
|
||||
Suggestions
|
||||
</p>
|
||||
</div>
|
||||
<ProjectSearchBarItem
|
||||
{...getItemProps({ item, index })}
|
||||
key={item.id}
|
||||
item={item}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
: inputValue && <ProjectSearchBarEmpty />}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { InfoRoundFilledIcon } from 'components/shared/CustomIcon';
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
|
||||
|
||||
export const ProjectSearchBarEmpty = ({
|
||||
className,
|
||||
...props
|
||||
}: ProjectSearchBarEmptyProps) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex items-center px-2 py-2 gap-3', className)}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning">
|
||||
<InfoRoundFilledIcon size={16} />
|
||||
</div>
|
||||
<p className="text-elements-low-em text-sm tracking-[-0.006em]">
|
||||
No projects matching this name
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import React, { ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||
import { OmitCommon } from 'types/common';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
|
||||
/**
|
||||
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
|
||||
* @type {MergedComponentPropsWithoutRef}
|
||||
*/
|
||||
type MergedComponentPropsWithoutRef = OmitCommon<
|
||||
ComponentPropsWithoutRef<'button'>,
|
||||
Omit<
|
||||
Overwrite<UseComboboxGetItemPropsReturnValue, Project[]>,
|
||||
'index' | 'item'
|
||||
>
|
||||
>;
|
||||
|
||||
interface ProjectSearchBarItemProps extends MergedComponentPropsWithoutRef {
|
||||
item: Project;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const ProjectSearchBarItem = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ProjectSearchBarItemProps
|
||||
>(({ item, active, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
ref={ref}
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'px-2 py-2 flex items-center gap-3 rounded-lg text-left hover:bg-base-bg-emphasized',
|
||||
{
|
||||
'bg-base-bg-emphasized': active,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
imageSrc={item.icon}
|
||||
initials={getInitials(item.name)}
|
||||
/>
|
||||
<div className="flex flex-col flex-1">
|
||||
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
|
||||
{item.name}
|
||||
</p>
|
||||
<p className="text-xs text-elements-low-em">{item.organization.name}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectSearchBarItem.displayName = 'ProjectSearchBarItem';
|
||||
|
||||
export { ProjectSearchBarItem };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ProjectSearchBar';
|
||||
export * from './ProjectSearchBarDialog';
|
||||
@@ -1,73 +1,103 @@
|
||||
import React from 'react';
|
||||
import OauthPopup from 'react-oauth-popup';
|
||||
import { GitType } from 'gql-client';
|
||||
|
||||
import { Button } from '@material-tailwind/react';
|
||||
|
||||
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';
|
||||
import { MockConnectGitCard } from './MockConnectGitCard';
|
||||
|
||||
const SCOPES = 'repo user';
|
||||
|
||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.REACT_APP_GITHUB_CLIENT_ID
|
||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
||||
|
||||
const REDIRECT_URI = `${window.location.origin}/organization/projects/create`;
|
||||
const GITEA_OAUTH_URL = `https://git.vdb.to/login/oauth/authorize?client_id=${process.env.REACT_APP_GITEA_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`;
|
||||
|
||||
interface ConnectAccountInterface {
|
||||
onAuth: (token: string) => void;
|
||||
}
|
||||
|
||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
onAuth: onToken,
|
||||
}: ConnectAccountInterface) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const handleCode = async (type: GitType, code: string) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
const handleCode = async (code: string) => {
|
||||
// Pass code to backend and get access token
|
||||
const {
|
||||
authenticateGit: { token },
|
||||
} = await client.authenticateGit(type, code);
|
||||
|
||||
// TODO: Handle token according to Git type
|
||||
authenticateGitHub: { token },
|
||||
} = await client.authenticateGitHub(code);
|
||||
onToken(token);
|
||||
toast({
|
||||
onDismiss: dismiss,
|
||||
id: 'connected-to-github',
|
||||
title: 'The Git account is connected.',
|
||||
variant: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Use correct height
|
||||
return (
|
||||
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||
<div>^</div>
|
||||
<div>
|
||||
<p>Connect to your git account</p>
|
||||
<p>
|
||||
Once connected, you can import a repository from your
|
||||
<br />
|
||||
account or start with one of our templates.
|
||||
</p>
|
||||
<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 className="flex flex-col items-center max-w-[420px]">
|
||||
{/** Icons */}
|
||||
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
|
||||
<IconWithFrame icon={<GitIcon />} />
|
||||
<EllipsesIcon className="items-center gap-1.5 flex" />
|
||||
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
|
||||
</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>
|
||||
</div>
|
||||
{/** CTA Buttons */}
|
||||
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
|
||||
<OauthPopup
|
||||
url={GITHUB_OAUTH_URL}
|
||||
onCode={handleCode}
|
||||
onClose={() => {}}
|
||||
title="Snowball"
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
leftIcon={<GithubIcon />}
|
||||
variant="tertiary"
|
||||
>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</OauthPopup>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
leftIcon={<GitTeaIcon />}
|
||||
variant="tertiary"
|
||||
>
|
||||
Connect to GitTea
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex">
|
||||
<OauthPopup
|
||||
url={GITHUB_OAUTH_URL}
|
||||
onCode={(code) => handleCode(GitType.GitHub, code)}
|
||||
onClose={() => {}}
|
||||
title="Snowball"
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button className="rounded-full mx-2">Connect to Github</Button>
|
||||
</OauthPopup>
|
||||
<OauthPopup
|
||||
url={GITEA_OAUTH_URL}
|
||||
onCode={(code) => handleCode(GitType.Gitea, code)}
|
||||
onClose={() => {}}
|
||||
title="Snowball"
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
||||
</OauthPopup>
|
||||
</div>
|
||||
<ConnectAccountTabPanel />
|
||||
|
||||
{/* TODO: Add ConnectAccountTabPanel */}
|
||||
<MockConnectGitCard />
|
||||
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||
<ConnectAccountTabPanel />
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
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 (
|
||||
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
|
||||
<TabsHeader className="grid grid-cols-2">
|
||||
<Tab className="row-span-1" value="import">
|
||||
Import a repository
|
||||
</Tab>
|
||||
<Tab className="row-span-2" value="template">
|
||||
Start with a template
|
||||
</Tab>
|
||||
</TabsHeader>
|
||||
{/* <TabsBody> */}
|
||||
{/* TODO: Add content */}
|
||||
{/* </TabsBody> */}
|
||||
<Tabs
|
||||
defaultValue="Connect Accounts Tab Panel"
|
||||
orientation="horizontal"
|
||||
className="mt-10"
|
||||
>
|
||||
<Tabs.List>
|
||||
{[
|
||||
{ title: 'Import a repository' },
|
||||
{ title: 'Start with a template' },
|
||||
].map(({ title }, index) => (
|
||||
<Tabs.Trigger value={title} key={index}>
|
||||
{title}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import ConfirmDialog from '../../shared/ConfirmDialog';
|
||||
import { Stopwatch, setStopWatchOffset } from 'components/StopWatch';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon';
|
||||
import { CancelDeploymentDialog } from 'components/projects/Dialog/CancelDeploymentDialog';
|
||||
|
||||
const INTERVAL_DURATION = 5000;
|
||||
const TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = searchParams.get('projectId');
|
||||
@@ -25,64 +26,64 @@ const Deploy = () => {
|
||||
useEffect(() => {
|
||||
const timerID = setTimeout(() => {
|
||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||
}, INTERVAL_DURATION);
|
||||
}, TIMEOUT_DURATION);
|
||||
|
||||
return () => clearInterval(timerID);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between mb-6">
|
||||
<div>
|
||||
<h4>Deployment started ...</h4>
|
||||
<div className="flex">
|
||||
^
|
||||
<div className="space-y-7">
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<Heading as="h4" className="md:text-lg font-medium">
|
||||
Deployment started ...
|
||||
</Heading>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={handleOpen} variant="outlined" size="sm">
|
||||
^ Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Cancel deployment?"
|
||||
handleOpen={handleOpen}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, Cancel deployment"
|
||||
handleConfirm={handleCancel}
|
||||
color="red"
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
leftIcon={<WarningIcon size={16} />}
|
||||
>
|
||||
<Typography variant="small">
|
||||
This will halt the deployment and you will have to start the process
|
||||
from scratch.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
Cancel
|
||||
</Button>
|
||||
<CancelDeploymentDialog
|
||||
handleCancel={handleOpen}
|
||||
open={open}
|
||||
handleConfirm={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
<DeployStep
|
||||
title="Running checks"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="3"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Assigning domains"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
</div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
<DeployStep
|
||||
title="Running checks"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="3"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Assigning domains"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Collapse, Button, Typography } from '@material-tailwind/react';
|
||||
import { Collapse } from '@material-tailwind/react';
|
||||
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import FormatMillisecond from '../../FormatMilliSecond';
|
||||
import processLogs from '../../../assets/process-logs.json';
|
||||
import { cn } from 'utils/classnames';
|
||||
import {
|
||||
CheckRoundFilledIcon,
|
||||
ClockOutlineIcon,
|
||||
CopyIcon,
|
||||
LoaderIcon,
|
||||
MinusCircleIcon,
|
||||
PlusIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { useIntersectionObserver } from 'usehooks-ts';
|
||||
|
||||
enum DeployStatus {
|
||||
PROCESSING = 'progress',
|
||||
@@ -28,55 +39,115 @@ const DeployStep = ({
|
||||
startTime,
|
||||
processTime,
|
||||
}: DeployStepsProps) => {
|
||||
const [collapse, setCollapse] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { toast, dismiss } = useToast();
|
||||
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
||||
threshold: 1,
|
||||
});
|
||||
|
||||
const disableCollapse = status !== DeployStatus.COMPLETE;
|
||||
|
||||
return (
|
||||
<div className="border-b-2">
|
||||
<div className="flex justify-between p-2 gap-2">
|
||||
{status === DeployStatus.NOT_STARTED && <div>{step}</div>}
|
||||
{status === DeployStatus.PROCESSING && <div>O</div>}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCollapse(!collapse);
|
||||
}}
|
||||
>
|
||||
{collapse ? '-' : '+'}
|
||||
</button>
|
||||
<div className="border-b border-border-separator">
|
||||
{/* Collapisble trigger */}
|
||||
<button
|
||||
className={cn(
|
||||
'flex justify-between w-full py-5 gap-2',
|
||||
disableCollapse && 'cursor-auto',
|
||||
)}
|
||||
tabIndex={disableCollapse ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!disableCollapse) {
|
||||
setIsOpen((val) => !val);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={cn('grow flex items-center gap-3')}>
|
||||
{/* Icon */}
|
||||
<div className="w-6 h-6 grid place-content-center">
|
||||
{status === DeployStatus.NOT_STARTED && (
|
||||
<div className="grid place-content-center w-6 h-6 rounded-[48px] bg-base-bg-emphasized font-mono text-xs">
|
||||
{step}
|
||||
</div>
|
||||
)}
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<LoaderIcon className="animate-spin text-elements-link" />
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div className="text-controls-primary">
|
||||
{!isOpen && <PlusIcon size={24} />}
|
||||
{isOpen && <MinusCircleIcon size={24} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<span
|
||||
className={cn(
|
||||
'text-left text-sm md:text-base',
|
||||
status === DeployStatus.PROCESSING && 'text-elements-link',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Timer */}
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="grow">{title}</div>
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<>
|
||||
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</>
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<>
|
||||
^<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4.5 h-4.5 grid place-content-center">
|
||||
<CheckRoundFilledIcon
|
||||
className="text-elements-success"
|
||||
size={15}
|
||||
/>
|
||||
</div>
|
||||
<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Collapse open={collapse}>
|
||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
||||
</button>
|
||||
|
||||
{/* Collapsible */}
|
||||
<Collapse open={isOpen}>
|
||||
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
|
||||
{/* Logs */}
|
||||
{processLogs.map((log, key) => {
|
||||
return (
|
||||
<Typography variant="small" color="gray" key={key}>
|
||||
<p className="font-mono" key={key}>
|
||||
{log}
|
||||
</Typography>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
<div className="sticky bottom-0 left-1/2 flex justify-center">
|
||||
|
||||
{/* End of logs ref used for hiding gradient overlay */}
|
||||
<div ref={ref} />
|
||||
|
||||
{/* Overflow gradient overlay */}
|
||||
{!hideGradientOverlay && (
|
||||
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
|
||||
)}
|
||||
|
||||
{/* Copy log button */}
|
||||
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
|
||||
<Button
|
||||
size="sm"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(processLogs.join('\n'));
|
||||
toast.success('Logs copied');
|
||||
toast({
|
||||
title: 'Logs copied',
|
||||
variant: 'success',
|
||||
id: 'logs',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}}
|
||||
color="blue"
|
||||
leftIcon={<CopyIcon size={16} />}
|
||||
>
|
||||
^ Copy log
|
||||
Copy log
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { SegmentedControls } from 'components/shared/SegmentedControls';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
GithubIcon,
|
||||
LockIcon,
|
||||
TemplateIcon,
|
||||
TemplateIconType,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
export const MockConnectGitCard = () => {
|
||||
const [segmentedControlsValue, setSegmentedControlsValue] =
|
||||
useState<string>('import');
|
||||
|
||||
const isDesktopView = useMediaQuery('(min-width: 960px)'); // lg:
|
||||
const segmentedControlsProps = isDesktopView ? {} : { size: 'sm' as const };
|
||||
|
||||
const fiveMinutesAgo = new Date(Date.now() - 1000 * 300).toISOString();
|
||||
|
||||
const SEGMENTED_CONTROLS_OPTIONS = [
|
||||
{
|
||||
label: 'Import a repository',
|
||||
value: 'import',
|
||||
},
|
||||
{
|
||||
label: 'Start with a template',
|
||||
value: 'template',
|
||||
},
|
||||
];
|
||||
|
||||
const IMPORT_CONTENT = [
|
||||
{
|
||||
full_name: 'snowball/igloo',
|
||||
updated_at: fiveMinutesAgo,
|
||||
},
|
||||
{
|
||||
full_name: 'snowball/android-sdk',
|
||||
updated_at: fiveMinutesAgo,
|
||||
visibility: 'private',
|
||||
},
|
||||
{
|
||||
full_name: 'snowball/landing-page',
|
||||
updated_at: fiveMinutesAgo,
|
||||
},
|
||||
];
|
||||
|
||||
const TEMPLATE_CONTENT = [
|
||||
{
|
||||
name: 'Web app',
|
||||
icon: 'web',
|
||||
},
|
||||
{
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: 'pwa',
|
||||
},
|
||||
{
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
},
|
||||
{
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
},
|
||||
{
|
||||
name: 'Swift',
|
||||
icon: 'swift',
|
||||
},
|
||||
];
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (segmentedControlsValue === 'import') {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 relative z-0">
|
||||
{IMPORT_CONTENT.map((repo, index) => (
|
||||
<>
|
||||
<MockProjectCard key={index} {...repo} />
|
||||
{index !== IMPORT_CONTENT.length - 1 && (
|
||||
<div className="border-b border-base-border" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 relative z-0">
|
||||
{TEMPLATE_CONTENT.map((template, index) => (
|
||||
<MockTemplateCard key={index} {...template} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [segmentedControlsValue]);
|
||||
|
||||
return (
|
||||
<div className="relative bg-base-bg shadow-card rounded-2xl px-2 py-2 w-full max-w-[560px] flex flex-col gap-2">
|
||||
{/* Content */}
|
||||
<SegmentedControls
|
||||
value={segmentedControlsValue}
|
||||
onChange={setSegmentedControlsValue}
|
||||
options={SEGMENTED_CONTROLS_OPTIONS}
|
||||
type="full-width"
|
||||
{...segmentedControlsProps}
|
||||
/>
|
||||
{renderContent}
|
||||
|
||||
{/* Shade */}
|
||||
<div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white to-transparent" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MockProjectCard = ({
|
||||
full_name,
|
||||
updated_at,
|
||||
visibility,
|
||||
}: {
|
||||
full_name: string;
|
||||
updated_at?: string;
|
||||
visibility?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative">
|
||||
{/* Icon container */}
|
||||
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
|
||||
<GithubIcon />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-1 gap-3 flex-wrap">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
|
||||
{full_name}
|
||||
</p>
|
||||
<p className="text-elements-low-em text-xs">
|
||||
{updated_at && relativeTimeISO(updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
{visibility === 'private' && (
|
||||
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
|
||||
<LockIcon />
|
||||
Private
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MockTemplateCard = ({ icon, name }: { icon: string; name: string }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-3 py-3 hover:bg-base-bg-emphasized rounded-2xl group relative cursor-default">
|
||||
{/* Icon */}
|
||||
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
|
||||
<TemplateIcon type={icon as TemplateIconType} />
|
||||
</div>
|
||||
{/* Name */}
|
||||
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from '../../../utils/time';
|
||||
import { GitRepositoryDetails } from '../../../types';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const createProject = useCallback(async () => {
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug!, {
|
||||
name: `${repository.owner!.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch!,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
|
||||
if (Boolean(addProject)) {
|
||||
setIsLoading(false);
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
toast.error('Failed to create project');
|
||||
}
|
||||
}, [client, repository]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
||||
onClick={createProject}
|
||||
>
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<div>
|
||||
<span className="text-black">{repository.full_name}</span>
|
||||
{repository.visibility === 'private' && (
|
||||
<Chip
|
||||
className="normal-case inline ml-6 font-normal"
|
||||
size="sm"
|
||||
value="Private"
|
||||
icon={'^'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm">{'>'}</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRepoCard;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user