forked from cerc-io/snowballtools-base
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ba84fccd | ||
|
|
d770926651 | ||
|
|
e46544cfd3 | ||
|
|
44e38740a2 | ||
|
|
4ca1a4bcfd | ||
|
|
d6bdcf8a6c | ||
|
|
d2990424d2 | ||
|
|
d943423780 | ||
|
|
239df3661e | ||
|
|
bdd892ebe8 |
Vendored
+1
-2
@@ -1,7 +1,6 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
|
||||
```bash
|
||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
||||
cd snowballtools-base
|
||||
```
|
||||
|
||||
- Install dependencies in root
|
||||
- In root of the repo, install depedencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
@@ -21,212 +20,250 @@
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
Let us assume the following domains for backend and 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
|
||||
- Backend server: `api.snowballtools.com`
|
||||
- Frontend app: `dashboard.snowballtools.com`
|
||||
|
||||
- Configuration Files
|
||||
|
||||
- For Backend:
|
||||
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```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
|
||||
```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 = ""
|
||||
```
|
||||
|
||||
- Start the frontend
|
||||
- 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`
|
||||
|
||||
```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,6 +9,14 @@ 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.16",
|
||||
"@cerc-io/laconic-sdk": "^0.1.14",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
|
||||
@@ -12,14 +12,12 @@ 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 {
|
||||
@@ -113,21 +111,16 @@ 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,
|
||||
@@ -146,9 +139,9 @@ export class Registry {
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
packageJsonName: string,
|
||||
repository: string,
|
||||
environmentVariables: { [key: string]: string },
|
||||
dns: string,
|
||||
environmentVariables: { [key: string]: string }
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
@@ -167,7 +160,7 @@ export class Registry {
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: data.dns,
|
||||
dns: `${data.deployment.project.name}-${data.deployment.id}`,
|
||||
|
||||
// TODO: Not set in test-progressive-web-app CI
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||
@@ -185,7 +178,6 @@ export class Registry {
|
||||
})
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
@@ -219,12 +211,11 @@ export class Registry {
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||
// Filter records with ApplicationRecord ids
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationRecordId === record.attributes.application &&
|
||||
record.attributes.url.includes(deployment.id)
|
||||
deployment.applicationRecordId === record.attributes.application
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -382,7 +382,8 @@ export class Service {
|
||||
async createDeployment (
|
||||
userId: string,
|
||||
octokit: Octokit,
|
||||
data: DeepPartial<Deployment>
|
||||
data: DeepPartial<Deployment>,
|
||||
recordData: { repoUrl?: string } = {}
|
||||
): Promise<Deployment> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
log(
|
||||
@@ -406,10 +407,13 @@ export class Service {
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
const repoUrl = (await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo
|
||||
})).data.html_url;
|
||||
if (!recordData.repoUrl) {
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo
|
||||
});
|
||||
recordData.repoUrl = repoDetails.html_url;
|
||||
}
|
||||
|
||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||
const { applicationRecordId, applicationRecordData } =
|
||||
@@ -418,7 +422,7 @@ export class Service {
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl
|
||||
repoUrl: recordData.repoUrl
|
||||
});
|
||||
|
||||
// Update previous deployment with prod branch domain
|
||||
@@ -460,23 +464,11 @@ export class Service {
|
||||
{
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
repository: repoUrl,
|
||||
environmentVariables: environmentVariablesObj,
|
||||
dns: `${newDeployment.project.name}-${newDeployment.id}`
|
||||
packageJsonName: packageJSON.name,
|
||||
repository: recordData.repoUrl,
|
||||
environmentVariables: environmentVariablesObj
|
||||
});
|
||||
|
||||
// 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;
|
||||
@@ -506,6 +498,8 @@ 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,
|
||||
@@ -516,6 +510,9 @@ export class Service {
|
||||
domain: null,
|
||||
commitHash: latestCommit.sha,
|
||||
commitMessage: latestCommit.commit.message
|
||||
},
|
||||
{
|
||||
repoUrl: repoDetails.html_url
|
||||
}
|
||||
);
|
||||
|
||||
@@ -558,14 +555,8 @@ export class Service {
|
||||
}
|
||||
|
||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||
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 { repository, ref, head_commit: headCommit } = data;
|
||||
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
||||
const projects = await this.db.getProjects({
|
||||
where: { repository: repository.full_name }
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ export interface GitPushEventPayload {
|
||||
id: string;
|
||||
message: string;
|
||||
};
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
|
||||
@@ -66,5 +66,3 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
return savedEntity;
|
||||
};
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
@@ -4,10 +4,51 @@
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
```bash
|
||||
brew install jq # if you do not have jq installed already
|
||||
|
||||
- Replace variables in the following files
|
||||
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||
- `<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)
|
||||
```bash
|
||||
...
|
||||
RCD_APP_VERSION="0.1.0"
|
||||
REPO_REF="<COMMIT_HASH>"
|
||||
...
|
||||
```
|
||||
|
||||
- Run script to deploy app
|
||||
```
|
||||
./deploy-frontend.sh
|
||||
@@ -30,9 +71,6 @@
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
|
||||
- Check records in [registry console app](https://console.laconic.com/#/registry).
|
||||
|
||||
- If deployment fails due to low bond balance
|
||||
- Check balances
|
||||
```bash
|
||||
@@ -40,9 +78,9 @@
|
||||
yarn laconic cns account get
|
||||
|
||||
# Bond balance
|
||||
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
yarn laconic cns bond get --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||
```
|
||||
- Command to refill bond
|
||||
```bash
|
||||
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
||||
```
|
||||
yarn laconic cns bond refill --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac --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: 1200000
|
||||
gas: 1000000
|
||||
fees: 200000aphoton
|
||||
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
|
||||
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||
|
||||
@@ -1,67 +1,11 @@
|
||||
#!/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.0"
|
||||
REPO_REF="353fd0f6219a955be186b5914e93baa9058b4473"
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
@@ -71,11 +15,8 @@ echo $RECORD_ID
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${RCD_APP_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${REPO_REF}" "$RECORD_ID"
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
@@ -89,7 +30,6 @@ 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,18 +1,21 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@0.1.8
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
||||
name: snowballtools-base-frontend@0.1.0
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.0
|
||||
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
|
||||
# If GitHub client ID is changed, same ID and corresponding secret has to be set in backend config
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: 1ed99146e0d07aa62d44
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: deep-stack/test-progressive-web-app
|
||||
# New config env after changes for image upload PWA
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: deep-stack/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: deep-stack/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||
# Set CURRENT_DATE_TIME; Use command date -u
|
||||
note: Added by Snowball @ <CURRENT_DATE_TIME>
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
repository_ref: 353fd0f6219a955be186b5914e93baa9058b4473
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.2
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
version: 0.0.5
|
||||
repository_ref: 353fd0f6219a955be186b5914e93baa9058b4473
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
# name is set to repo name
|
||||
name: snowballtools-base-frontend
|
||||
app_version: 0.1.8
|
||||
# app_version is set from package.json
|
||||
app_version: 0.1.0
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
@@ -8,7 +8,6 @@
|
||||
"@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",
|
||||
@@ -31,9 +30,7 @@
|
||||
"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",
|
||||
@@ -51,7 +48,7 @@
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
<svg width="333" height="5" viewBox="0 0 333 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 4L6.39555 1.30714C8.38078 0.47125 10.6192 0.47125 12.6045 1.30714L15.8955 2.69286C17.8808 3.52875 20.1192 3.52875 22.1045 2.69286L25.3955 1.30714C27.3808 0.47125 29.6192 0.47125 31.6045 1.30714L34.8955 2.69286C36.8808 3.52875 39.1192 3.52875 41.1045 2.69286L44.3955 1.30714C46.3808 0.47125 48.6192 0.47125 50.6045 1.30714L53.8955 2.69286C55.8808 3.52875 58.1192 3.52875 60.1045 2.69286L63.3955 1.30714C65.3808 0.47125 67.6192 0.47125 69.6045 1.30714L72.8955 2.69286C74.8808 3.52875 77.1192 3.52875 79.1045 2.69286L82.3955 1.30714C84.3808 0.47125 86.6192 0.47125 88.6045 1.30714L91.8955 2.69286C93.8808 3.52875 96.1192 3.52875 98.1045 2.69286L101.396 1.30714C103.381 0.47125 105.619 0.47125 107.604 1.30714L110.896 2.69286C112.881 3.52875 115.119 3.52875 117.104 2.69286L120.396 1.30714C122.381 0.47125 124.619 0.47125 126.604 1.30714L129.896 2.69286C131.881 3.52875 134.119 3.52875 136.104 2.69286L139.396 1.30714C141.381 0.47125 143.619 0.47125 145.604 1.30714L148.896 2.69286C150.881 3.52875 153.119 3.52875 155.104 2.69286L158.396 1.30714C160.381 0.47125 162.619 0.47125 164.604 1.30714L167.896 2.69286C169.881 3.52875 172.119 3.52875 174.104 2.69286L177.396 1.30714C179.381 0.47125 181.619 0.47125 183.604 1.30714L186.896 2.69286C188.881 3.52875 191.119 3.52875 193.104 2.69286L196.396 1.30714C198.381 0.47125 200.619 0.47125 202.604 1.30714L205.896 2.69286C207.881 3.52875 210.119 3.52875 212.104 2.69286L215.396 1.30714C217.381 0.47125 219.619 0.47125 221.604 1.30714L224.896 2.69286C226.881 3.52875 229.119 3.52875 231.104 2.69286L234.396 1.30714C236.381 0.47125 238.619 0.47125 240.604 1.30714L243.896 2.69286C245.881 3.52875 248.119 3.52875 250.104 2.69286L253.396 1.30714C255.381 0.47125 257.619 0.47125 259.604 1.30714L262.896 2.69286C264.881 3.52875 267.119 3.52875 269.104 2.69286L272.396 1.30714C274.381 0.47125 276.619 0.47125 278.604 1.30714L281.896 2.69286C283.881 3.52875 286.119 3.52875 288.104 2.69286L291.396 1.30714C293.381 0.47125 295.619 0.47125 297.604 1.30714L300.896 2.69286C302.881 3.52875 305.119 3.52875 307.104 2.69286L310.396 1.30714C312.381 0.47125 314.619 0.47125 316.604 1.30714L319.973 2.72566C321.913 3.54216 324.095 3.56183 326.049 2.78039L330.029 1.18845C331.936 0.425535 334.064 0.425535 335.971 1.18845L343 4" stroke="#DBEBF9"/>
|
||||
<path d="M6.39555 1.30714L0 4H342.5L336.027 1.27434C334.087 0.457837 331.905 0.438174 329.951 1.21961L326.049 2.78039C324.095 3.56183 321.913 3.54216 319.973 2.72566L316.604 1.30714C314.619 0.47125 312.381 0.47125 310.396 1.30714L307.104 2.69286C305.119 3.52875 302.881 3.52875 300.896 2.69286L297.604 1.30714C295.619 0.47125 293.381 0.47125 291.396 1.30714L288.104 2.69286C286.119 3.52875 283.881 3.52875 281.896 2.69286L278.604 1.30714C276.619 0.47125 274.381 0.47125 272.396 1.30714L269.104 2.69286C267.119 3.52875 264.881 3.52875 262.896 2.69286L259.604 1.30714C257.619 0.47125 255.381 0.47125 253.396 1.30714L250.104 2.69286C248.119 3.52875 245.881 3.52875 243.896 2.69286L240.604 1.30714C238.619 0.47125 236.381 0.47125 234.396 1.30714L231.104 2.69286C229.119 3.52875 226.881 3.52875 224.896 2.69286L221.604 1.30714C219.619 0.47125 217.381 0.47125 215.396 1.30714L212.104 2.69286C210.119 3.52875 207.881 3.52875 205.896 2.69286L202.604 1.30714C200.619 0.47125 198.381 0.47125 196.396 1.30714L193.104 2.69286C191.119 3.52875 188.881 3.52875 186.896 2.69286L183.604 1.30714C181.619 0.47125 179.381 0.47125 177.396 1.30714L174.104 2.69286C172.119 3.52875 169.881 3.52875 167.896 2.69286L164.604 1.30714C162.619 0.47125 160.381 0.47125 158.396 1.30714L155.104 2.69286C153.119 3.52875 150.881 3.52875 148.896 2.69286L145.604 1.30714C143.619 0.47125 141.381 0.47125 139.396 1.30714L136.104 2.69286C134.119 3.52875 131.881 3.52875 129.896 2.69286L126.604 1.30714C124.619 0.47125 122.381 0.47125 120.396 1.30714L117.104 2.69286C115.119 3.52875 112.881 3.52875 110.896 2.69286L107.604 1.30714C105.619 0.47125 103.381 0.47125 101.396 1.30714L98.1045 2.69286C96.1192 3.52875 93.8808 3.52875 91.8955 2.69286L88.6045 1.30714C86.6192 0.47125 84.3808 0.47125 82.3955 1.30714L79.1045 2.69286C77.1192 3.52875 74.8808 3.52875 72.8955 2.69286L69.6045 1.30714C67.6192 0.47125 65.3808 0.47125 63.3955 1.30714L60.1045 2.69286C58.1192 3.52875 55.8808 3.52875 53.8955 2.69286L50.6045 1.30714C48.6192 0.47125 46.3808 0.47125 44.3955 1.30714L41.1045 2.69286C39.1192 3.52875 36.8808 3.52875 34.8955 2.69286L31.6045 1.30714C29.6192 0.47125 27.3808 0.47125 25.3955 1.30714L22.1045 2.69286C20.1192 3.52875 17.8808 3.52875 15.8955 2.69286L12.6045 1.30714C10.6192 0.47125 8.38078 0.47125 6.39555 1.30714Z" fill="url(#paint0_linear_1729_11298)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1729_11298" x1="171.25" y1="0" x2="171.25" y2="4" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E6F4FF"/>
|
||||
<stop offset="1" stop-color="#F9FCFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="currentColor"/>
|
||||
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,15 +0,0 @@
|
||||
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1177_19315)">
|
||||
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="url(#paint0_linear_1177_19315)"/>
|
||||
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="#DBEBF9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1177_19315" x1="9.25" y1="0.650024" x2="9.25" y2="3.9698" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EBF6FF"/>
|
||||
<stop offset="1" stop-color="#F7FCFF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1177_19315">
|
||||
<rect width="19" height="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1175_19292)">
|
||||
<path d="M-9.5 0L-3.10445 2.69286C-1.11922 3.52875 1.11922 3.52875 3.10446 2.69286L6.39555 1.30714C8.38078 0.47125 10.6192 0.47125 12.6045 1.30714L15.8955 2.69286C17.8808 3.52875 20.1192 3.52875 22.1045 2.69286L28.5 0" stroke="#082F56" stroke-opacity="0.1"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1175_19292">
|
||||
<rect width="19" height="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 507 B |
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import OrgSlug from './pages/OrgSlug';
|
||||
import Projects from './pages/org-slug';
|
||||
import Settings from './pages/org-slug/Settings';
|
||||
import {
|
||||
@@ -10,12 +11,11 @@ import {
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import Index from './pages';
|
||||
import Login from './pages/Login';
|
||||
import { DashboardLayout } from 'pages/org-slug/layout';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: ':orgSlug',
|
||||
element: <DashboardLayout />,
|
||||
element: <OrgSlug />,
|
||||
children: [
|
||||
{
|
||||
element: <ProjectSearchLayout />,
|
||||
|
||||
@@ -2,36 +2,31 @@ export default [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: 'pwa',
|
||||
icon: '^',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Image Upload PWA',
|
||||
icon: 'pwa',
|
||||
icon: '^',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Swift',
|
||||
icon: 'swift',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { Duration } from 'luxon';
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
import React from 'react';
|
||||
|
||||
export interface FormatMilliSecondProps
|
||||
extends ComponentPropsWithoutRef<'div'> {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
|
||||
const FormatMillisecond = ({ time }: { time: number }) => {
|
||||
const formatTime = Duration.fromMillis(time)
|
||||
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
||||
.toObject();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('text-sm text-elements-mid-em', props?.className)}
|
||||
>
|
||||
<div>
|
||||
{formatTime.days !== 0 && <span>{formatTime.days}d </span>}
|
||||
{formatTime.hours !== 0 && <span>{formatTime.hours}h </span>}
|
||||
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m </span>}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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,8 +15,7 @@ const SearchBar: React.ForwardRefRenderFunction<
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
appearance="borderless"
|
||||
className="w-full lg:w-[459px]"
|
||||
appearance={'borderless'}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Organization } from 'gql-client';
|
||||
|
||||
import { Option } from '@material-tailwind/react';
|
||||
import { useDisconnect } from 'wagmi';
|
||||
|
||||
import { useGQLClient } from '../context/GQLClientContext';
|
||||
import AsyncSelect from './shared/AsyncSelect';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
FolderIcon,
|
||||
GlobeIcon,
|
||||
LifeBuoyIcon,
|
||||
QuestionMarkRoundIcon,
|
||||
SettingsSlidersIcon,
|
||||
} from './shared/CustomIcon';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
|
||||
const Sidebar = () => {
|
||||
const { orgSlug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
|
||||
const fetchUserOrganizations = useCallback(async () => {
|
||||
const { organizations } = await client.getOrganizations();
|
||||
setOrganizations(organizations);
|
||||
}, [orgSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserOrganizations();
|
||||
setSelectedOrgSlug(orgSlug);
|
||||
}, [orgSlug]);
|
||||
|
||||
const handleLogOut = useCallback(() => {
|
||||
disconnect();
|
||||
navigate('/login');
|
||||
}, [disconnect, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4 pt-10">
|
||||
<div className="grow">
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<div className="flex items-center space-x-3 mb-10 ml-2">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Snowball Logo"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<span className="text-2xl font-bold text-snowball-900">
|
||||
Snowball
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<AsyncSelect
|
||||
containerProps={{ className: 'h-14 border-none' }}
|
||||
labelProps={{ className: 'before:border-none after:border-none' }}
|
||||
className="bg-white rounded-lg shadow border-none"
|
||||
value={selectedOrgSlug}
|
||||
onChange={(value) => {
|
||||
setSelectedOrgSlug(value!);
|
||||
navigate(`/${value}`);
|
||||
}}
|
||||
selected={(_, index) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Application Logo"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-semibold">
|
||||
{organizations[index!]?.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Organization</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
|
||||
>
|
||||
{/* TODO: Show label organization and manage in option */}
|
||||
{organizations.map((org) => (
|
||||
<Option key={org.id} value={org.slug}>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Application Logo"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-semibold">{org.name}</div>
|
||||
<div className="text-xs text-gray-500">Organization</div>
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
<Tabs defaultValue="Projects" orientation="vertical" className="mt-10">
|
||||
<Tabs.List>
|
||||
{[
|
||||
{ title: 'Projects', icon: <FolderIcon /> },
|
||||
{ title: 'Settings', icon: <SettingsSlidersIcon /> },
|
||||
].map(({ title, icon }, index) => (
|
||||
<NavLink to={`/${orgSlug}/${title}`} key={index}>
|
||||
<Tabs.Trigger icon={icon} value={title}>
|
||||
{title}
|
||||
</Tabs.Trigger>
|
||||
</NavLink>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="grow flex flex-col justify-end mb-8">
|
||||
<Tabs defaultValue="Projects" orientation="vertical">
|
||||
{/* TODO: use proper link buttons */}
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger icon={<GlobeIcon />} value="">
|
||||
<a className="cursor-pointer" onClick={handleLogOut}>
|
||||
Log Out
|
||||
</a>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger icon={<QuestionMarkRoundIcon />} value="">
|
||||
<a className="cursor-pointer">Documentation</a>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger icon={<LifeBuoyIcon />} value="">
|
||||
<a className="cursor-pointer">Support</a>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useStopwatch } from 'react-timer-hook';
|
||||
|
||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
import FormatMillisecond from './FormatMilliSecond';
|
||||
|
||||
const setStopWatchOffset = (time: string) => {
|
||||
const providedTime = new Date(time);
|
||||
@@ -11,17 +11,13 @@ const setStopWatchOffset = (time: string) => {
|
||||
return currentTime;
|
||||
};
|
||||
|
||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => {
|
||||
const { totalSeconds } = useStopwatch({
|
||||
autoStart: true,
|
||||
offsetTimestamp: offsetTimestamp,
|
||||
});
|
||||
|
||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||
return <FormatMillisecond time={totalSeconds * 1000} />;
|
||||
};
|
||||
|
||||
export { Stopwatch, setStopWatchOffset };
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -8,30 +8,25 @@ export const projectCardTheme = tv({
|
||||
'rounded-2xl',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'group',
|
||||
'cursor-pointer',
|
||||
],
|
||||
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
|
||||
content: ['flex', 'flex-col', 'gap-1', 'flex-1', 'overflow-hidden'],
|
||||
content: ['flex', 'flex-col', 'gap-1', 'flex-1'],
|
||||
title: [
|
||||
'text-sm',
|
||||
'font-medium',
|
||||
'text-elements-high-em',
|
||||
'tracking-[-0.006em]',
|
||||
'truncate',
|
||||
],
|
||||
description: ['text-xs', 'text-elements-low-em', 'truncate'],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
icons: ['flex', 'items-center', 'gap-1'],
|
||||
lowerContent: [
|
||||
'transition-colors',
|
||||
'duration-150',
|
||||
'bg-surface-card-hovered',
|
||||
'px-4',
|
||||
'py-4',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-2',
|
||||
'rounded-b-2xl',
|
||||
'group-hover:bg-surface-card-hovered',
|
||||
],
|
||||
latestDeployment: ['flex', 'items-center', 'gap-2'],
|
||||
deploymentStatusContainer: [
|
||||
@@ -51,12 +46,6 @@ export const projectCardTheme = tv({
|
||||
'items-center',
|
||||
'gap-2',
|
||||
],
|
||||
wavyBorder: [
|
||||
'bg-surface-card',
|
||||
'transition-colors',
|
||||
'duration-150',
|
||||
'group-hover:bg-surface-card-hovered',
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
status: {
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { ComponentPropsWithoutRef, MouseEvent } from 'react';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
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,
|
||||
@@ -20,11 +10,16 @@ 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 { Link } from 'react-router-dom';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
export interface ProjectCardProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
@@ -32,8 +27,6 @@ export interface ProjectCardProps
|
||||
project: Project;
|
||||
}
|
||||
|
||||
// TODO: Update the whole component to use `Link` from `react-router-dom` and remove the `useNavigate` hook,
|
||||
// currently it's not possible to use `Link` because the dot menu is not a direct child of the `Link` component
|
||||
export const ProjectCard = ({
|
||||
className,
|
||||
project,
|
||||
@@ -45,24 +38,14 @@ export const ProjectCard = ({
|
||||
// TODO: Update this to use the actual status from the API
|
||||
const hasError = status === 'failure';
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOptionsClick = (
|
||||
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
navigate(`projects/${project.id}`);
|
||||
}, [project.id, navigate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={theme.wrapper({ className })}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div {...props} className={theme.wrapper({ className })}>
|
||||
{/* Upper content */}
|
||||
<div className={theme.upperContent()}>
|
||||
{/* Icon container */}
|
||||
@@ -71,15 +54,14 @@ export const ProjectCard = ({
|
||||
imageSrc={project.icon}
|
||||
initials={getInitials(project.name)}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* Title and website */}
|
||||
<div className={theme.content()}>
|
||||
<Tooltip content={project.name}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
</Tooltip>
|
||||
<Link to={`projects/${project.id}`} className={theme.content()}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
<p className={theme.description()}>
|
||||
{project.deployments[0]?.domain?.name ?? 'No domain'}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
{/* Icons */}
|
||||
<div className={theme.icons()}>
|
||||
{hasError && <WarningDiamondIcon className="text-elements-danger" />}
|
||||
@@ -105,7 +87,7 @@ export const ProjectCard = ({
|
||||
</div>
|
||||
</div>
|
||||
{/* Wave */}
|
||||
<WavyBorder variant="stroke-and-fill" className={theme.wavyBorder()} />
|
||||
<WavyBorder />
|
||||
{/* Lower content */}
|
||||
<div className={theme.lowerContent()}>
|
||||
{/* Latest deployment */}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
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;
|
||||
@@ -1,93 +0,0 @@
|
||||
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
@@ -1,112 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
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 };
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './ProjectSearchBar';
|
||||
export * from './ProjectSearchBarDialog';
|
||||
@@ -7,13 +7,7 @@ import {
|
||||
GitIcon,
|
||||
EllipsesIcon,
|
||||
SnowballIcon,
|
||||
GithubIcon,
|
||||
GitTeaIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { IconWithFrame } from 'components/shared/IconWithFrame';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { MockConnectGitCard } from './MockConnectGitCard';
|
||||
|
||||
const SCOPES = 'repo user';
|
||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||
@@ -29,72 +23,50 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
}: ConnectAccountInterface) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
const handleCode = async (code: string) => {
|
||||
// Pass code to backend and get access token
|
||||
const {
|
||||
authenticateGitHub: { token },
|
||||
} = await client.authenticateGitHub(code);
|
||||
onToken(token);
|
||||
toast({
|
||||
onDismiss: dismiss,
|
||||
id: 'connected-to-github',
|
||||
title: 'The Git account is connected.',
|
||||
variant: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Use correct height
|
||||
return (
|
||||
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||
<div className="flex flex-col items-center max-w-[420px]">
|
||||
{/** Icons */}
|
||||
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
|
||||
<IconWithFrame icon={<GitIcon />} />
|
||||
<EllipsesIcon className="items-center gap-1.5 flex" />
|
||||
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
|
||||
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 inline-flex">
|
||||
<GitIcon />
|
||||
</div>
|
||||
{/** Text */}
|
||||
<div className="flex flex-col gap-1.5 mb-6">
|
||||
<Heading className="text-xl font-medium">
|
||||
Connect to your Git account
|
||||
</Heading>
|
||||
<p className="text-center text-elements-mid-em">
|
||||
Once connected, you can import a repository from your account or
|
||||
start with one of our templates.
|
||||
</p>
|
||||
<EllipsesIcon className="items-center gap-1.5 flex" />
|
||||
<div className="w-16 h-16 bg-blue-400 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 flex">
|
||||
<SnowballIcon />
|
||||
</div>
|
||||
{/** CTA Buttons */}
|
||||
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
|
||||
<OauthPopup
|
||||
url={GITHUB_OAUTH_URL}
|
||||
onCode={handleCode}
|
||||
onClose={() => {}}
|
||||
title="Snowball"
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
leftIcon={<GithubIcon />}
|
||||
variant="tertiary"
|
||||
>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</OauthPopup>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
leftIcon={<GitTeaIcon />}
|
||||
variant="tertiary"
|
||||
>
|
||||
Connect to GitTea
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-center text-slate-900 text-xl font-medium leading-7">
|
||||
Connect to your Git account
|
||||
</div>
|
||||
<div className="text-center text-slate-600 text-base font-normal leading-normal">
|
||||
Once connected, you can import a repository from your account or start
|
||||
with one of our templates.
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-3">
|
||||
<OauthPopup
|
||||
url={GITHUB_OAUTH_URL}
|
||||
onCode={handleCode}
|
||||
onClose={() => {}}
|
||||
title="Snowball"
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button>Connect to GitHub</Button>
|
||||
</OauthPopup>
|
||||
<Button>Connect to Gitea</Button>
|
||||
</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,12 +1,11 @@
|
||||
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 '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';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||
|
||||
const TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
@@ -32,58 +31,63 @@ const Deploy = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<div>
|
||||
<div className="flex justify-between mb-6">
|
||||
<div>
|
||||
<h4>Deployment started ...</h4>
|
||||
<div className="flex">
|
||||
^
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
leftIcon={<WarningIcon size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<CancelDeploymentDialog
|
||||
handleCancel={handleOpen}
|
||||
<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}
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
This will halt the deployment and you will have to start the process
|
||||
from scratch.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
</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,22 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Collapse } from '@material-tailwind/react';
|
||||
import { Collapse, Button, Typography } 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',
|
||||
@@ -39,115 +28,61 @@ const DeployStep = ({
|
||||
startTime,
|
||||
processTime,
|
||||
}: DeployStepsProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { toast, dismiss } = useToast();
|
||||
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
||||
threshold: 1,
|
||||
});
|
||||
|
||||
const disableCollapse = status !== DeployStatus.COMPLETE;
|
||||
const [collapse, setCollapse] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="border-b border-border-separator">
|
||||
{/* Collapisble trigger */}
|
||||
<button
|
||||
className={cn(
|
||||
'flex justify-between w-full py-5 gap-2',
|
||||
disableCollapse && 'cursor-auto',
|
||||
<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>
|
||||
)}
|
||||
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 */}
|
||||
<div className="grow">{title}</div>
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</div>
|
||||
<>
|
||||
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</>
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<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>
|
||||
<>
|
||||
^<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Collapsible */}
|
||||
<Collapse open={isOpen}>
|
||||
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
|
||||
{/* Logs */}
|
||||
</div>
|
||||
<Collapse open={collapse}>
|
||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
||||
{processLogs.map((log, key) => {
|
||||
return (
|
||||
<p className="font-mono" key={key}>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
key={key}
|
||||
placeholder={''}
|
||||
>
|
||||
{log}
|
||||
</p>
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 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')}>
|
||||
<div className="sticky bottom-0 left-1/2 flex justify-center">
|
||||
<Button
|
||||
size="xs"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(processLogs.join('\n'));
|
||||
toast({
|
||||
title: 'Logs copied',
|
||||
variant: 'success',
|
||||
id: 'logs',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
toast.success('Logs copied');
|
||||
}}
|
||||
leftIcon={<CopyIcon size={16} />}
|
||||
color="blue"
|
||||
placeholder={''}
|
||||
>
|
||||
Copy log
|
||||
^ Copy log
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from '../../../utils/time';
|
||||
import { GitRepositoryDetails } from '../../../types';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const createProject = useCallback(async () => {
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug!, {
|
||||
name: `${repository.owner!.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch!,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
|
||||
if (Boolean(addProject)) {
|
||||
setIsLoading(false);
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
toast.error('Failed to create project');
|
||||
}
|
||||
}, [client, repository]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
||||
onClick={createProject}
|
||||
>
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<div>
|
||||
<span className="text-black">{repository.full_name}</span>
|
||||
{repository.visibility === 'private' && (
|
||||
<Chip
|
||||
className="normal-case inline ml-6 font-normal"
|
||||
size="sm"
|
||||
value="Private"
|
||||
icon={'^'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm" placeholder={''}>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRepoCard;
|
||||
@@ -1,114 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ProjectRepoCard';
|
||||
+46
-68
@@ -3,17 +3,12 @@ import { Octokit } from 'octokit';
|
||||
import assert from 'assert';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
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';
|
||||
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';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
@@ -22,9 +17,9 @@ interface RepositoryListProps {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
// TODO: Add new type for Git user when required
|
||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||
@@ -39,7 +34,7 @@ export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||
setSelectedAccount(user.data.login);
|
||||
};
|
||||
|
||||
fetchUserAndOrgs();
|
||||
@@ -58,7 +53,7 @@ export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||
|
||||
// Check if selected account is an organization
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
if (selectedAccount === gitUser.login) {
|
||||
query = query + ` user:${selectedAccount}`;
|
||||
} else {
|
||||
query = query + ` org:${selectedAccount}`;
|
||||
@@ -73,7 +68,7 @@ export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
if (selectedAccount === gitUser.login) {
|
||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
per_page: REPOS_PER_PAGE,
|
||||
affiliation: 'owner',
|
||||
@@ -82,9 +77,7 @@ export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOrg = orgs.find(
|
||||
(org) => org.login === selectedAccount.value,
|
||||
);
|
||||
const selectedOrg = orgs.find((org) => org.login === selectedAccount);
|
||||
assert(selectedOrg, 'Selected org not found in list');
|
||||
|
||||
const result = await octokit.rest.repos.listForOrg({
|
||||
@@ -102,7 +95,7 @@ export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const handleResetFilters = useCallback(() => {
|
||||
assert(gitUser, 'Git user is not available');
|
||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||
setSelectedAccount({ label: gitUser.login, value: gitUser.login });
|
||||
setSelectedAccount(gitUser.login);
|
||||
}, [gitUser]);
|
||||
|
||||
const accounts = useMemo(() => {
|
||||
@@ -113,65 +106,50 @@ export 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 (
|
||||
<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"
|
||||
<div className="p-4">
|
||||
<div className="flex gap-2 mb-2">
|
||||
<div className="basis-1/3">
|
||||
<AsyncSelect
|
||||
value={selectedAccount}
|
||||
leftIcon={selectedAccount ? <GithubIcon /> : undefined}
|
||||
rightIcon={<ChevronGrabberHorizontal />}
|
||||
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||
/>
|
||||
onChange={(value) => setSelectedAccount(value!)}
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<Option key={account.id} value={account.login}>
|
||||
^ {account.login}
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
</div>
|
||||
<div className="basis-2/3 flex w-full flex-grow">
|
||||
<Input
|
||||
className="w-full"
|
||||
<div className="basis-2/3">
|
||||
<SearchBar
|
||||
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) ? (
|
||||
<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>
|
||||
repositoryDetails.map((repo, key) => {
|
||||
return <ProjectRepoCard repository={repo} key={key} />;
|
||||
})
|
||||
) : (
|
||||
<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 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>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryList;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './RepositoryList';
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { IconButton, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface TemplateDetails {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
interface TemplateCardProps {
|
||||
template: TemplateDetails;
|
||||
isGitAuth: boolean;
|
||||
}
|
||||
|
||||
const CardDetails = ({ template }: { template: TemplateDetails }) => {
|
||||
return (
|
||||
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
|
||||
<Typography className="grow" placeholder={''}>
|
||||
{template.icon} {template.name}
|
||||
</Typography>
|
||||
<div>
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full hidden group-hover:block"
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TemplateCard: React.FC<TemplateCardProps> = ({ template, isGitAuth }) => {
|
||||
return isGitAuth ? (
|
||||
<Link to={`template?templateId=${template.id}`}>
|
||||
<CardDetails template={template} />
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
onClick={() =>
|
||||
toast.error('Connect Git account to start with a template')
|
||||
}
|
||||
>
|
||||
<CardDetails template={template} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateCard;
|
||||
@@ -1,91 +0,0 @@
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
ClockOutlineIcon,
|
||||
TemplateIcon,
|
||||
TemplateIconType,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
export interface TemplateDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
repoFullName?: string;
|
||||
isComingSoon?: boolean;
|
||||
}
|
||||
|
||||
export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> {
|
||||
template: TemplateDetail;
|
||||
isGitAuth: boolean;
|
||||
}
|
||||
|
||||
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) {
|
||||
return toast({
|
||||
id: 'coming-soon',
|
||||
title: 'This template is coming soon',
|
||||
variant: 'info',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
if (isGitAuth) {
|
||||
return navigate(
|
||||
`/${orgSlug}/projects/create/template?templateId=${template.id}`,
|
||||
);
|
||||
}
|
||||
return toast({
|
||||
id: 'connect-git-account',
|
||||
title: 'Connect Git account to start with a template',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||
|
||||
return (
|
||||
<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 cursor-pointer',
|
||||
{
|
||||
'cursor-default': template?.isComingSoon,
|
||||
},
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
|
||||
<TemplateIcon type={template.icon as TemplateIconType} />
|
||||
</div>
|
||||
{/* Name */}
|
||||
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
|
||||
{template.name}
|
||||
</p>
|
||||
{template?.isComingSoon ? (
|
||||
<Tag size="xs" type="neutral" leftIcon={<ClockOutlineIcon />}>
|
||||
Soon
|
||||
</Tag>
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
iconOnly
|
||||
className="group-hover:flex hidden absolute right-3"
|
||||
>
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './TemplateCard';
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Typography, IconButton, Avatar } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from '../../../utils/time';
|
||||
import { GitCommitWithBranch } from '../../../types';
|
||||
|
||||
interface ActivityCardProps {
|
||||
activity: GitCommitWithBranch;
|
||||
}
|
||||
|
||||
const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
return (
|
||||
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
|
||||
<div className="w-8">
|
||||
<Avatar
|
||||
src={activity.author?.avatar_url}
|
||||
variant="rounded"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<Typography placeholder={''}>{activity.commit.author?.name}</Typography>
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
{relativeTimeISO(activity.commit.author!.date!)} ^{' '}
|
||||
{activity.branch.name}
|
||||
</Typography>
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
{activity.commit.message}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="mr-2 self-center hidden group-hover:block">
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full bg-gray-600"
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityCard;
|
||||
+248
-123
@@ -1,28 +1,31 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
Domain,
|
||||
Environment,
|
||||
Project,
|
||||
Domain,
|
||||
DeploymentStatus,
|
||||
Deployment,
|
||||
} from 'gql-client';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
|
||||
import {
|
||||
BranchStrokeIcon,
|
||||
CheckRoundFilledIcon,
|
||||
ClockOutlineIcon,
|
||||
CommitIcon,
|
||||
LoadingIcon,
|
||||
WarningIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Chip,
|
||||
ChipProps,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import { DeploymentMenu } from './DeploymentMenu';
|
||||
|
||||
interface DeployDetailsCardProps {
|
||||
deployment: Deployment;
|
||||
@@ -32,12 +35,10 @@ interface DeployDetailsCardProps {
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
const STATUS_COLORS: {
|
||||
[key in DeploymentStatus]: TagTheme['type'];
|
||||
} = {
|
||||
[DeploymentStatus.Building]: 'emphasized',
|
||||
[DeploymentStatus.Ready]: 'positive',
|
||||
[DeploymentStatus.Error]: 'negative',
|
||||
const STATUS_COLORS: { [key in DeploymentStatus]: ChipProps['color'] } = {
|
||||
[DeploymentStatus.Building]: 'blue',
|
||||
[DeploymentStatus.Ready]: 'green',
|
||||
[DeploymentStatus.Error]: 'red',
|
||||
};
|
||||
|
||||
const DeploymentDetailsCard = ({
|
||||
@@ -47,117 +48,241 @@ const DeploymentDetailsCard = ({
|
||||
project,
|
||||
prodBranchDomains,
|
||||
}: DeployDetailsCardProps) => {
|
||||
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||
if (status === DeploymentStatus.Building) {
|
||||
return <LoadingIcon className="animate-spin" />;
|
||||
}
|
||||
if (status === DeploymentStatus.Ready) {
|
||||
return <CheckRoundFilledIcon />;
|
||||
}
|
||||
const client = useGQLClient();
|
||||
|
||||
if (status === DeploymentStatus.Error) {
|
||||
return <WarningIcon />;
|
||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||
|
||||
const updateDeployment = async () => {
|
||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||
if (isUpdated) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment changed to production');
|
||||
} else {
|
||||
toast.error('Unable to change deployment to production');
|
||||
}
|
||||
};
|
||||
|
||||
const renderDeploymentStatus = useCallback(
|
||||
(className?: string) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[deployment.status, deployment.commitHash],
|
||||
);
|
||||
const redeployToProd = async () => {
|
||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||
if (isRedeployed) {
|
||||
await onUpdate();
|
||||
toast.success('Redeployed to production');
|
||||
} else {
|
||||
toast.error('Unable to redeploy to production');
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackDeploymentHandler = async () => {
|
||||
const isRollbacked = await client.rollbackDeployment(
|
||||
project.id,
|
||||
deployment.id,
|
||||
);
|
||||
if (isRollbacked) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment rolled back');
|
||||
} else {
|
||||
toast.error('Unable to rollback deployment');
|
||||
}
|
||||
};
|
||||
|
||||
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 */}
|
||||
<div className="grid grid-cols-8 gap-2 border-b border-gray-300 p-3 my-2">
|
||||
<div className="col-span-3">
|
||||
<div className="flex">
|
||||
{deployment.url && (
|
||||
<Heading
|
||||
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||
as="h2"
|
||||
>
|
||||
<OverflownText content={deployment.url}>
|
||||
{deployment.url}
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
<Typography className="basis-3/4" placeholder={''}>
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</span>
|
||||
</div>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT STATUS */}
|
||||
{renderDeploymentStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')}
|
||||
|
||||
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||
<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="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>
|
||||
</div>
|
||||
<DeploymentMenu
|
||||
className="ml-auto md:static absolute top-4 right-0"
|
||||
deployment={deployment}
|
||||
currentDeployment={currentDeployment}
|
||||
onUpdate={onUpdate}
|
||||
project={project}
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
<div className="col-span-1">
|
||||
<Chip
|
||||
value={deployment.status}
|
||||
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
||||
variant="ghost"
|
||||
icon={<i>^</i>}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.branch}
|
||||
</Typography>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<Typography color="gray" className="grow" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
<Tooltip content={deployment.createdBy.name}>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
<Menu placement="bottom-start">
|
||||
<MenuHandler>
|
||||
<button className="self-start">...</button>
|
||||
</MenuHandler>
|
||||
<MenuList placeholder={''}>
|
||||
<a href={deployment.url} target="_blank" rel="noreferrer">
|
||||
<MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
|
||||
^ Visit
|
||||
</MenuItem>
|
||||
</a>
|
||||
<MenuItem
|
||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Assign domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||
disabled={!(deployment.environment !== Environment.Production)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Change to production
|
||||
</MenuItem>
|
||||
<hr className="my-3" />
|
||||
<MenuItem
|
||||
onClick={() => setRedeployToProduction(!redeployToProduction)}
|
||||
disabled={
|
||||
!(
|
||||
deployment.environment === Environment.Production &&
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Redeploy to production
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => setRollbackDeployment(!rollbackDeployment)}
|
||||
disabled={
|
||||
deployment.isCurrent ||
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Rollback to this version
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Change to production?"
|
||||
handleOpen={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
confirmButtonTitle="Change"
|
||||
color="blue"
|
||||
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
|
||||
dialogTitle="Redeploy to production?"
|
||||
handleOpen={() => 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>
|
||||
{Boolean(currentDeployment) && (
|
||||
<ConfirmDialog
|
||||
dialogTitle="Rollback to this deployment?"
|
||||
handleOpen={() => 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>
|
||||
)}
|
||||
<AssignDomainDialog
|
||||
open={assignDomainDialog}
|
||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+27
-56
@@ -1,23 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Deployment } from 'gql-client';
|
||||
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { Typography, Chip, Card } from '@material-tailwind/react';
|
||||
import { color } from '@material-tailwind/react/types/components/chip';
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
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';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
|
||||
interface DeploymentDialogBodyCardProps {
|
||||
deployment: Deployment;
|
||||
chip?: {
|
||||
value: string;
|
||||
type?: TagProps['type'];
|
||||
color?: color;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,54 +19,31 @@ const DeploymentDialogBodyCard = ({
|
||||
chip,
|
||||
deployment,
|
||||
}: DeploymentDialogBodyCardProps) => {
|
||||
const commit =
|
||||
deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH) +
|
||||
' ' +
|
||||
deployment.commitMessage;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 px-4 py-4 rounded-xl bg-base-bg-emphasized text-elements-low-em">
|
||||
<Card className="p-2 shadow-none" placeholder={''}>
|
||||
{chip && (
|
||||
<Tag className="w-fit" size="xs" type={chip.type}>
|
||||
{chip.value}
|
||||
</Tag>
|
||||
)}
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Title */}
|
||||
<p className="text-sm font-medium text-elements-high-em tracking-[0.006em]">
|
||||
{deployment.url}
|
||||
</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}
|
||||
<Chip
|
||||
className={`w-fit normal-case font-normal`}
|
||||
size="sm"
|
||||
value={chip.value}
|
||||
color={chip.color}
|
||||
/>
|
||||
<p className="text-sm tracking-[0.006em]">
|
||||
{deployment.createdBy.name ?? 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{deployment.url && (
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
GlobeIcon,
|
||||
HorizontalDotIcon,
|
||||
LinkIcon,
|
||||
RefreshIcon,
|
||||
RocketIcon,
|
||||
UndoIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import { ComponentPropsWithRef } from 'react';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
||||
|
||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||
deployment: Deployment;
|
||||
currentDeployment: Deployment;
|
||||
onUpdate: () => Promise<void>;
|
||||
project: Project;
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
export const DeploymentMenu = ({
|
||||
deployment,
|
||||
currentDeployment,
|
||||
onUpdate,
|
||||
project,
|
||||
prodBranchDomains,
|
||||
className,
|
||||
...props
|
||||
}: DeploymentMenuProps) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||
|
||||
const updateDeployment = async () => {
|
||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||
if (isUpdated) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment changed to production');
|
||||
} else {
|
||||
toast.error('Unable to change deployment to production');
|
||||
}
|
||||
};
|
||||
|
||||
const redeployToProd = async () => {
|
||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||
if (isRedeployed) {
|
||||
await onUpdate();
|
||||
toast.success('Redeployed to production');
|
||||
} else {
|
||||
toast.error('Unable to redeploy to production');
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackDeploymentHandler = async () => {
|
||||
const isRollbacked = await client.rollbackDeployment(
|
||||
project.id,
|
||||
deployment.id,
|
||||
);
|
||||
if (isRollbacked) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment rolled back');
|
||||
} else {
|
||||
toast.error('Unable to rollback deployment');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('max-w-[32px]', className)} {...props}>
|
||||
<Menu placement="bottom-start">
|
||||
<MenuHandler>
|
||||
<Button
|
||||
shape="default"
|
||||
size="xs"
|
||||
variant="unstyled"
|
||||
className={cn(
|
||||
'h-8 w-8 rounded-full border border-transparent transition-colors background-transparent',
|
||||
'[&[aria-expanded=true]]:border [&[aria-expanded=true]]:border-border-interactive [&[aria-expanded=true]]:bg-controls-tertiary [&[aria-expanded=true]]:shadow-button',
|
||||
)}
|
||||
leftIcon={<HorizontalDotIcon />}
|
||||
aria-label="Toggle Menu"
|
||||
/>
|
||||
</MenuHandler>
|
||||
<MenuList className="text-elements-high-em" placeholder={''}>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized"
|
||||
disabled={!Boolean(deployment.url)}
|
||||
placeholder={''}
|
||||
>
|
||||
<a
|
||||
className="flex items-center gap-3"
|
||||
href={deployment.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<LinkIcon /> Visit
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
placeholder={''}
|
||||
>
|
||||
<GlobeIcon /> Assign domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||
disabled={!(deployment.environment !== Environment.Production)}
|
||||
placeholder={''}
|
||||
>
|
||||
<RocketIcon /> Change to production
|
||||
</MenuItem>
|
||||
<hr className="my-3" />
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setRedeployToProduction(!redeployToProduction)}
|
||||
disabled={
|
||||
!(
|
||||
deployment.environment === Environment.Production &&
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<RefreshIcon /> Redeploy to production
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setRollbackDeployment(!rollbackDeployment)}
|
||||
disabled={
|
||||
deployment.isCurrent ||
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<UndoIcon /> Rollback to this version
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
{/* Dialogs */}
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Change to production?"
|
||||
confirmButtonTitle="Change"
|
||||
handleCancel={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
handleConfirm={async () => {
|
||||
await updateDeployment();
|
||||
setChangeToProduction((preVal) => !preVal);
|
||||
}}
|
||||
deployment={deployment}
|
||||
domains={prodBranchDomains}
|
||||
/>
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Redeploy to production?"
|
||||
handleCancel={() => setRedeployToProduction((preVal) => !preVal)}
|
||||
open={redeployToProduction}
|
||||
confirmButtonTitle="Redeploy"
|
||||
handleConfirm={async () => {
|
||||
await redeployToProd();
|
||||
setRedeployToProduction((preVal) => !preVal);
|
||||
}}
|
||||
deployment={deployment}
|
||||
domains={deployment.domain ? [deployment.domain] : []}
|
||||
/>
|
||||
{Boolean(currentDeployment) && (
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Rollback to this deployment?"
|
||||
handleCancel={() => setRollbackDeployment((preVal) => !preVal)}
|
||||
open={rollbackDeployment}
|
||||
confirmButtonTitle="Rollback"
|
||||
handleConfirm={async () => {
|
||||
await rollbackDeploymentHandler();
|
||||
setRollbackDeployment((preVal) => !preVal);
|
||||
}}
|
||||
deployment={currentDeployment}
|
||||
newDeployment={deployment}
|
||||
domains={currentDeployment.domain ? [currentDeployment.domain] : []}
|
||||
/>
|
||||
)}
|
||||
<AssignDomainDialog
|
||||
open={assignDomainDialog}
|
||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
|
||||
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';
|
||||
import { IconButton, Option, Select } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../../SearchBar';
|
||||
import DatePicker from '../../../DatePicker';
|
||||
|
||||
export enum StatusOptions {
|
||||
ALL_STATUS = 'All status',
|
||||
@@ -22,8 +15,8 @@ export enum StatusOptions {
|
||||
|
||||
export interface FilterValue {
|
||||
searchedBranch: string;
|
||||
status: StatusOptions | string;
|
||||
updateAtRange?: Value;
|
||||
status: StatusOptions;
|
||||
updateAtRange?: DateRange;
|
||||
}
|
||||
|
||||
interface FilterFormProps {
|
||||
@@ -34,7 +27,7 @@ interface FilterFormProps {
|
||||
const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch);
|
||||
const [selectedStatus, setSelectedStatus] = useState(value.status);
|
||||
const [dateRange, setDateRange] = useState<Value>();
|
||||
const [dateRange, setDateRange] = useState<DateRange>();
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
@@ -50,68 +43,46 @@ 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="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-3">
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Input
|
||||
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
|
||||
<div className="col-span-4">
|
||||
<SearchBar
|
||||
placeholder="Search branches"
|
||||
leftIcon={<SearchIcon />}
|
||||
rightIcon={
|
||||
searchedBranch && <CrossCircleIcon onClick={handleReset} />
|
||||
}
|
||||
value={searchedBranch}
|
||||
onChange={(e) => setSearchedBranch(e.target.value)}
|
||||
onChange={(event) => setSearchedBranch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
selectRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
onReset={() => setDateRange(undefined)}
|
||||
/>
|
||||
<div className="col-span-2">
|
||||
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<div className="col-span-2 relative">
|
||||
<Select
|
||||
leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
|
||||
options={statusOptions}
|
||||
clearable
|
||||
value={selectedStatus}
|
||||
onChange={(value) => setSelectedStatus(value as StatusOptions)}
|
||||
placeholder="All status"
|
||||
value={
|
||||
selectedStatus
|
||||
? { label: selectedStatus, value: selectedStatus }
|
||||
: undefined
|
||||
}
|
||||
onChange={(item) =>
|
||||
setSelectedStatus((item as SelectOption).value as StatusOptions)
|
||||
}
|
||||
onClear={() => setSelectedStatus('')}
|
||||
/>
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
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-5 md:col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<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">
|
||||
{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>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GitCommitWithBranch } from '../../../../../types';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowRightCircleFilledIcon,
|
||||
BranchStrokeIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
|
||||
interface ActivityCardProps {
|
||||
activity: GitCommitWithBranch;
|
||||
}
|
||||
|
||||
const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
const formattedDate = formatDistance(
|
||||
new Date(activity.commit.author!.date!),
|
||||
new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
to={activity.html_url}
|
||||
target="_blank"
|
||||
className="p-3 gap-2 focus-within:ring-2 focus-within:ring-controls-primary/40 focus:outline-none rounded-xl transition-colors hover:bg-base-bg-alternate flex group"
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
type="orange"
|
||||
size={36}
|
||||
initials={getInitials(activity.commit.author?.name ?? '')}
|
||||
imageSrc={activity.author?.avatar_url}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||
{activity.commit.author?.name}
|
||||
</span>
|
||||
<span className="text-elements-low-em text-xs flex items-center gap-2">
|
||||
<span title={formattedDate} className="whitespace-nowrap">
|
||||
{formattedDate}
|
||||
</span>
|
||||
{/* Dot */}
|
||||
<span className="w-0.5 h-0.5 rounded-full bg-border-interactive-hovered"></span>
|
||||
<span className="flex justify-center items-center gap-1.5">
|
||||
<div>
|
||||
<BranchStrokeIcon className="w-3 h-3" />
|
||||
</div>
|
||||
<span
|
||||
title={activity.branch.name}
|
||||
// pseudo to increase hover area
|
||||
className="before:absolute relative before:h-5 before:-top-4 before:inset-x-0"
|
||||
>
|
||||
<span className="line-clamp-1">{activity.branch.name}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<p
|
||||
title={activity.commit.message}
|
||||
className="text-sm line-clamp-4 tracking-tight text-elements-mid-em mt-2"
|
||||
>
|
||||
{activity.commit.message}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Go to commit"
|
||||
variant="unstyled"
|
||||
tabIndex={-1}
|
||||
className="p-0 text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 opacity-0 transition-all"
|
||||
leftIcon={<ArrowRightCircleFilledIcon className="w-5 h-5" />}
|
||||
/>
|
||||
</Link>
|
||||
{/* Separator calc => 100% - 36px (avatar) - 12px (padding-left) - 8px (gap)
|
||||
*/}
|
||||
<div className="pt-2 mb-2 ml-auto h-1 w-[calc(100%-36px-12px-8px)] border-b border-border-separator last:border-b-transparent"></div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityCard;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './Activity';
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { cloneElement } from 'utils/cloneElement';
|
||||
|
||||
interface OverviewInfoProps {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export const OverviewInfo = ({
|
||||
label,
|
||||
icon,
|
||||
children,
|
||||
}: PropsWithChildren<OverviewInfoProps>) => {
|
||||
const styledIcon = cloneElement({
|
||||
element: icon,
|
||||
className: 'w-4 h-4',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-2 py-3 text-sm items-center">
|
||||
<div className="flex gap-2 items-center text-elements-high-em">
|
||||
{styledIcon}
|
||||
{label}
|
||||
</div>
|
||||
<div>{children}</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 { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
|
||||
enum RefreshStatus {
|
||||
IDLE,
|
||||
@@ -118,16 +118,28 @@ const DomainCard = ({
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<DeleteDomainDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete domain?"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Delete domain"
|
||||
handleConfirm={() => {
|
||||
deleteDomain();
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
projectName={project.name}
|
||||
domainName={domain.name}
|
||||
/>
|
||||
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>
|
||||
</div>
|
||||
|
||||
<Typography variant="small" placeholder={''}>
|
||||
|
||||
+14
-6
@@ -5,8 +5,8 @@ import { EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import { IconButton, Input, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
|
||||
const ShowPasswordIcon = ({
|
||||
handler,
|
||||
@@ -161,12 +161,20 @@ const EditEnvironmentVariableRow = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DeleteVariableDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete variable"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Confirm delete"
|
||||
handleConfirm={removeEnvironmentVariableHandler}
|
||||
variableKey={variable.key}
|
||||
/>
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,14 +3,15 @@ import { Permission, User } from 'gql-client';
|
||||
|
||||
import {
|
||||
Select,
|
||||
Typography,
|
||||
Option,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
|
||||
const PERMISSION_OPTIONS = [
|
||||
{
|
||||
@@ -140,19 +141,25 @@ const MemberCard = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RemoveMemberDialog
|
||||
handleCancel={() => setRemoveMemberDialogOpen((preVal) => !preVal)}
|
||||
<ConfirmDialog
|
||||
dialogTitle="Remove member?"
|
||||
handleOpen={() => setRemoveMemberDialogOpen((preVal) => !preVal)}
|
||||
open={removeMemberDialogOpen}
|
||||
confirmButtonTitle="Yes, Remove member"
|
||||
handleConfirm={() => {
|
||||
setRemoveMemberDialogOpen((preVal) => !preVal);
|
||||
if (onRemoveProjectMember) {
|
||||
onRemoveProjectMember();
|
||||
}
|
||||
}}
|
||||
memberName={member.name ?? ''}
|
||||
ethAddress={ethAddress}
|
||||
emailDomain={emailDomain}
|
||||
/>
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once removed, {formatAddress(member.name ?? '')} (
|
||||
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
||||
this project.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+13
-5
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { GitRepositoryDetails } from 'types';
|
||||
import { DisconnectRepositoryDialog } from 'components/projects/Dialog/DisconnectRepositoryDialog';
|
||||
import { GitRepositoryDetails } from '../../../../types';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
|
||||
const RepoConnectedSection = ({
|
||||
linkedRepo,
|
||||
@@ -34,13 +34,21 @@ const RepoConnectedSection = ({
|
||||
^ Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
<DisconnectRepositoryDialog
|
||||
handleCancel={() => setDisconnectRepoDialogOpen((preVal) => !preVal)}
|
||||
<ConfirmDialog
|
||||
dialogTitle="Disconnect repository?"
|
||||
handleOpen={() => 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 } from '@material-tailwind/react';
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
|
||||
interface WebhookCardProps {
|
||||
webhookUrl: string;
|
||||
@@ -15,6 +15,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
return (
|
||||
<div className="flex justify-between w-full mb-3">
|
||||
{webhookUrl}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -37,15 +38,23 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
<DeleteWebhookDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete webhook"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Confirm delete"
|
||||
handleConfirm={() => {
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
onDelete();
|
||||
}}
|
||||
webhookUrl={webhookUrl}
|
||||
/>
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const avatarTheme = tv(
|
||||
{
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden', 'shrink-0'],
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||
slots: {
|
||||
image: [
|
||||
'h-full',
|
||||
|
||||
@@ -14,13 +14,11 @@ export const buttonTheme = tv(
|
||||
'whitespace-nowrap',
|
||||
'focus-ring',
|
||||
'disabled:cursor-not-allowed',
|
||||
'transition-colors',
|
||||
'duration-150',
|
||||
],
|
||||
variants: {
|
||||
size: {
|
||||
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
|
||||
md: ['gap-2', 'py-3', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
md: ['gap-2', 'py-3.25', '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'],
|
||||
},
|
||||
@@ -63,13 +61,14 @@ export const buttonTheme = tv(
|
||||
tertiary: [
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-border-interactive',
|
||||
'border-border-interactive/10',
|
||||
'bg-controls-tertiary',
|
||||
'shadow-button',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
@@ -82,8 +81,10 @@ export const buttonTheme = tv(
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
@@ -108,8 +109,10 @@ export const buttonTheme = tv(
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
@@ -150,7 +153,7 @@ export const buttonTheme = tv(
|
||||
{
|
||||
size: 'md',
|
||||
iconOnly: true,
|
||||
class: ['py-3', 'px-3'],
|
||||
class: ['py-3.25', 'px-3.25'],
|
||||
},
|
||||
{
|
||||
size: 'sm',
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
|
||||
import './Calendar.css';
|
||||
import { format } from 'date-fns';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
export type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
@@ -64,11 +63,6 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +80,6 @@ export const Calendar = ({
|
||||
actions,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onReset,
|
||||
onChange: onChangeProp,
|
||||
...props
|
||||
}: CalendarProps): JSX.Element => {
|
||||
@@ -224,11 +217,6 @@ export const Calendar = ({
|
||||
[setValue, setActiveDate, changeNavigationLabel, selectRange],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setValue(null);
|
||||
onReset?.();
|
||||
}, [setValue, onReset]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
@@ -288,30 +276,21 @@ export const Calendar = ({
|
||||
{/* Footer or CTA */}
|
||||
<div
|
||||
{...footerProps}
|
||||
className={cn(footer({ className: footerProps?.className }), {
|
||||
'justify-between': Boolean(value),
|
||||
})}
|
||||
className={footer({ className: footerProps?.className })}
|
||||
>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<>
|
||||
{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>
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!value}
|
||||
onClick={() => (value ? onSelect?.(value) : null)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,64 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Modal, ModalProps } from './Modal';
|
||||
import { Button, ButtonOrLinkProps } from './Button';
|
||||
import React from 'react';
|
||||
|
||||
export type ConfirmDialogProps = ModalProps & {
|
||||
children?: ReactNode;
|
||||
dialogTitle?: string;
|
||||
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;
|
||||
open: boolean;
|
||||
handleCancel: () => void;
|
||||
confirmButtonTitle?: string;
|
||||
handleOpen: () => void;
|
||||
confirmButtonTitle: string;
|
||||
handleConfirm?: () => void;
|
||||
cancelButtonProps?: ButtonOrLinkProps;
|
||||
confirmButtonProps?: ButtonOrLinkProps;
|
||||
color: color;
|
||||
};
|
||||
|
||||
const ConfirmDialog = ({
|
||||
children,
|
||||
dialogTitle,
|
||||
handleCancel,
|
||||
open,
|
||||
handleOpen,
|
||||
confirmButtonTitle,
|
||||
handleConfirm,
|
||||
cancelButtonProps,
|
||||
confirmButtonProps,
|
||||
...props
|
||||
}: ConfirmDialogProps) => {
|
||||
// Close the dialog when the user clicks outside of it
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) return handleCancel?.();
|
||||
};
|
||||
|
||||
color,
|
||||
}: ConfirmDialogProp) => {
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ArrowRightCircleFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
{...props}
|
||||
fill="none"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
width="32"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M29.3334 16C29.3334 8.63619 23.3638 2.66666 16 2.66666C8.63622 2.66666 2.66669 8.63619 2.66669 16C2.66669 23.3638 8.63622 29.3333 16 29.3333C23.3638 29.3333 29.3334 23.3638 29.3334 16ZM18.1144 17.3333L16.3905 19.0572C15.8698 19.5779 15.8698 20.4221 16.3905 20.9428C16.9112 21.4635 17.7555 21.4635 18.2762 20.9428L21.3334 17.8856C22.3748 16.8442 22.3748 15.1558 21.3334 14.1144L18.2762 11.0572C17.7555 10.5365 16.9112 10.5365 16.3905 11.0572C15.8698 11.5779 15.8698 12.4221 16.3905 12.9428L18.1144 14.6667H10.6667C9.93031 14.6667 9.33335 15.2636 9.33335 16C9.33335 16.7364 9.93031 17.3333 10.6667 17.3333H18.1144Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ArrowRightCircleIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="17"
|
||||
height="16"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.3333 8C15.3333 4.3181 12.3486 1.33333 8.66667 1.33333C4.98477 1.33333 2 4.3181 2 8C2 11.6819 4.98477 14.6667 8.66667 14.6667C12.3486 14.6667 15.3333 11.6819 15.3333 8ZM10.2929 8.5L8.97978 9.81311C8.78452 10.0084 8.78452 10.325 8.97978 10.5202C9.17504 10.7155 9.49162 10.7155 9.68689 10.5202L11.3821 8.82495C11.8378 8.36934 11.8378 7.63065 11.3821 7.17504L9.68689 5.47977C9.49162 5.28451 9.17504 5.28451 8.97978 5.47977C8.78452 5.67504 8.78452 5.99162 8.97978 6.18688L10.2929 7.5H5.83333C5.55719 7.5 5.33333 7.72385 5.33333 8C5.33333 8.27614 5.55719 8.5 5.83333 8.5H10.2929Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const BranchStrokeIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.66667 4.99984C5.58714 4.99984 6.33333 4.25365 6.33333 3.33317C6.33333 2.4127 5.58714 1.6665 4.66667 1.6665C3.74619 1.6665 3 2.4127 3 3.33317C3 4.25365 3.74619 4.99984 4.66667 4.99984ZM4.66667 4.99984V10.9998M4.66667 10.9998C3.74619 10.9998 3 11.746 3 12.6665C3 13.587 3.74619 14.3332 4.66667 14.3332C5.58714 14.3332 6.33333 13.587 6.33333 12.6665C6.33333 11.746 5.58714 10.9998 4.66667 10.9998ZM4.66667 10.9998V9.33317C4.66667 8.59679 5.26362 7.99984 6 7.99984H10C10.7364 7.99984 11.3333 7.40288 11.3333 6.6665V4.99984M11.3333 4.99984C12.2538 4.99984 13 4.25365 13 3.33317C13 2.4127 12.2538 1.6665 11.3333 1.6665C10.4129 1.6665 9.66667 2.4127 9.66667 3.33317C9.66667 4.25365 10.4129 4.99984 11.3333 4.99984Z"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarDaysIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2.3335 5.00016H13.6668M4.33349 13.6668H11.6668C12.7714 13.6668 13.6668 12.7714 13.6668 11.6668V4.3335C13.6668 3.22893 12.7714 2.3335 11.6668 2.3335H4.3335C3.22893 2.3335 2.3335 3.22893 2.3335 4.33349V11.6668C2.3335 12.7714 3.22893 13.6668 4.33349 13.6668Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.6665 7.8335C4.6665 7.55735 4.89036 7.3335 5.1665 7.3335C5.44265 7.3335 5.6665 7.55735 5.6665 7.8335C5.6665 8.10964 5.44265 8.3335 5.1665 8.3335C4.89036 8.3335 4.6665 8.10964 4.6665 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.6665 10.8335C4.6665 10.5574 4.89036 10.3335 5.1665 10.3335C5.44265 10.3335 5.6665 10.5574 5.6665 10.8335C5.6665 11.1096 5.44265 11.3335 5.1665 11.3335C4.89036 11.3335 4.6665 11.1096 4.6665 10.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.49984 10.8335C7.49984 10.5574 7.72369 10.3335 7.99984 10.3335C8.27598 10.3335 8.49984 10.5574 8.49984 10.8335C8.49984 11.1096 8.27598 11.3335 7.99984 11.3335C7.72369 11.3335 7.49984 11.1096 7.49984 10.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.49984 7.8335C7.49984 7.55735 7.72369 7.3335 7.99984 7.3335C8.27598 7.3335 8.49984 7.55735 8.49984 7.8335C8.49984 8.10964 8.27598 8.3335 7.99984 8.3335C7.72369 8.3335 7.49984 8.10964 7.49984 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10.3332 7.8335C10.3332 7.55735 10.557 7.3335 10.8332 7.3335C11.1093 7.3335 11.3332 7.55735 11.3332 7.8335C11.3332 8.10964 11.1093 8.3335 10.8332 8.3335C10.557 8.3335 10.3332 8.10964 10.3332 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -5,16 +5,16 @@ export const CalendarIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const 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,11 +3,17 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon width="20" height="20" viewBox="0 0 20 20" {...props}>
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
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 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"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user