forked from cerc-io/snowballtools-base
Compare commits
178
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 | ||
|
|
aa5507f309 | ||
|
|
91a80cac8a | ||
|
|
ee397d0e6e | ||
|
|
98ec1915ca | ||
|
|
f08081932c | ||
|
|
fe427a9fb8 | ||
|
|
a5b4103515 | ||
|
|
f77f7c120a | ||
|
|
22f3cbafb7 | ||
|
|
30c50a3eec | ||
|
|
0381fdbbed | ||
|
|
2f466d4fbb | ||
|
|
ee35e64f69 | ||
|
|
075cec58e5 | ||
|
|
d52a34da35 | ||
|
|
eb4ccfbc9c | ||
|
|
769593913e | ||
|
|
eda2cc76ed | ||
|
|
99eb514306 |
@@ -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,250 +21,212 @@
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
## Backend
|
||||
|
||||
- Change directory to `packages/backend`
|
||||
|
||||
```bash
|
||||
cd packages/backend
|
||||
```
|
||||
|
||||
- Rename backend config file from [environments/local.toml.example](packages/backend/environments/local.toml.example) to `local.toml`
|
||||
|
||||
```bash
|
||||
mv environments/local.toml.example environments/local.toml
|
||||
```
|
||||
|
||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||
- 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
|
||||
```
|
||||
|
||||
- Rename [.env.example](packages/frontend/.env.example) to `.env`
|
||||
|
||||
```bash
|
||||
mv .env.example .env
|
||||
```
|
||||
|
||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||
|
||||
```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
|
||||
# Set actual owner/name of the template repo that will be used for creating new repo
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = cerc-io/image-upload-pwa-example
|
||||
```
|
||||
|
||||
### Frontend Production
|
||||
|
||||
- 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/`
|
||||
|
||||
@@ -9,14 +9,6 @@ if [[ -d "$DEST_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$PKG_DIR/.env" ]]; then
|
||||
echo "Using existing .env file"
|
||||
else
|
||||
mv "$PKG_DIR/.env.example" "$PKG_DIR/.env"
|
||||
echo "Created .env file. Please populate with the correct values."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > $PKG_DIR/.env <<EOF
|
||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
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 {
|
||||
@@ -111,16 +113,21 @@ export class Registry {
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -139,9 +146,9 @@ export class Registry {
|
||||
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;
|
||||
@@ -160,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',
|
||||
@@ -178,6 +185,7 @@ export class Registry {
|
||||
})
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
@@ -211,11 +219,12 @@ export class Registry {
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationRecord ids
|
||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationRecordId === record.attributes.application
|
||||
deployment.applicationRecordId === record.attributes.application &&
|
||||
record.attributes.url.includes(deployment.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -382,8 +382,7 @@ 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(
|
||||
@@ -407,13 +406,10 @@ 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 } =
|
||||
@@ -422,7 +418,7 @@ export class Service {
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl: recordData.repoUrl
|
||||
repoUrl
|
||||
});
|
||||
|
||||
// Update previous deployment with prod branch domain
|
||||
@@ -464,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;
|
||||
@@ -498,8 +506,6 @@ export class Service {
|
||||
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,
|
||||
@@ -510,9 +516,6 @@ export class Service {
|
||||
domain: null,
|
||||
commitHash: latestCommit.sha,
|
||||
commitMessage: latestCommit.commit.message
|
||||
},
|
||||
{
|
||||
repoUrl: repoDetails.html_url
|
||||
}
|
||||
);
|
||||
|
||||
@@ -555,8 +558,14 @@ 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 { 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 }
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface GitPushEventPayload {
|
||||
id: string;
|
||||
message: string;
|
||||
};
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
|
||||
@@ -66,3 +66,5 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
return savedEntity;
|
||||
};
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
@@ -8,57 +8,6 @@
|
||||
brew install jq # if you do not have jq installed already
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
Example of how to make the necessary deploy edits [here](https://github.com/snowball-tools/snowballtools-base/pull/131/files).
|
||||
|
||||
- Replace variables in the following files
|
||||
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||
- update the name & application version numbers
|
||||
- `<CURRENT_DATE_TIME>`: Replace with current time which can be generated by command `date -u`
|
||||
```yml
|
||||
# Example
|
||||
record:
|
||||
...
|
||||
meta:
|
||||
note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC
|
||||
...
|
||||
```
|
||||
|
||||
- Update record version in [records/application-record.yml](records/application-record.yml)
|
||||
```yml
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: <NEW_VERSION>
|
||||
...
|
||||
```
|
||||
|
||||
- Update commit hash in the following places:
|
||||
- [records/application-record.yml](records/application-record.yml)
|
||||
```yml
|
||||
record:
|
||||
...
|
||||
repository_ref: <COMMIT_HASH>
|
||||
...
|
||||
```
|
||||
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||
```yml
|
||||
record:
|
||||
...
|
||||
meta:
|
||||
...
|
||||
repository_ref: <COMMIT_HASH>
|
||||
```
|
||||
- [deploy-frontend.sh](deploy-frontend.sh)
|
||||
Also be sure to update the app version
|
||||
```bash
|
||||
...
|
||||
RCD_APP_VERSION="<NEW_VERSION>"
|
||||
REPO_REF="<COMMIT_HASH>"
|
||||
...
|
||||
```
|
||||
|
||||
- Run script to deploy app
|
||||
```
|
||||
./deploy-frontend.sh
|
||||
@@ -81,8 +30,8 @@ Example of how to make the necessary deploy edits [here](https://github.com/snow
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
- Check deployment status [here](https://console.laconic.com/deployer).
|
||||
- Check records [here](https://console.laconic.com/#/registry).
|
||||
- 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
|
||||
@@ -91,9 +40,9 @@ Example of how to make the necessary deploy edits [here](https://github.com/snow
|
||||
yarn laconic cns account get
|
||||
|
||||
# Bond balance
|
||||
yarn laconic cns bond get --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
```
|
||||
- Command to refill bond
|
||||
```bash
|
||||
yarn laconic cns bond refill --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac --type aphoton --quantity 10000000
|
||||
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
||||
```
|
||||
|
||||
@@ -2,8 +2,8 @@ services:
|
||||
cns:
|
||||
restEndpoint: http://console.laconic.com:1317
|
||||
gqlEndpoint: http://console.laconic.com:9473/api
|
||||
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
||||
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
chainId: laconic_9000-1
|
||||
gas: 1000000
|
||||
gas: 1200000
|
||||
fees: 200000aphoton
|
||||
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
|
||||
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||
|
||||
@@ -1,11 +1,67 @@
|
||||
#!/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
|
||||
CONFIG_FILE=config.yml
|
||||
RCD_APP_VERSION="0.1.3"
|
||||
REPO_REF="513ca69d01bee857cf207a0605483205b384e218"
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
@@ -15,8 +71,11 @@ echo $RECORD_ID
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${RCD_APP_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${REPO_REF}" "$RECORD_ID"
|
||||
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"
|
||||
@@ -30,6 +89,7 @@ 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
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@0.1.3
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.3
|
||||
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
|
||||
# If GitHub client ID is changed, same ID and corresponding secret has to be set in backend config
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
# New config env after changes for image upload PWA
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
# Set CURRENT_DATE_TIME; Use command date -u
|
||||
note: Added by Snowball @ Tue Feb 27 17:24:06 UTC 2024
|
||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
repository_ref: 513ca69d01bee857cf207a0605483205b384e218
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.11
|
||||
repository_ref: 513ca69d01bee857cf207a0605483205b384e218
|
||||
version: 0.0.2
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
# name is set to repo name
|
||||
name: snowballtools-base-frontend
|
||||
# app_version is set from package.json
|
||||
app_version: 0.1.3
|
||||
app_version: 0.1.8
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
@@ -8,6 +8,7 @@
|
||||
"@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",
|
||||
@@ -30,7 +31,9 @@
|
||||
"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",
|
||||
@@ -48,7 +51,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",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -18,14 +18,14 @@ export default [
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
repoFullName: '',
|
||||
isComingSoon: false,
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
repoFullName: '',
|
||||
isComingSoon: false,
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,8 @@ const SearchBar: React.ForwardRefRenderFunction<
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
appearance={'borderless'}
|
||||
appearance="borderless"
|
||||
className="w-full lg:w-[459px]"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -9,16 +9,18 @@ export const projectCardTheme = tv({
|
||||
'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'],
|
||||
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'],
|
||||
description: ['text-xs', 'text-elements-low-em', 'truncate'],
|
||||
icons: ['flex', 'items-center', 'gap-1'],
|
||||
lowerContent: [
|
||||
'transition-colors',
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Project } from 'gql-client';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import {
|
||||
BranchIcon,
|
||||
ClockIcon,
|
||||
@@ -14,16 +20,11 @@ import {
|
||||
HorizontalDotIcon,
|
||||
WarningDiamondIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tooltip } from 'components/shared/Tooltip';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
|
||||
export interface ProjectCardProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
@@ -72,7 +73,9 @@ export const ProjectCard = ({
|
||||
/>
|
||||
{/* Title and website */}
|
||||
<div className={theme.content()}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
<Tooltip content={project.name}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
</Tooltip>
|
||||
<p className={theme.description()}>
|
||||
{project.deployments[0]?.domain?.name ?? 'No domain'}
|
||||
</p>
|
||||
|
||||
@@ -1,127 +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'
|
||||
}`}
|
||||
placeholder={''}
|
||||
>
|
||||
<List {...getMenuProps()}>
|
||||
{items.length ? (
|
||||
<>
|
||||
<div className="p-3">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
Suggestions
|
||||
</Typography>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<ListItem
|
||||
selected={highlightedIndex === index || selectedItem === item}
|
||||
key={item.id}
|
||||
placeholder={''}
|
||||
{...getItemProps({ item, index })}
|
||||
>
|
||||
<ListItemPrefix placeholder={''}>
|
||||
<Avatar
|
||||
src={item.icon || '/gray.png'}
|
||||
variant="rounded"
|
||||
placeholder={''}
|
||||
/>
|
||||
</ListItemPrefix>
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray" placeholder={''}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
className="font-normal"
|
||||
placeholder={''}
|
||||
>
|
||||
{item.organization.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<Typography placeholder={''}>
|
||||
^ 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';
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
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=${
|
||||
@@ -93,6 +94,7 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
</div>
|
||||
|
||||
{/* TODO: Add ConnectAccountTabPanel */}
|
||||
<MockConnectGitCard />
|
||||
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||
<ConnectAccountTabPanel />
|
||||
</div> */}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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 'components/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 TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
@@ -31,63 +32,58 @@ const Deploy = () => {
|
||||
}, []);
|
||||
|
||||
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"
|
||||
placeholder={''}
|
||||
>
|
||||
^ 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" placeholder={''}>
|
||||
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,61 +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}
|
||||
placeholder={''}
|
||||
>
|
||||
<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"
|
||||
placeholder={''}
|
||||
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,81 +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';
|
||||
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
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 className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
|
||||
<GithubIcon />
|
||||
</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 text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
|
||||
size="sm"
|
||||
value="Private"
|
||||
icon={<LockIcon />}
|
||||
/>
|
||||
)}
|
||||
</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" placeholder={''}>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRepoCard;
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Spinner } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import { GitRepositoryDetails } from 'types';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
GithubIcon,
|
||||
LockIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
repository,
|
||||
}) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
const createProject = useCallback(async () => {
|
||||
if (!repository || !orgSlug) {
|
||||
return toast({
|
||||
id: 'missing-repository-or-org-slug',
|
||||
title: 'Repository or organization slug is missing',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug, {
|
||||
name: `${repository.owner?.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch as string,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
if (addProject) {
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error((error as Error).message);
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-start sm:items-center gap-3 px-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"
|
||||
onClick={createProject}
|
||||
>
|
||||
{/* 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 gap-1">
|
||||
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
|
||||
{repository.full_name}
|
||||
</p>
|
||||
<p className="text-elements-low-em text-xs">
|
||||
{repository.updated_at && relativeTimeISO(repository.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
{repository.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>
|
||||
{/* Right action */}
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4 absolute right-3" />
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
iconOnly
|
||||
className="sm:group-hover:flex hidden absolute right-3"
|
||||
>
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ProjectRepoCard';
|
||||
+68
-49
@@ -3,13 +3,17 @@ import { Octokit } from 'octokit';
|
||||
import assert from 'assert';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { Button, Typography, Option } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../SearchBar';
|
||||
import ProjectRepoCard from './ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
||||
import AsyncSelect from '../../shared/AsyncSelect';
|
||||
import { GithubIcon } from 'components/shared/CustomIcon';
|
||||
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from 'types';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
GithubIcon,
|
||||
RefreshIcon,
|
||||
SearchIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
@@ -18,9 +22,9 @@ interface RepositoryListProps {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
// TODO: Add new type for Git user when required
|
||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||
@@ -35,7 +39,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount(user.data.login);
|
||||
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||
};
|
||||
|
||||
fetchUserAndOrgs();
|
||||
@@ -54,7 +58,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||
|
||||
// Check if selected account is an organization
|
||||
if (selectedAccount === gitUser.login) {
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
query = query + ` user:${selectedAccount}`;
|
||||
} else {
|
||||
query = query + ` org:${selectedAccount}`;
|
||||
@@ -69,7 +73,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAccount === gitUser.login) {
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
per_page: REPOS_PER_PAGE,
|
||||
affiliation: 'owner',
|
||||
@@ -78,7 +82,9 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOrg = orgs.find((org) => org.login === selectedAccount);
|
||||
const selectedOrg = orgs.find(
|
||||
(org) => org.login === selectedAccount.value,
|
||||
);
|
||||
assert(selectedOrg, 'Selected org not found in list');
|
||||
|
||||
const result = await octokit.rest.repos.listForOrg({
|
||||
@@ -96,7 +102,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const handleResetFilters = useCallback(() => {
|
||||
assert(gitUser, 'Git user is not available');
|
||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||
setSelectedAccount(gitUser.login);
|
||||
setSelectedAccount({ label: gitUser.login, value: gitUser.login });
|
||||
}, [gitUser]);
|
||||
|
||||
const accounts = useMemo(() => {
|
||||
@@ -107,52 +113,65 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return [gitUser, ...orgs];
|
||||
}, [octokit, orgs, gitUser]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return accounts.map((account) => ({
|
||||
label: account.login,
|
||||
value: account.login,
|
||||
leftIcon: <GithubIcon />,
|
||||
}));
|
||||
}, [accounts]);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex gap-2 mb-2 items-center">
|
||||
<div className="basis-1/3">
|
||||
<AsyncSelect
|
||||
<section className="space-y-3">
|
||||
{/* Dropdown and search */}
|
||||
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
|
||||
<div className="lg:basis-1/3 w-full">
|
||||
<Select
|
||||
options={options}
|
||||
placeholder="Select a repository"
|
||||
value={selectedAccount}
|
||||
onChange={(value) => setSelectedAccount(value!)}
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<Option key={account.id} value={account.login}>
|
||||
<div className="flex items-center gap-2 justify-start">
|
||||
<GithubIcon /> {account.login}
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
leftIcon={selectedAccount ? <GithubIcon /> : undefined}
|
||||
rightIcon={<ChevronGrabberHorizontal />}
|
||||
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||
/>
|
||||
</div>
|
||||
<div className="basis-2/3 flex-grow flex items-center">
|
||||
<SearchBar
|
||||
<div className="basis-2/3 flex w-full flex-grow">
|
||||
<Input
|
||||
className="w-full"
|
||||
value={searchedRepo}
|
||||
onChange={(event) => setSearchedRepo(event.target.value)}
|
||||
placeholder="Search for repository"
|
||||
leftIcon={<SearchIcon />}
|
||||
onChange={(e) => setSearchedRepo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Repository list */}
|
||||
{Boolean(repositoryDetails.length) ? (
|
||||
repositoryDetails.map((repo, key) => {
|
||||
return <ProjectRepoCard repository={repo} key={key} />;
|
||||
})
|
||||
<div className="flex flex-col gap-2">
|
||||
{repositoryDetails.map((repo, index) => (
|
||||
<>
|
||||
<ProjectRepoCard repository={repo} key={index} />
|
||||
{/* Horizontal line */}
|
||||
{index !== repositoryDetails.length - 1 && (
|
||||
<div className="border-b border-border-separator/[0.06] w-full" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Typography placeholder={''}>No repository found</Typography>
|
||||
<Button
|
||||
className="rounded-full mt-5"
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Reset filters
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 p-6 flex flex-col gap-4 items-center justify-center">
|
||||
<p className="text-elements-high-em font-sans">No repository found</p>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
leftIcon={<RefreshIcon />}
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Reset filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryList;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RepositoryList';
|
||||
@@ -1,3 +1,4 @@
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
@@ -6,8 +7,7 @@ import {
|
||||
TemplateIconType,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
@@ -24,9 +24,13 @@ export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> {
|
||||
isGitAuth: boolean;
|
||||
}
|
||||
|
||||
export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
template,
|
||||
isGitAuth,
|
||||
}: TemplateCardProps) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (template?.isComingSoon) {
|
||||
@@ -38,7 +42,9 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
});
|
||||
}
|
||||
if (isGitAuth) {
|
||||
return navigate(`/template?templateId=${template.id}`);
|
||||
return navigate(
|
||||
`/${orgSlug}/projects/create/template?templateId=${template.id}`,
|
||||
);
|
||||
}
|
||||
return toast({
|
||||
id: 'connect-git-account',
|
||||
@@ -46,12 +52,12 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}, [isGitAuth, navigate, template, toast]);
|
||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative',
|
||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative cursor-pointer',
|
||||
{
|
||||
'cursor-default': template?.isComingSoon,
|
||||
},
|
||||
@@ -80,6 +86,6 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+80
-62
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
@@ -60,79 +60,97 @@ const DeploymentDetailsCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex lg:flex gap-2 lg:gap-2 2xl:gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent ">
|
||||
<div className="flex-1 max-w-[30%] space-y-2">
|
||||
{/* DEPLOYMENT URL */}
|
||||
{deployment.url && (
|
||||
<Heading
|
||||
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||
as="h2"
|
||||
const renderDeploymentStatus = useCallback(
|
||||
(className?: string) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
<OverflownText content={deployment.url}>
|
||||
{deployment.url}
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</span>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[deployment.status, deployment.commitHash],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
||||
<div className="flex-1 flex justify-between w-full md:max-w-[25%] lg:max-w-[28%]">
|
||||
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
|
||||
{/* DEPLOYMENT URL */}
|
||||
{deployment.url && (
|
||||
<Heading
|
||||
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||
as="h2"
|
||||
>
|
||||
<OverflownText content={deployment.url}>
|
||||
{deployment.url}
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT STATUS */}
|
||||
<div className="w-[10%] max-w-[110px]">
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
{renderDeploymentStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')}
|
||||
|
||||
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||
<div className="text-sm w-[25%] space-y-2 text-elements-low-em">
|
||||
<span className="flex gap-1.5 items-center">
|
||||
<BranchStrokeIcon className="h-4 w-4" />
|
||||
{deployment.branch}
|
||||
</span>
|
||||
<span className="flex gap-2 items-center">
|
||||
<CommitIcon />
|
||||
<OverflownText content={deployment.commitMessage}>
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</OverflownText>
|
||||
</span>
|
||||
<div className="flex w-full justify-between md:w-[25%]">
|
||||
<div className="text-sm max-w-[60%] md:max-w-full space-y-2 w-full text-elements-low-em">
|
||||
<span className="flex gap-1.5 items-center">
|
||||
<BranchStrokeIcon className="h-4 w-4" />
|
||||
<OverflownText content={deployment.branch}>
|
||||
{deployment.branch}
|
||||
</OverflownText>
|
||||
</span>
|
||||
<span className="flex w-full gap-2 items-center">
|
||||
<CommitIcon />
|
||||
<OverflownText content={deployment.commitMessage}>
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</OverflownText>
|
||||
</span>
|
||||
</div>
|
||||
{renderDeploymentStatus('flex md:hidden h-fit')}
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT INFOs */}
|
||||
<div className="ml-auto max-w-[312px] w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
|
||||
<div className="flex w-[70%] items-center gap-0.5 2xl:gap-2 flex-1">
|
||||
<ClockOutlineIcon className="h-4 w-4" />
|
||||
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</OverflownText>
|
||||
<div>
|
||||
<Avatar
|
||||
type="orange"
|
||||
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||
className="lg:size-5 2xl:size-6"
|
||||
// TODO: Add avatarUrl
|
||||
// imageSrc={deployment.createdBy.avatarUrl}
|
||||
></Avatar>
|
||||
<div className="md:ml-auto w-full md:max-w-[312px] md:w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
|
||||
<div className="flex md:w-[70%] xl:items-center gap-2 flex-1 xl:flex-row md:flex-col">
|
||||
<div className="flex gap-2 items-center">
|
||||
<ClockOutlineIcon className="h-4 w-4" />
|
||||
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</OverflownText>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div>
|
||||
<Avatar
|
||||
type="orange"
|
||||
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||
className="lg:size-5 2xl:size-6"
|
||||
// TODO: Add avatarUrl
|
||||
// imageSrc={deployment.createdBy.avatarUrl}
|
||||
></Avatar>
|
||||
</div>
|
||||
<OverflownText
|
||||
content={formatAddress(deployment.createdBy?.name ?? '')}
|
||||
>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</OverflownText>
|
||||
</div>
|
||||
|
||||
<OverflownText
|
||||
// className="min-w-[200px]"
|
||||
content={formatAddress(deployment.createdBy?.name ?? '')}
|
||||
>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</OverflownText>
|
||||
</div>
|
||||
<DeploymentMenu
|
||||
className="ml-auto"
|
||||
className="ml-auto md:static absolute top-4 right-0"
|
||||
deployment={deployment}
|
||||
currentDeployment={currentDeployment}
|
||||
onUpdate={onUpdate}
|
||||
|
||||
+55
-26
@@ -1,17 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Deployment } from 'gql-client';
|
||||
|
||||
import { Typography, Chip, Card } from '@material-tailwind/react';
|
||||
import { color } from '@material-tailwind/react/types/components/chip';
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import {
|
||||
BranchStrokeIcon,
|
||||
ClockOutlineIcon,
|
||||
CommitIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag, TagProps } from 'components/shared/Tag';
|
||||
|
||||
interface DeploymentDialogBodyCardProps {
|
||||
deployment: Deployment;
|
||||
chip?: {
|
||||
value: string;
|
||||
color?: color;
|
||||
type?: TagProps['type'];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,31 +25,54 @@ const DeploymentDialogBodyCard = ({
|
||||
chip,
|
||||
deployment,
|
||||
}: DeploymentDialogBodyCardProps) => {
|
||||
const commit =
|
||||
deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH) +
|
||||
' ' +
|
||||
deployment.commitMessage;
|
||||
|
||||
return (
|
||||
<Card className="p-2 shadow-none" placeholder={''}>
|
||||
<div className="flex flex-col gap-4 px-4 py-4 rounded-xl bg-base-bg-emphasized text-elements-low-em">
|
||||
{chip && (
|
||||
<Chip
|
||||
className={`w-fit normal-case font-normal`}
|
||||
size="sm"
|
||||
value={chip.value}
|
||||
color={chip.color}
|
||||
/>
|
||||
<Tag className="w-fit" size="xs" type={chip.type}>
|
||||
{chip.value}
|
||||
</Tag>
|
||||
)}
|
||||
{deployment.url && (
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Title */}
|
||||
<p className="text-sm font-medium text-elements-high-em tracking-[0.006em]">
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {deployment.branch} ^{' '}
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</Typography>
|
||||
</Card>
|
||||
</p>
|
||||
{/* Branch & commit */}
|
||||
<div className="flex items-center gap-6 text-elements-low-em">
|
||||
<div className="flex items-center gap-2">
|
||||
<BranchStrokeIcon size={16} />
|
||||
<p className="text-sm tracking-[0.006em]">{deployment.branch}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<CommitIcon size={16} />
|
||||
<p className="text-sm tracking-[0.006em] max-w-[67.5%] sm:max-w-[80%]">
|
||||
<OverflownText content={commit}>{commit}</OverflownText>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-elements-low-em">
|
||||
<ClockOutlineIcon size={16} />
|
||||
<p className="text-sm tracking-[0.006em]">
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</p>
|
||||
<Avatar
|
||||
size={20}
|
||||
type="orange"
|
||||
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||
// TODO: Add avatar image URL
|
||||
// imageSrc={deployment.createdBy.imageUrl}
|
||||
/>
|
||||
<p className="text-sm tracking-[0.006em]">
|
||||
{deployment.createdBy.name ?? 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ import {
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import { ComponentPropsWithRef } from 'react';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||
import { Typography } from '@material-tailwind/react';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
||||
|
||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||
deployment: Deployment;
|
||||
@@ -158,106 +156,44 @@ export const DeploymentMenu = ({
|
||||
</Menu>
|
||||
</div>
|
||||
{/* Dialogs */}
|
||||
<ConfirmDialog
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Change to production?"
|
||||
handleOpen={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
confirmButtonTitle="Change"
|
||||
color="blue"
|
||||
handleCancel={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
handleConfirm={async () => {
|
||||
await updateDeployment();
|
||||
setChangeToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
The new deployment will be associated with these domains:
|
||||
</Typography>
|
||||
{prodBranchDomains.length > 0 &&
|
||||
prodBranchDomains.map((value) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="small"
|
||||
color="blue"
|
||||
key={value.id}
|
||||
placeholder={''}
|
||||
>
|
||||
^ {value.name}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
deployment={deployment}
|
||||
domains={prodBranchDomains}
|
||||
/>
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Redeploy to production?"
|
||||
handleOpen={() => setRedeployToProduction((preVal) => !preVal)}
|
||||
handleCancel={() => setRedeployToProduction((preVal) => !preVal)}
|
||||
open={redeployToProduction}
|
||||
confirmButtonTitle="Redeploy"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await redeployToProd();
|
||||
setRedeployToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, new deployment will be created with the same
|
||||
source code as current deployment.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
{deployment.domain?.name && (
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
{deployment.domain?.name}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
deployment={deployment}
|
||||
domains={deployment.domain ? [deployment.domain] : []}
|
||||
/>
|
||||
{Boolean(currentDeployment) && (
|
||||
<ConfirmDialog
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Rollback to this deployment?"
|
||||
handleOpen={() => setRollbackDeployment((preVal) => !preVal)}
|
||||
handleCancel={() => setRollbackDeployment((preVal) => !preVal)}
|
||||
open={rollbackDeployment}
|
||||
confirmButtonTitle="Rollback"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await rollbackDeploymentHandler();
|
||||
setRollbackDeployment((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will replace your current
|
||||
deployment
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={currentDeployment}
|
||||
chip={{
|
||||
value: 'Live Deployment',
|
||||
color: 'green',
|
||||
}}
|
||||
/>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={deployment}
|
||||
chip={{
|
||||
value: 'New Deployment',
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
^ {currentDeployment.domain?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
deployment={currentDeployment}
|
||||
newDeployment={deployment}
|
||||
domains={currentDeployment.domain ? [currentDeployment.domain] : []}
|
||||
/>
|
||||
)}
|
||||
<AssignDomainDialog
|
||||
open={assignDomainDialog}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
|
||||
import { IconButton, Option, Select } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../../SearchBar';
|
||||
import DatePicker from '../../../DatePicker';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import {
|
||||
CheckRadioOutlineIcon,
|
||||
CrossCircleIcon,
|
||||
LoaderIcon,
|
||||
SearchIcon,
|
||||
TrendingIcon,
|
||||
WarningTriangleIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { DatePicker } from 'components/shared/DatePicker';
|
||||
import { Value } from 'react-calendar/dist/cjs/shared/types';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
|
||||
export enum StatusOptions {
|
||||
ALL_STATUS = 'All status',
|
||||
@@ -15,8 +22,8 @@ export enum StatusOptions {
|
||||
|
||||
export interface FilterValue {
|
||||
searchedBranch: string;
|
||||
status: StatusOptions;
|
||||
updateAtRange?: DateRange;
|
||||
status: StatusOptions | string;
|
||||
updateAtRange?: Value;
|
||||
}
|
||||
|
||||
interface FilterFormProps {
|
||||
@@ -27,7 +34,7 @@ interface FilterFormProps {
|
||||
const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch);
|
||||
const [selectedStatus, setSelectedStatus] = useState(value.status);
|
||||
const [dateRange, setDateRange] = useState<DateRange>();
|
||||
const [dateRange, setDateRange] = useState<Value>();
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
@@ -43,46 +50,68 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
setDateRange(value.updateAtRange);
|
||||
}, [value]);
|
||||
|
||||
const getOptionIcon = (status: StatusOptions) => {
|
||||
switch (status) {
|
||||
case StatusOptions.BUILDING:
|
||||
return <LoaderIcon />;
|
||||
case StatusOptions.READY:
|
||||
return <CheckRadioOutlineIcon />;
|
||||
case StatusOptions.ERROR:
|
||||
return <WarningTriangleIcon />;
|
||||
case StatusOptions.ALL_STATUS:
|
||||
default:
|
||||
return <TrendingIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const statusOptions = Object.values(StatusOptions).map((status) => ({
|
||||
label: status,
|
||||
value: status,
|
||||
leftIcon: getOptionIcon(status),
|
||||
}));
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchedBranch('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid items-center grid-cols-8 gap-2 text-sm text-gray-600">
|
||||
<div className="col-span-4">
|
||||
<SearchBar
|
||||
<div className="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-3">
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Input
|
||||
placeholder="Search branches"
|
||||
leftIcon={<SearchIcon />}
|
||||
rightIcon={
|
||||
searchedBranch && <CrossCircleIcon onClick={handleReset} />
|
||||
}
|
||||
value={searchedBranch}
|
||||
onChange={(event) => setSearchedBranch(event.target.value)}
|
||||
onChange={(e) => setSearchedBranch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
||||
<div className="col-span-2 flex items-center">
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
selectRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
onReset={() => setDateRange(undefined)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 relative">
|
||||
<div className="col-span-2 flex items-center">
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
onChange={(value) => setSelectedStatus(value as StatusOptions)}
|
||||
leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
|
||||
options={statusOptions}
|
||||
clearable
|
||||
placeholder="All status"
|
||||
>
|
||||
{Object.values(StatusOptions).map((status) => (
|
||||
<Option
|
||||
className={status === StatusOptions.ALL_STATUS ? 'hidden' : ''}
|
||||
key={status}
|
||||
value={status}
|
||||
>
|
||||
^ {status}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{selectedStatus !== StatusOptions.ALL_STATUS && (
|
||||
<div className="absolute end-1 inset-y-0 my-auto h-8">
|
||||
<IconButton
|
||||
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
value={
|
||||
selectedStatus
|
||||
? { label: selectedStatus, value: selectedStatus }
|
||||
: undefined
|
||||
}
|
||||
onChange={(item) =>
|
||||
setSelectedStatus((item as SelectOption).value as StatusOptions)
|
||||
}
|
||||
onClear={() => setSelectedStatus('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,24 +4,46 @@ import { GitCommitWithBranch } from 'types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import ActivityCard from './ActivityCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { LoadingIcon, LinkIcon } from 'components/shared/CustomIcon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const Activity = ({
|
||||
isLoading,
|
||||
activities,
|
||||
repoPath,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
activities: GitCommitWithBranch[];
|
||||
repoPath: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-2 mr-1">
|
||||
<div className="col-span-5 md:col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
See all
|
||||
</Button>
|
||||
<Link to={`https://github.com/${repoPath}/activity`} target="_blank">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
rightIcon={
|
||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||
}
|
||||
>
|
||||
See all
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
{activities.map((activity, index) => {
|
||||
return <ActivityCard activity={activity} key={`activity-${index}`} />;
|
||||
})}
|
||||
{isLoading ? (
|
||||
<div className="grid place-content-center mt-10">
|
||||
<LoadingIcon className="animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
activities.map((activity, index) => {
|
||||
return (
|
||||
<ActivityCard activity={activity} key={`activity-${index}`} />
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
Card,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import EditDomainDialog from './EditDomainDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
||||
|
||||
enum RefreshStatus {
|
||||
IDLE,
|
||||
@@ -118,28 +118,16 @@ const DomainCard = ({
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete domain?"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
<DeleteDomainDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Delete domain"
|
||||
handleConfirm={() => {
|
||||
deleteDomain();
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once deleted, the project{' '}
|
||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||
{project.name}
|
||||
</span>{' '}
|
||||
will not be accessible from the domain{' '}
|
||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||
{domain.name}.
|
||||
</span>
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
projectName={project.name}
|
||||
domainName={domain.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Typography variant="small" placeholder={''}>
|
||||
|
||||
+6
-14
@@ -5,8 +5,8 @@ import { EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import { IconButton, Input, Typography } from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog';
|
||||
|
||||
const ShowPasswordIcon = ({
|
||||
handler,
|
||||
@@ -161,20 +161,12 @@ const EditEnvironmentVariableRow = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete variable"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
<DeleteVariableDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Confirm delete"
|
||||
handleConfirm={removeEnvironmentVariableHandler}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Are you sure you want to delete the variable
|
||||
<span className="bg-blue-100">{variable.key}</span>?
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
variableKey={variable.key}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,15 +3,14 @@ import { Permission, User } from 'gql-client';
|
||||
|
||||
import {
|
||||
Select,
|
||||
Typography,
|
||||
Option,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog';
|
||||
|
||||
const PERMISSION_OPTIONS = [
|
||||
{
|
||||
@@ -141,25 +140,19 @@ const MemberCard = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Remove member?"
|
||||
handleOpen={() => setRemoveMemberDialogOpen((preVal) => !preVal)}
|
||||
<RemoveMemberDialog
|
||||
handleCancel={() => setRemoveMemberDialogOpen((preVal) => !preVal)}
|
||||
open={removeMemberDialogOpen}
|
||||
confirmButtonTitle="Yes, Remove member"
|
||||
handleConfirm={() => {
|
||||
setRemoveMemberDialogOpen((preVal) => !preVal);
|
||||
if (onRemoveProjectMember) {
|
||||
onRemoveProjectMember();
|
||||
}
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once removed, {formatAddress(member.name ?? '')} (
|
||||
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
||||
this project.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
memberName={member.name ?? ''}
|
||||
ethAddress={ethAddress}
|
||||
emailDomain={emailDomain}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-13
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { GitRepositoryDetails } from '../../../../types';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { GitRepositoryDetails } from 'types';
|
||||
import { DisconnectRepositoryDialog } from 'components/projects/Dialog/DisconnectRepositoryDialog';
|
||||
|
||||
const RepoConnectedSection = ({
|
||||
linkedRepo,
|
||||
@@ -34,21 +34,13 @@ const RepoConnectedSection = ({
|
||||
^ Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Disconnect repository?"
|
||||
handleOpen={() => setDisconnectRepoDialogOpen((preVal) => !preVal)}
|
||||
<DisconnectRepositoryDialog
|
||||
handleCancel={() => setDisconnectRepoDialogOpen((preVal) => !preVal)}
|
||||
open={disconnectRepoDialogOpen}
|
||||
confirmButtonTitle="Yes, confirm disconnect"
|
||||
handleConfirm={() => {
|
||||
setDisconnectRepoDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Any data tied to your Git project may become misconfigured. Are you
|
||||
sure you want to continue?
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
import { Button } from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
||||
|
||||
interface WebhookCardProps {
|
||||
webhookUrl: string;
|
||||
@@ -15,7 +15,6 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
return (
|
||||
<div className="flex justify-between w-full mb-3">
|
||||
{webhookUrl}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -38,23 +37,15 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete webhook"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
<DeleteWebhookDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Confirm delete"
|
||||
handleConfirm={() => {
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
onDelete();
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Are you sure you want to delete the variable{' '}
|
||||
<span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
webhookUrl={webhookUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const avatarTheme = tv(
|
||||
{
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden', 'shrink-0'],
|
||||
slots: {
|
||||
image: [
|
||||
'h-full',
|
||||
|
||||
@@ -20,7 +20,7 @@ export const buttonTheme = tv(
|
||||
variants: {
|
||||
size: {
|
||||
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
|
||||
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
md: ['gap-2', 'py-3', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
|
||||
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
|
||||
import './Calendar.css';
|
||||
import { format } from 'date-fns';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
export type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
@@ -63,6 +64,11 @@ export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
|
||||
* @returns None
|
||||
*/
|
||||
onCancel?: () => void;
|
||||
/**
|
||||
* Optional callback function that is called when a reset action is triggered.
|
||||
* @returns None
|
||||
*/
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +86,7 @@ export const Calendar = ({
|
||||
actions,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onReset,
|
||||
onChange: onChangeProp,
|
||||
...props
|
||||
}: CalendarProps): JSX.Element => {
|
||||
@@ -217,6 +224,11 @@ export const Calendar = ({
|
||||
[setValue, setActiveDate, changeNavigationLabel, selectRange],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setValue(null);
|
||||
onReset?.();
|
||||
}, [setValue, onReset]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
@@ -276,21 +288,30 @@ export const Calendar = ({
|
||||
{/* Footer or CTA */}
|
||||
<div
|
||||
{...footerProps}
|
||||
className={footer({ className: footerProps?.className })}
|
||||
className={cn(footer({ className: footerProps?.className }), {
|
||||
'justify-between': Boolean(value),
|
||||
})}
|
||||
>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<>
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!value}
|
||||
onClick={() => (value ? onSelect?.(value) : null)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
{value && (
|
||||
<Button variant="danger" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<div className="space-x-3">
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!value}
|
||||
onClick={() => (value ? onSelect?.(value) : null)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Modal, ModalProps } from './Modal';
|
||||
import { Button, ButtonOrLinkProps } from './Button';
|
||||
|
||||
import { color } from '@material-tailwind/react/types/components/button';
|
||||
import {
|
||||
Typography,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
type ConfirmDialogProp = {
|
||||
children: React.ReactNode;
|
||||
dialogTitle: string;
|
||||
export type ConfirmDialogProps = ModalProps & {
|
||||
children?: ReactNode;
|
||||
dialogTitle?: string;
|
||||
open: boolean;
|
||||
handleOpen: () => void;
|
||||
confirmButtonTitle: string;
|
||||
handleCancel: () => void;
|
||||
confirmButtonTitle?: string;
|
||||
handleConfirm?: () => void;
|
||||
color: color;
|
||||
cancelButtonProps?: ButtonOrLinkProps;
|
||||
confirmButtonProps?: ButtonOrLinkProps;
|
||||
};
|
||||
|
||||
const ConfirmDialog = ({
|
||||
children,
|
||||
dialogTitle,
|
||||
open,
|
||||
handleOpen,
|
||||
handleCancel,
|
||||
confirmButtonTitle,
|
||||
handleConfirm,
|
||||
color,
|
||||
}: ConfirmDialogProp) => {
|
||||
cancelButtonProps,
|
||||
confirmButtonProps,
|
||||
...props
|
||||
}: ConfirmDialogProps) => {
|
||||
// Close the dialog when the user clicks outside of it
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) return handleCancel?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
{dialogTitle}{' '}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className=" rounded-full"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<DialogBody placeholder={''}>{children}</DialogBody>
|
||||
<DialogFooter className="flex justify-start gap-2" placeholder={''}>
|
||||
<Button variant="outlined" onClick={handleOpen} placeholder={''}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color={color}
|
||||
onClick={handleConfirm}
|
||||
placeholder={''}
|
||||
>
|
||||
{confirmButtonTitle}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
<Modal {...props} onOpenChange={handleOpenChange}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>{dialogTitle}</Modal.Header>
|
||||
<Modal.Body>{children}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={handleCancel}
|
||||
{...cancelButtonProps}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} {...confirmButtonProps}>
|
||||
{confirmButtonTitle}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ArrowLeftCircleFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM10.4697 15.7803C10.7626 16.0732 11.2374 16.0732 11.5303 15.7803C11.8232 15.4874 11.8232 15.0126 11.5303 14.7197L9.56066 12.75H16.25C16.6642 12.75 17 12.4142 17 12C17 11.5858 16.6642 11.25 16.25 11.25H9.56066L11.5303 9.28033C11.8232 8.98744 11.8232 8.51256 11.5303 8.21967C11.2374 7.92678 10.7626 7.92678 10.4697 8.21967L7.21967 11.4697C6.92678 11.7626 6.92678 12.2374 7.21967 12.5303L10.4697 15.7803Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const BuildingIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3.75 19.25H14.25M3.75 19.25V5.75C3.75 4.64543 4.64543 3.75 5.75 3.75H12.25C13.3546 3.75 14.25 4.64543 14.25 5.75V8M3.75 19.25H1.75M14.25 19.25V8M14.25 19.25H20.25M14.25 8H18.25C19.3546 8 20.25 8.89543 20.25 10V19.25M20.25 19.25H22.25M10.25 8.75H7.75M7.75 12.75H10.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -5,16 +5,16 @@ export const CalendarIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
|
||||
fill="currentColor"
|
||||
d="M2.625 7.125H15.375M5.625 3.375V1.875M12.375 3.375V1.875M4.125 15.375H13.875C14.7034 15.375 15.375 14.7034 15.375 13.875V4.875C15.375 4.04657 14.7034 3.375 13.875 3.375H4.125C3.29657 3.375 2.625 4.04657 2.625 4.875V13.875C2.625 14.7034 3.29657 15.375 4.125 15.375Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRadioOutlineIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.25 7.125L7.875 11.25L6.375 9.75M16.125 9C16.125 12.935 12.935 16.125 9 16.125C5.06497 16.125 1.875 12.935 1.875 9C1.875 5.06497 5.06497 1.875 9 1.875C12.935 1.875 16.125 5.06497 16.125 9Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -3,17 +3,11 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<CustomIcon width="20" height="20" viewBox="0 0 20 20" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
|
||||
d="M10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0ZM13.774 8.13327C14.1237 7.70582 14.0607 7.0758 13.6332 6.72607C13.2058 6.37635 12.5758 6.43935 12.226 6.86679L8.42576 11.5116L7.20711 10.2929C6.81658 9.9024 6.18342 9.9024 5.79289 10.2929C5.40237 10.6834 5.40237 11.3166 5.79289 11.7071L7.79289 13.7071C7.99267 13.9069 8.26764 14.0129 8.54981 13.9988C8.83199 13.9847 9.09505 13.8519 9.27396 13.6333L13.774 8.13327Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronDoubleDownIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="21"
|
||||
viewBox="0 0 20 21"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6.66699 12.0174L10.0003 15.3507L13.3337 12.0174M6.66699 6.18408L10.0003 9.51742L13.3337 6.18408"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronUpDown = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8 15L12 19L16 15M8 9L12 5L16 9"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CirclePlaceholderOnIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.125 9C16.125 12.935 12.935 16.125 9 16.125C5.06497 16.125 1.875 12.935 1.875 9C1.875 5.06497 5.06497 1.875 9 1.875C12.935 1.875 16.125 5.06497 16.125 9Z"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CopyIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.33301 2.66665C1.33301 1.93027 1.92996 1.33331 2.66634 1.33331H9.33301C10.0694 1.33331 10.6663 1.93027 10.6663 2.66665V5.33331H13.333C14.0694 5.33331 14.6663 5.93027 14.6663 6.66665V13.3333C14.6663 14.0697 14.0694 14.6666 13.333 14.6666H6.66634C5.92996 14.6666 5.33301 14.0697 5.33301 13.3333V10.6666H2.66634C1.92996 10.6666 1.33301 10.0697 1.33301 9.33331V2.66665ZM9.66634 5.33331H6.66634C5.92996 5.33331 5.33301 5.93027 5.33301 6.66665V9.66665H2.66634C2.48225 9.66665 2.33301 9.51741 2.33301 9.33331V2.66665C2.33301 2.48255 2.48225 2.33331 2.66634 2.33331H9.33301C9.5171 2.33331 9.66634 2.48255 9.66634 2.66665V5.33331Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CrossCircleIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.5 9C1.5 4.85786 4.85786 1.5 9 1.5C13.1421 1.5 16.5 4.85786 16.5 9C16.5 13.1421 13.1421 16.5 9 16.5C4.85786 16.5 1.5 13.1421 1.5 9ZM7.01516 6.48484C6.86872 6.33839 6.63128 6.33839 6.48484 6.48484C6.33839 6.63128 6.33839 6.86872 6.48484 7.01516L8.46967 9L6.48484 10.9848C6.33839 11.1313 6.33839 11.3687 6.48484 11.5152C6.63128 11.6616 6.86872 11.6616 7.01516 11.5152L9 9.53033L10.9848 11.5152C11.1313 11.6616 11.3687 11.6616 11.5152 11.5152C11.6616 11.3687 11.6616 11.1313 11.5152 10.9848L9.53033 9L11.5152 7.01516C11.6616 6.86872 11.6616 6.63128 11.5152 6.48484C11.3687 6.33839 11.1313 6.33839 10.9848 6.48484L9 8.46967L7.01516 6.48484Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LinkChainIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9.75027 5.52371L10.7168 4.55722C13.1264 2.14759 17.0332 2.14759 19.4428 4.55722C21.8524 6.96684 21.8524 10.8736 19.4428 13.2832L18.4742 14.2519M5.52886 9.74513L4.55722 10.7168C2.14759 13.1264 2.1476 17.0332 4.55722 19.4428C6.96684 21.8524 10.8736 21.8524 13.2832 19.4428L14.2478 18.4782M9.5 14.5L14.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LoaderIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12.5003 3V6M12.5003 18V21M6.13634 5.63604L8.25766 7.75736M16.7429 16.2426L18.8643 18.364M3.5 12.0007H6.5M18.5 12.0007H21.5M6.13634 18.364L8.25766 16.2426M16.7429 7.75736L18.8643 5.63604"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LogoutIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.3125 3.375C3.79473 3.375 3.375 3.79473 3.375 4.3125L3.375 13.6875C3.375 14.2053 3.79473 14.625 4.3125 14.625H8.4375C8.74816 14.625 9 14.8768 9 15.1875C9 15.4982 8.74816 15.75 8.4375 15.75H4.3125C3.17341 15.75 2.25 14.8266 2.25 13.6875L2.25 4.3125C2.25 3.17341 3.17341 2.25 4.3125 2.25L8.4375 2.25C8.74816 2.25 9 2.50184 9 2.8125C9 3.12316 8.74816 3.375 8.4375 3.375L4.3125 3.375ZM11.4148 5.22725C11.6344 5.00758 11.9906 5.00758 12.2102 5.22725L15.5852 8.60224C15.8049 8.82191 15.8049 9.17807 15.5852 9.39774L12.2102 12.7727C11.9906 12.9924 11.6344 12.9924 11.4148 12.7727C11.1951 12.5531 11.1951 12.1969 11.4148 11.9773L13.8295 9.56249L6.75 9.56249C6.43934 9.56249 6.1875 9.31065 6.1875 8.99999C6.1875 8.68933 6.43934 8.43749 6.75 8.43749L13.8295 8.43749L11.4148 6.02275C11.1951 5.80308 11.1951 5.44692 11.4148 5.22725Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const MenuIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2 4.5C2 4.22386 2.22386 4 2.5 4H18.5C18.7761 4 19 4.22386 19 4.5C19 4.77614 18.7761 5 18.5 5H2.5C2.22386 5 2 4.77614 2 4.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M2 10.5C2 10.2239 2.22386 10 2.5 10H10.5C10.7761 10 11 10.2239 11 10.5C11 10.7761 10.7761 11 10.5 11H2.5C2.22386 11 2 10.7761 2 10.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M2 16.5C2 16.2239 2.22386 16 2.5 16H18.5C18.7761 16 19 16.2239 19 16.5C19 16.7761 18.7761 17 18.5 17H2.5C2.22386 17 2 16.7761 2 16.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const MinusCircleIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.5 12C2.5 6.47715 6.97715 2 12.5 2C18.0228 2 22.5 6.47715 22.5 12C22.5 17.5228 18.0228 22 12.5 22C6.97715 22 2.5 17.5228 2.5 12ZM16.5 12.9999C17.0523 12.9999 17.5 12.5522 17.5 11.9999C17.5 11.4476 17.0523 10.9999 16.5 10.9999L8.49997 11.0001C7.94769 11.0001 7.49998 11.4479 7.5 12.0001C7.50002 12.5524 7.94774 13.0001 8.50003 13.0001L16.5 12.9999Z"
|
||||
fill="#0F86F5"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const QuestionMarkRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0ZM10 7C9.62268 7 9.29263 7.2086 9.12148 7.52152C8.85646 8.00606 8.24881 8.18401 7.76427 7.91899C7.27973 7.65396 7.10178 7.04632 7.3668 6.56178C7.87463 5.63331 8.86264 5 10 5C11.5148 5 12.5669 6.00643 12.8664 7.189C13.1676 8.37785 12.7101 9.76299 11.3416 10.4472C11.1323 10.5519 11 10.7659 11 11C11 11.5523 10.5523 12 10 12C9.44772 12 9.00001 11.5523 9.00001 11C9.00001 10.0084 9.56027 9.10183 10.4472 8.65836C10.902 8.43099 11.0188 8.03973 10.9277 7.6801C10.835 7.31417 10.5283 7 10 7ZM10 15C10.5523 15 11 14.5523 11 14C11 13.4477 10.5523 13 10 13C9.44771 13 9 13.4477 9 14C9 14.5523 9.44771 15 10 15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -4,36 +4,18 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
export const RefreshIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7.125 10.875V15.375H2.625M6.75 14.9666C4.33948 14.0571 2.625 11.7288 2.625 9C2.625 5.47918 5.47918 2.625 9 2.625C9.93578 2.625 10.8245 2.82663 11.625 3.1888"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.3125 15.1877C10.3125 15.4984 10.0607 15.7502 9.75 15.7502C9.43934 15.7502 9.1875 15.4984 9.1875 15.1877C9.1875 14.8771 9.43934 14.6252 9.75 14.6252C10.0607 14.6252 10.3125 14.8771 10.3125 15.1877Z"
|
||||
d="M4.7586 5.25C5.80171 4.1001 7.33209 3.375 9 3.375C12.1066 3.375 14.625 5.8934 14.625 9C14.625 9.23861 14.6102 9.47349 14.5815 9.70383C14.543 10.0121 14.7618 10.2932 15.07 10.3316C15.3783 10.3701 15.6594 10.1513 15.6978 9.84304C15.7323 9.56663 15.75 9.28526 15.75 9C15.75 5.27208 12.7279 2.25 9 2.25C7.10933 2.25 5.36655 3.02807 4.125 4.28342V2.8125C4.125 2.50184 3.87316 2.25 3.5625 2.25C3.25184 2.25 3 2.50184 3 2.8125L3 5.25C3 5.87132 3.50368 6.375 4.125 6.375H6.5625C6.87316 6.375 7.125 6.12316 7.125 5.8125C7.125 5.50184 6.87316 5.25 6.5625 5.25H4.7586Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13.2304 13.7025C13.3857 13.9716 13.2936 14.3156 13.0245 14.4709C12.7555 14.6262 12.4115 14.5341 12.2561 14.265C12.1008 13.996 12.193 13.652 12.462 13.4966C12.7311 13.3413 13.0751 13.4335 13.2304 13.7025Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M15.0147 10.9573C15.2838 11.1126 15.3759 11.4567 15.2206 11.7257C15.0653 11.9947 14.7213 12.0869 14.4522 11.9316C14.1832 11.7763 14.091 11.4322 14.2463 11.1632C14.4017 10.8942 14.7457 10.802 15.0147 10.9573Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M14.2657 5.74407C13.9967 5.8994 13.6527 5.80722 13.4973 5.53818C13.342 5.26914 13.4342 4.92512 13.7032 4.76979C13.9723 4.61446 14.3163 4.70664 14.4716 4.97568C14.6269 5.24472 14.5348 5.58874 14.2657 5.74407Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M15.75 8.25023C15.75 8.56089 15.4982 8.81273 15.1875 8.81273C14.8768 8.81273 14.625 8.56089 14.625 8.25023C14.625 7.93957 14.8768 7.68773 15.1875 7.68773C15.4982 7.68773 15.75 7.93957 15.75 8.25023Z"
|
||||
d="M3.41855 8.29617C3.45699 7.98789 3.23825 7.70683 2.92997 7.66839C2.6217 7.62994 2.34063 7.84869 2.30219 8.15696C2.26773 8.43337 2.25 8.71474 2.25 9C2.25 12.7279 5.27208 15.75 9 15.75C10.8952 15.75 12.6418 14.9682 13.8839 13.7076V15.1875C13.8839 15.4982 14.1357 15.75 14.4464 15.75C14.757 15.75 15.0089 15.4982 15.0089 15.1875V12.75C15.0089 12.1287 14.5052 11.625 13.8839 11.625L11.4464 11.625C11.1357 11.625 10.8839 11.8768 10.8839 12.1875C10.8839 12.4982 11.1357 12.75 11.4464 12.75H13.2414C12.1983 13.8999 10.6679 14.625 9 14.625C5.8934 14.625 3.375 12.1066 3.375 9C3.375 8.76139 3.38982 8.52651 3.41855 8.29617Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
|
||||
@@ -4,17 +4,17 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
export const SearchIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20 20L16.05 16.05M18 11C18 14.866 14.866 18 11 18C7.13401 18 4 14.866 4 11C4 7.13401 7.13401 4 11 4C14.866 4 18 7.13401 18 11Z"
|
||||
d="M15.375 15.375L12.225 12.225M13.875 8.25C13.875 11.3566 11.3566 13.875 8.25 13.875C5.1434 13.875 2.625 11.3566 2.625 8.25C2.625 5.1434 5.1434 2.625 8.25 2.625C11.3566 2.625 13.875 5.1434 13.875 8.25Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const TrendingIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.875 9.79623L2.68636 7.88111C2.95597 7.24472 3.86788 7.28002 4.08767 7.93535L5.09939 10.9518C5.33069 11.6415 6.30748 11.6322 6.52567 10.9383L8.97436 3.15067C9.2032 2.42289 10.2437 2.46059 10.4194 3.20303L13.1643 14.7969C13.3368 15.5254 14.3502 15.5807 14.6007 14.8754L16.125 10.583"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const WarningTriangleIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9.00004 6.75007V10.1251M7.71074 2.91845L2.09776 12.3585C1.50324 13.3583 2.22379 14.6251 3.38706 14.6251H14.613C15.7763 14.6251 16.4968 13.3583 15.9023 12.3585L10.2893 2.91844C9.70792 1.9406 8.29216 1.9406 7.71074 2.91845Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="9" cy="12" r="0.5625" fill="currentColor" />
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ export * from './CheckIcon';
|
||||
export * from './ChevronGrabberHorizontal';
|
||||
export * from './ChevronLeft';
|
||||
export * from './ChevronRight';
|
||||
export * from './ChevronUpDown';
|
||||
export * from './InfoSquareIcon';
|
||||
export * from './WarningIcon';
|
||||
export * from './SearchIcon';
|
||||
@@ -17,6 +18,7 @@ export * from './FolderIcon';
|
||||
export * from './SettingsSlidersIcon';
|
||||
export * from './LifeBuoyIcon';
|
||||
export * from './QuestionMarkRoundIcon';
|
||||
export * from './QuestionMarkRoundFilledIcon';
|
||||
export * from './GitIcon';
|
||||
export * from './EllipseIcon';
|
||||
export * from './EllipsesIcon';
|
||||
@@ -26,6 +28,7 @@ export * from './GithubIcon';
|
||||
export * from './GitTeaIcon';
|
||||
export * from './LockIcon';
|
||||
export * from './PencilIcon';
|
||||
export * from './BuildingIcon';
|
||||
export * from './CheckRadioIcon';
|
||||
export * from './ChevronDownIcon';
|
||||
export * from './BranchIcon';
|
||||
@@ -36,15 +39,29 @@ export * from './WarningDiamondIcon';
|
||||
export * from './ArrowRightCircleIcon';
|
||||
export * from './ClockOutlineIcon';
|
||||
export * from './ArrowRightCircleFilledIcon';
|
||||
export * from './ArrowLeftCircleFilledIcon';
|
||||
export * from './GithubStrokeIcon';
|
||||
export * from './BranchStrokeIcon';
|
||||
export * from './StorageIcon';
|
||||
export * from './LinkIcon';
|
||||
export * from './LinkChainIcon';
|
||||
export * from './CursorBoxIcon';
|
||||
export * from './CrossCircleIcon';
|
||||
export * from './RefreshIcon';
|
||||
export * from './CommitIcon';
|
||||
export * from './RocketIcon';
|
||||
export * from './RefreshIcon';
|
||||
export * from './UndoIcon';
|
||||
export * from './LoaderIcon';
|
||||
export * from './MinusCircleIcon';
|
||||
export * from './CopyIcon';
|
||||
export * from './MenuIcon';
|
||||
export * from './LogoutIcon';
|
||||
export * from './CirclePlaceholderOnIcon';
|
||||
export * from './WarningTriangleIcon';
|
||||
export * from './CheckRadioOutlineIcon';
|
||||
export * from './TrendingIcon';
|
||||
export * from './ChevronDoubleDownIcon';
|
||||
|
||||
// Templates
|
||||
export * from './templates';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const datePickerTheme = tv({
|
||||
slots: {
|
||||
input: [],
|
||||
input: ['w-full'],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Input, InputProps } from 'components/shared/Input';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { datePickerTheme } from './DatePicker.theme';
|
||||
import { Calendar, CalendarProps } from 'components/shared/Calendar';
|
||||
import { CalendarIcon } from 'components/shared/CustomIcon';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronGrabberHorizontal,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Value } from 'react-calendar/dist/cjs/shared/types';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
@@ -27,6 +30,10 @@ export interface DatePickerProps
|
||||
* Whether to allow the selection of a date range.
|
||||
*/
|
||||
selectRange?: boolean;
|
||||
/**
|
||||
* Optional callback function that is called when the date picker is reset.
|
||||
*/
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,6 +46,7 @@ export const DatePicker = ({
|
||||
calendarProps,
|
||||
value,
|
||||
onChange,
|
||||
onReset,
|
||||
selectRange = false,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
@@ -50,15 +58,15 @@ export const DatePicker = ({
|
||||
* Renders the value of the date based on the current state of `props.value`.
|
||||
* @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy.
|
||||
*/
|
||||
const renderValue = useCallback(() => {
|
||||
if (!value) return undefined;
|
||||
const renderValue = useMemo(() => {
|
||||
if (!value) return '';
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((date) => format(date as Date, 'dd/MM/yyyy'))
|
||||
.join(' - ');
|
||||
}
|
||||
return format(value, 'dd/MM/yyyy');
|
||||
}, [value]);
|
||||
}, [value, onReset]);
|
||||
|
||||
/**
|
||||
* Handles the selection of a date from the calendar.
|
||||
@@ -71,15 +79,21 @@ export const DatePicker = ({
|
||||
[setOpen, onChange],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setOpen(false);
|
||||
onReset?.();
|
||||
}, [setOpen, onReset]);
|
||||
|
||||
return (
|
||||
<Popover.Root open={open}>
|
||||
<Popover.Trigger>
|
||||
<Popover.Trigger className="w-full">
|
||||
<Input
|
||||
{...props}
|
||||
rightIcon={<CalendarIcon onClick={() => setOpen(true)} />}
|
||||
leftIcon={<CalendarIcon onClick={() => setOpen(true)} />}
|
||||
rightIcon={<ChevronGrabberHorizontal />}
|
||||
readOnly
|
||||
placeholder="Select a date..."
|
||||
value={renderValue()}
|
||||
value={renderValue}
|
||||
className={input({ className })}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
@@ -93,6 +107,7 @@ export const DatePicker = ({
|
||||
{...calendarProps}
|
||||
selectRange={selectRange}
|
||||
value={value}
|
||||
onReset={handleReset}
|
||||
onCancel={() => setOpen(false)}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,8 @@ export const inputTheme = tv(
|
||||
'items-center',
|
||||
'rounded-lg',
|
||||
'relative',
|
||||
'gap-2',
|
||||
'w-full',
|
||||
'placeholder:text-elements-disabled',
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:bg-controls-disabled',
|
||||
@@ -27,7 +29,7 @@ export const inputTheme = tv(
|
||||
'disabled:shadow-none',
|
||||
'disabled:border-none',
|
||||
],
|
||||
icon: ['text-elements-mid-em'],
|
||||
icon: ['text-elements-low-em'],
|
||||
iconContainer: [
|
||||
'absolute',
|
||||
'inset-y-0',
|
||||
|
||||
@@ -47,15 +47,15 @@ export const Input = ({
|
||||
helperIcon: helperIconCls,
|
||||
} = inputTheme({ ...styleProps });
|
||||
|
||||
const renderLabels = useMemo(
|
||||
() => (
|
||||
<div className="space-y-1">
|
||||
const renderLabels = useMemo(() => {
|
||||
if (!label && !description) return null;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className={labelCls()}>{label}</p>
|
||||
<p className={descriptionCls()}>{description}</p>
|
||||
</div>
|
||||
),
|
||||
[labelCls, descriptionCls, label, description],
|
||||
);
|
||||
);
|
||||
}, [labelCls, descriptionCls, label, description]);
|
||||
|
||||
const renderLeftIcon = useMemo(() => {
|
||||
return (
|
||||
@@ -73,8 +73,9 @@ export const Input = ({
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
|
||||
|
||||
const renderHelperText = useMemo(
|
||||
() => (
|
||||
const renderHelperText = useMemo(() => {
|
||||
if (!helperText) return null;
|
||||
return (
|
||||
<div className={helperTextCls()}>
|
||||
{state &&
|
||||
cloneIcon(<WarningIcon className={helperIconCls()} />, {
|
||||
@@ -82,17 +83,16 @@ export const Input = ({
|
||||
})}
|
||||
<p>{helperText}</p>
|
||||
</div>
|
||||
),
|
||||
[cloneIcon, state, helperIconCls, helperText, helperTextCls],
|
||||
);
|
||||
);
|
||||
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-y-2 w-full">
|
||||
{renderLabels}
|
||||
<div className={containerCls({ class: className })}>
|
||||
{leftIcon && renderLeftIcon}
|
||||
<input
|
||||
className={cn(inputCls({ class: 'w-80' }), {
|
||||
className={cn(inputCls(), {
|
||||
'pl-10': leftIcon,
|
||||
})}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
export const modalTheme = tv({
|
||||
slots: {
|
||||
overlay: [
|
||||
'z-modal',
|
||||
'fixed',
|
||||
'inset-0',
|
||||
'bg-black/80',
|
||||
'backdrop-blur-sm',
|
||||
'overflow-y-auto',
|
||||
'flex',
|
||||
'justify-center',
|
||||
'items-end',
|
||||
'sm:items-center',
|
||||
'p-0',
|
||||
'sm:p-10',
|
||||
'data-[state=closed]:animate-[dialog-overlay-hide_200ms]',
|
||||
'data-[state=open]:animate-[dialog-overlay-show_200ms]',
|
||||
'data-[state=closed]:hidden', // Fix overlay not close when modal is closed
|
||||
],
|
||||
close: ['absolute', 'right-4', 'top-2', 'sm:right-6', 'sm:top-3', 'z-[1]'],
|
||||
header: [
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-4',
|
||||
'items-start',
|
||||
'px-4',
|
||||
'py-4',
|
||||
'sm:px-6',
|
||||
'sm:py-5',
|
||||
'bg-base-bg-alternate',
|
||||
],
|
||||
headerTitle: [
|
||||
'text-base',
|
||||
'sm:text-lg',
|
||||
'tracking-[0.011em]',
|
||||
'sm:tracking-normal',
|
||||
'text-elements-high-em',
|
||||
],
|
||||
headerDescription: ['text-sm', 'text-elements-low-em'],
|
||||
footer: ['flex', 'gap-3', 'px-4', 'pb-4', 'pt-7', 'sm:pb-6', 'sm:px-6'],
|
||||
content: [
|
||||
'h-fit',
|
||||
'sm:min-h-0',
|
||||
'sm:m-auto',
|
||||
'relative',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'overflow-hidden',
|
||||
'w-full',
|
||||
'sm:max-w-[562px]',
|
||||
'rounded-2xl',
|
||||
'bg-base-bg',
|
||||
'shadow-card',
|
||||
'text-elements-high-em',
|
||||
],
|
||||
body: ['flex-1', 'px-4', 'pt-4', 'sm:pt-6', 'sm:px-6'],
|
||||
},
|
||||
variants: {
|
||||
fullPage: {
|
||||
true: {
|
||||
content: ['h-full'],
|
||||
overlay: ['!p-0'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
fullPage: false,
|
||||
},
|
||||
});
|
||||
export type ModalVariants = VariantProps<typeof modalTheme>;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import type { DialogProps } from '@radix-ui/react-dialog';
|
||||
import { Root, Trigger } from '@radix-ui/react-dialog';
|
||||
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||
|
||||
import type { ModalVariants } from './Modal.theme';
|
||||
import { ModalBody } from './ModalBody';
|
||||
import { ModalContent } from './ModalContent';
|
||||
import { ModalFooter } from './ModalFooter';
|
||||
import { ModalHeader } from './ModalHeader';
|
||||
import ModalProvider from './ModalProvider';
|
||||
|
||||
export interface ModalProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
ModalVariants,
|
||||
DialogProps {
|
||||
hasCloseButton?: boolean;
|
||||
hasOverlay?: boolean;
|
||||
preventClickOutsideToClose?: boolean;
|
||||
}
|
||||
export const Modal = ({
|
||||
children,
|
||||
hasCloseButton = true,
|
||||
hasOverlay = true,
|
||||
preventClickOutsideToClose = false,
|
||||
fullPage = false,
|
||||
...props
|
||||
}: PropsWithChildren<ModalProps>) => {
|
||||
return (
|
||||
<ModalProvider
|
||||
fullPage={fullPage}
|
||||
hasCloseButton={hasCloseButton}
|
||||
hasOverlay={hasOverlay}
|
||||
preventClickOutsideToClose={preventClickOutsideToClose}
|
||||
{...props}
|
||||
>
|
||||
<Root {...props}>{children}</Root>
|
||||
</ModalProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.Trigger = Trigger;
|
||||
Modal.Content = ModalContent;
|
||||
Modal.Header = ModalHeader;
|
||||
Modal.Footer = ModalFooter;
|
||||
Modal.Body = ModalBody;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||
|
||||
export interface ModalBodyProps extends ComponentPropsWithoutRef<'div'> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ModalBody = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<ModalBodyProps>) => {
|
||||
const { body } = modalTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={body({
|
||||
className,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ModalBody';
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { Close, Content, Overlay, Portal } from '@radix-ui/react-dialog';
|
||||
import { Ref, forwardRef, type PropsWithChildren } from 'react';
|
||||
import { useModal } from 'components/shared/Modal/ModalProvider';
|
||||
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { CrossIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
type PointerDownOutsideEvent = CustomEvent<{
|
||||
originalEvent: PointerEvent;
|
||||
}>;
|
||||
|
||||
export interface ModalContentProps extends DialogContentProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ModalContent = forwardRef(
|
||||
(
|
||||
{ children, className, ...props }: PropsWithChildren<ModalContentProps>,
|
||||
forwardedRef,
|
||||
) => {
|
||||
const { hasCloseButton, preventClickOutsideToClose, fullPage } = useModal();
|
||||
|
||||
const { content, close, overlay } = modalTheme({ fullPage });
|
||||
|
||||
const preventClickOutsideToCloseProps = preventClickOutsideToClose && {
|
||||
onPointerDownOutside: (e: PointerDownOutsideEvent) => e.preventDefault(),
|
||||
onEscapeKeyDown: (e: KeyboardEvent) => e.preventDefault(),
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Overlay className={overlay({ fullPage })}>
|
||||
<Content
|
||||
className={content({ className, fullPage })}
|
||||
{...preventClickOutsideToCloseProps}
|
||||
{...props}
|
||||
ref={forwardedRef as Ref<HTMLDivElement>}
|
||||
>
|
||||
{hasCloseButton && (
|
||||
<Close asChild>
|
||||
<Button
|
||||
aria-label="Close"
|
||||
className={close()}
|
||||
size="sm"
|
||||
iconOnly
|
||||
variant="tertiary"
|
||||
>
|
||||
<CrossIcon />
|
||||
</Button>
|
||||
</Close>
|
||||
)}
|
||||
{children}
|
||||
</Content>
|
||||
</Overlay>
|
||||
</Portal>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ModalContent.displayName = 'ModalContent';
|
||||
|
||||
export { ModalContent };
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ModalContent';
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||
|
||||
type ModalFooterProps = ComponentPropsWithoutRef<'div'> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ModalFooter = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<ModalFooterProps>) => {
|
||||
const { footer } = modalTheme({
|
||||
className,
|
||||
});
|
||||
|
||||
return (
|
||||
<footer className={footer({ className })} {...props}>
|
||||
{children}
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ModalFooter';
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import type { DialogDescriptionProps } from '@radix-ui/react-dialog';
|
||||
import { Description, Title } from '@radix-ui/react-dialog';
|
||||
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
|
||||
type ModalHeaderProps = ComponentPropsWithoutRef<'div'> & {
|
||||
className?: string;
|
||||
description?: string | React.ReactNode;
|
||||
descriptionProps?: DialogDescriptionProps;
|
||||
headingProps?: ComponentPropsWithoutRef<'h2'>;
|
||||
};
|
||||
|
||||
export const ModalHeader = ({
|
||||
children,
|
||||
description,
|
||||
className,
|
||||
descriptionProps,
|
||||
headingProps,
|
||||
...props
|
||||
}: PropsWithChildren<ModalHeaderProps>) => {
|
||||
const { header, headerDescription, headerTitle } = modalTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={header({
|
||||
className,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Title asChild>
|
||||
<Heading
|
||||
{...headingProps}
|
||||
className={headerTitle({ className: headingProps?.className })}
|
||||
>
|
||||
{children}
|
||||
</Heading>
|
||||
</Title>
|
||||
{description && (
|
||||
<Description
|
||||
{...descriptionProps}
|
||||
className={headerDescription({
|
||||
className: descriptionProps?.className,
|
||||
})}
|
||||
>
|
||||
{description}
|
||||
</Description>
|
||||
)}
|
||||
</div>
|
||||
<WavyBorder className="-mt-0.5" variant="stroke" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ModalHeader';
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import type { ModalProps } from './Modal';
|
||||
import type { ModalVariants } from './Modal.theme';
|
||||
|
||||
export interface ModalProviderProps
|
||||
extends Partial<ModalVariants>,
|
||||
ModalProps {}
|
||||
|
||||
type ModalProviderContext = ReturnType<typeof useModalValues>;
|
||||
|
||||
const ModalContext = createContext<Partial<ModalProviderContext> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// For inferring return type
|
||||
const useModalValues = (props: ModalProviderProps) => {
|
||||
return props;
|
||||
};
|
||||
|
||||
export const ModalProvider = ({
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<ModalProviderProps>): JSX.Element => {
|
||||
const values = useModalValues(props);
|
||||
|
||||
return (
|
||||
<ModalContext.Provider value={values}>{children}</ModalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useModal = () => {
|
||||
const context = useContext(ModalContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useModal was used outside of its Provider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default ModalProvider;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Modal';
|
||||
@@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const radioTheme = tv({
|
||||
slots: {
|
||||
root: ['flex', 'gap-3', 'flex-wrap'],
|
||||
root: ['flex', 'gap-3'],
|
||||
wrapper: ['flex', 'items-center', 'gap-2', 'group'],
|
||||
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
|
||||
radio: [
|
||||
@@ -39,15 +39,34 @@ export const radioTheme = tv({
|
||||
'after:data-[state=checked]:group-hover:bg-elements-on-primary',
|
||||
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
|
||||
],
|
||||
icon: ['w-[18px]', 'h-[18px]'],
|
||||
},
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: { root: ['flex-col'] },
|
||||
horizontal: { root: ['flex-row'] },
|
||||
},
|
||||
variant: {
|
||||
unstyled: {},
|
||||
card: {
|
||||
wrapper: [
|
||||
'px-4',
|
||||
'py-3',
|
||||
'rounded-lg',
|
||||
'border',
|
||||
'border-border-interactive',
|
||||
'bg-controls-tertiary',
|
||||
'shadow-button',
|
||||
'w-full',
|
||||
'cursor-pointer',
|
||||
],
|
||||
label: ['select-none', 'cursor-pointer'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
variant: 'unstyled',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -49,14 +49,15 @@ export const Radio = ({
|
||||
className,
|
||||
options,
|
||||
orientation,
|
||||
variant,
|
||||
...props
|
||||
}: RadioProps) => {
|
||||
const { root } = radioTheme({ orientation });
|
||||
const { root } = radioTheme({ orientation, variant });
|
||||
|
||||
return (
|
||||
<RadixRoot {...props} className={root({ className })}>
|
||||
{options.map((option) => (
|
||||
<RadioItem key={option.value} {...option} />
|
||||
<RadioItem key={option.value} variant={variant} {...option} />
|
||||
))}
|
||||
</RadixRoot>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import React, { ReactNode, ComponentPropsWithoutRef } from 'react';
|
||||
import {
|
||||
Item as RadixRadio,
|
||||
Indicator as RadixIndicator,
|
||||
RadioGroupItemProps,
|
||||
RadioGroupIndicatorProps,
|
||||
} from '@radix-ui/react-radio-group';
|
||||
import { radioTheme } from './Radio.theme';
|
||||
import { RadioTheme, radioTheme } from './Radio.theme';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
export interface RadioItemProps extends RadioGroupItemProps {
|
||||
export interface RadioItemProps
|
||||
extends RadioGroupItemProps,
|
||||
Pick<RadioTheme, 'variant'> {
|
||||
/**
|
||||
* The wrapper props of the radio item.
|
||||
* You can use this prop to customize the wrapper props.
|
||||
@@ -27,6 +30,10 @@ export interface RadioItemProps extends RadioGroupItemProps {
|
||||
* The id of the radio item.
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* The left icon of the radio item.
|
||||
*/
|
||||
leftIcon?: ReactNode;
|
||||
/**
|
||||
* The label of the radio item.
|
||||
*/
|
||||
@@ -41,18 +48,29 @@ export const RadioItem = ({
|
||||
wrapperProps,
|
||||
labelProps,
|
||||
indicatorProps,
|
||||
leftIcon,
|
||||
label,
|
||||
id,
|
||||
variant,
|
||||
...props
|
||||
}: RadioItemProps) => {
|
||||
const { wrapper, label: labelClass, radio, indicator } = radioTheme();
|
||||
const {
|
||||
wrapper,
|
||||
label: labelClass,
|
||||
radio,
|
||||
indicator,
|
||||
icon,
|
||||
} = radioTheme({ variant });
|
||||
|
||||
// Generate a unique id for the radio item from the label if the id is not provided
|
||||
const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
|
||||
const componentId = id ?? kebabCaseLabel;
|
||||
|
||||
return (
|
||||
<div className={wrapper({ className: wrapperProps?.className })}>
|
||||
<label
|
||||
htmlFor={componentId}
|
||||
className={wrapper({ className: wrapperProps?.className })}
|
||||
>
|
||||
<RadixRadio {...props} className={radio({ className })} id={componentId}>
|
||||
<RadixIndicator
|
||||
forceMount
|
||||
@@ -60,15 +78,20 @@ export const RadioItem = ({
|
||||
className={indicator({ className: indicatorProps?.className })}
|
||||
/>
|
||||
</RadixRadio>
|
||||
{leftIcon && (
|
||||
<span>
|
||||
{cloneIcon(leftIcon, { className: icon(), 'aria-hidden': true })}
|
||||
</span>
|
||||
)}
|
||||
{label && (
|
||||
<label
|
||||
{...labelProps}
|
||||
className={labelClass({ className: labelProps?.className })}
|
||||
htmlFor={componentId}
|
||||
className={labelClass({ className: labelProps?.className })}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const selectTheme = tv({
|
||||
slots: {
|
||||
container: ['flex', 'flex-col', 'relative', 'gap-2'],
|
||||
container: ['flex', 'flex-col', 'relative', 'gap-2', 'w-full'],
|
||||
label: ['text-sm', 'text-elements-high-em'],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
inputWrapper: [
|
||||
@@ -85,7 +85,7 @@ export const selectTheme = tv({
|
||||
size: {
|
||||
md: {
|
||||
container: ['min-h-11'],
|
||||
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4', 'py-1'],
|
||||
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4'],
|
||||
icon: ['h-[18px]', 'w-[18px]'],
|
||||
helperText: 'text-sm',
|
||||
helperIcon: ['h-5', 'w-5'],
|
||||
@@ -93,7 +93,7 @@ export const selectTheme = tv({
|
||||
},
|
||||
sm: {
|
||||
container: ['min-h-8'],
|
||||
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3', 'py-0.5'],
|
||||
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
helperText: 'text-xs',
|
||||
helperIcon: ['h-4', 'w-4'],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user