Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcaeabf07d | ||
|
|
1e056d7969 | ||
|
|
6d8723881c | ||
|
|
d387229e80 | ||
|
|
54c2e8ec0b | ||
|
|
410def0750 | ||
|
|
d181e6ba98 | ||
|
|
100e03b7a4 | ||
|
|
5d0dd93271 | ||
|
|
3bca06b590 | ||
|
|
c950bd8d93 | ||
|
|
b7fe301c81 | ||
|
|
3cf8769259 | ||
|
|
35bc69bb15 | ||
|
|
5451c3be6f | ||
|
|
5f8afab020 | ||
|
|
0bcb7ca4d6 | ||
|
|
cfeb85f5c4 | ||
|
|
705516f5e3 | ||
|
|
effa0bbf14 | ||
|
|
10a875d6e8 | ||
|
|
284eb1403a | ||
|
|
538e89e3c9 | ||
|
|
b88752da69 | ||
|
|
48c569c371 | ||
|
|
549dc5af10 | ||
|
|
012beaad57 | ||
|
|
81f27442fb | ||
|
|
bdb948f69e | ||
|
|
2442e92395 | ||
|
|
dcf271a01b | ||
|
|
48fd953c60 | ||
|
|
5eff273abb | ||
|
|
f9af066333 | ||
|
|
04460a24ac | ||
|
|
ccc4589033 | ||
|
|
cf2f9a88e1 | ||
|
|
50749055bf | ||
|
|
ef7c673698 | ||
|
|
7f11d8818d | ||
|
|
ece9a2ecec | ||
|
|
cb614c8d8f | ||
|
|
dbb928cd2f | ||
|
|
cfd129fb96 | ||
|
|
efc10d3d7e | ||
|
|
042aff2f87 | ||
|
|
b48afed24f | ||
|
|
0dfecd024d | ||
|
|
6126c03eee | ||
|
|
35be62b28e | ||
|
|
9db87e6be3 | ||
|
|
353fd0f621 | ||
|
|
5a5756bcf2 | ||
|
|
22233d95f8 | ||
|
|
12fba5ee0c | ||
|
|
9b6c777f5f | ||
|
|
3718e260f5 | ||
|
|
05e1ac6b02 | ||
|
|
55848b1dd1 | ||
|
|
42bf1534fb | ||
|
|
9acb9daacc | ||
|
|
f7c45ca045 | ||
|
|
ef0eac8293 | ||
|
|
7d1810ebd9 | ||
|
|
0e7343da6f | ||
|
|
8952393b31 | ||
|
|
32918b7930 | ||
|
|
596889b5e2 | ||
|
|
fb932eeb04 | ||
|
|
200ea3ca7b | ||
|
|
ba87c6f22a | ||
|
|
4d646e1ac0 | ||
|
|
5be29a9745 | ||
|
|
fffc370d40 | ||
|
|
1cd60d84dd | ||
|
|
a846531e43 | ||
|
|
c8a153ad27 | ||
|
|
bb7a88f7e9 | ||
|
|
675112079c | ||
|
|
cb928c3360 | ||
|
|
5836779403 | ||
|
|
028dd35db1 | ||
|
|
ef9f4df28a | ||
|
|
6b17dce2ae | ||
|
|
267b52a352 | ||
|
|
fa96b6e734 | ||
|
|
22c581dd33 | ||
|
|
d3013719e6 | ||
|
|
98a4d7be07 | ||
|
|
3928e2610f | ||
|
|
075cc05db5 |
@@ -1 +1,7 @@
|
||||
node_modules/
|
||||
yarn-error.log
|
||||
.yarnrc.yml
|
||||
.yarn/
|
||||
.yarnrc
|
||||
|
||||
packages/backend/environments/local.toml
|
||||
@@ -1,8 +1,8 @@
|
||||
# snowballtools
|
||||
# snowballtools-base
|
||||
|
||||
## Setup
|
||||
|
||||
- Clone the `snowballtools` repo
|
||||
- Clone the `snowballtools-base` repo
|
||||
|
||||
```bash
|
||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
||||
@@ -28,63 +28,144 @@
|
||||
cd packages/backend
|
||||
```
|
||||
|
||||
- Load fixtures in database
|
||||
- Rename backend config file from [environments/local.toml.example](packages/backend/environments/local.toml.example) to `local.toml`
|
||||
|
||||
```bash
|
||||
yarn test:db:load:fixtures
|
||||
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 Github OAuth app
|
||||
- https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
|
||||
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
- In "Homepage URL", type `http://localhost:3000`
|
||||
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
||||
- Generate a new client secret after app is created
|
||||
|
||||
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
||||
### Backend Production
|
||||
|
||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
||||
- Let us assume the following domains for backend and frontend
|
||||
- Backend server: `api.snowballtools.com`
|
||||
- Frontend app: `dashboard.snowballtools.com`
|
||||
|
||||
```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
|
||||
- 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 = ""
|
||||
```
|
||||
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
```bash
|
||||
# For registryConfig.restEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
||||
# 0.0.0.0:32777
|
||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
# For registryConfig.gqlEndpoint
|
||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
||||
# 0.0.0.0:32771
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://api.snowballtools.com"
|
||||
...
|
||||
```
|
||||
|
||||
- Run the script to create bond, reserve the authority and set authority bond
|
||||
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
||||
|
||||
```bash
|
||||
yarn test:registry:init
|
||||
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
||||
```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"
|
||||
...
|
||||
```
|
||||
|
||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
||||
- 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
|
||||
- https://ngrok.com/docs/getting-started/
|
||||
- [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]
|
||||
@@ -95,7 +176,7 @@
|
||||
- Start the server in `packages/backend`
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
yarn start:dev
|
||||
```
|
||||
|
||||
## Frontend
|
||||
@@ -106,10 +187,10 @@
|
||||
cd packages/frontend
|
||||
```
|
||||
|
||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
||||
- Rename [.env.example](packages/frontend/.env.example) to `.env`
|
||||
|
||||
```env
|
||||
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
||||
```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
|
||||
@@ -118,23 +199,36 @@
|
||||
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||
```
|
||||
|
||||
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
REACT_APP_GITHUB_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_PWA_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = cerc-io/image-upload-pwa-example
|
||||
```
|
||||
|
||||
### Development
|
||||
### Frontend Production
|
||||
|
||||
- Start the React application
|
||||
- Let us assume the following domains for backend and frontend
|
||||
- Backend server: `api.snowballtools.com`
|
||||
- Frontend app: `dashboard.snowballtools.com`
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
- Set the following values in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
# Backend server endpoint
|
||||
REACT_APP_SERVER_URL = 'https://api.snowballtools.com'
|
||||
```
|
||||
|
||||
- The React application will be running in `http://localhost:3000/`
|
||||
- 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
|
||||
|
||||
### Production
|
||||
```env
|
||||
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
|
||||
```
|
||||
|
||||
- Build the React application
|
||||
|
||||
@@ -148,3 +242,28 @@
|
||||
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,10 +9,20 @@ 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_GQL_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_gql_url'
|
||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
|
||||
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
|
||||
EOF
|
||||
|
||||
yarn || exit 1
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
db
|
||||
dist
|
||||
environments/local.toml
|
||||
@@ -2,6 +2,11 @@
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
trustProxy = false
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
dbPath = "db/snowball"
|
||||
@@ -19,7 +24,11 @@
|
||||
chainId = "laconic_9000-1"
|
||||
privateKey = ""
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
amount = "200000"
|
||||
denom = "aphoton"
|
||||
gas = "550000"
|
||||
gas = "750000"
|
||||
|
||||
[misc]
|
||||
projectDomain = "apps.snowballtools.com"
|
||||
@@ -12,8 +12,10 @@
|
||||
"@types/node": "^20.11.0",
|
||||
"apollo-server-core": "^3.13.0",
|
||||
"apollo-server-express": "^3.13.0",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.18.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"graphql": "^16.8.1",
|
||||
"luxon": "^3.4.4",
|
||||
@@ -42,6 +44,7 @@
|
||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
export interface SessionConfig {
|
||||
secret: string;
|
||||
appOriginUrl: string;
|
||||
trustProxy: boolean;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
gqlPath?: string;
|
||||
sessionSecret: string;
|
||||
appOriginUrl: string;
|
||||
isProduction: boolean;
|
||||
session: SessionConfig;
|
||||
}
|
||||
|
||||
export interface DatabaseConfig {
|
||||
@@ -23,6 +34,7 @@ export interface RegistryConfig {
|
||||
privateKey: string;
|
||||
bondId: string;
|
||||
fetchDeploymentRecordDelay: number;
|
||||
authority: string;
|
||||
fee: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
@@ -30,9 +42,14 @@ export interface RegistryConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export interface MiscConfig {
|
||||
projectDomain: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
server: ServerConfig;
|
||||
database: DatabaseConfig;
|
||||
gitHub: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
misc: MiscConfig;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,3 @@ export const DEFAULT_CONFIG_FILE_PATH =
|
||||
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||
|
||||
export const DEFAULT_GQL_PATH = '/graphql';
|
||||
|
||||
// Note: temporary hardcoded user, later to be derived from auth token
|
||||
export const USER_ID =
|
||||
process.env.SNOWBALL_BACKEND_USER_ID ||
|
||||
'59f4355d-9549-4aac-9b54-eeefceeabef0';
|
||||
|
||||
export const PROJECT_DOMAIN =
|
||||
process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz';
|
||||
|
||||
@@ -11,7 +11,7 @@ import assert from 'assert';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||
|
||||
import { DatabaseConfig } from './config';
|
||||
import { DatabaseConfig, MiscConfig } from './config';
|
||||
import { User } from './entity/User';
|
||||
import { Organization } from './entity/Organization';
|
||||
import { Project } from './entity/Project';
|
||||
@@ -19,13 +19,10 @@ import { Deployment } from './entity/Deployment';
|
||||
import { ProjectMember } from './entity/ProjectMember';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { PROJECT_DOMAIN } from './constants';
|
||||
import { getEntities, loadAndSaveData } from './utils';
|
||||
import { UserOrganization } from './entity/UserOrganization';
|
||||
|
||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||
const USER_DATA_PATH = '../test/fixtures/users.json';
|
||||
const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json';
|
||||
|
||||
const log = debug('snowball:database');
|
||||
|
||||
@@ -34,8 +31,9 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
||||
// TODO: Fix order of methods
|
||||
export class Database {
|
||||
private dataSource: DataSource;
|
||||
private projectDomain: string;
|
||||
|
||||
constructor ({ dbPath }: DatabaseConfig) {
|
||||
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
||||
this.dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: dbPath,
|
||||
@@ -43,6 +41,8 @@ export class Database {
|
||||
synchronize: true,
|
||||
logging: false
|
||||
});
|
||||
|
||||
this.projectDomain = projectDomain;
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
@@ -51,36 +51,10 @@ export class Database {
|
||||
|
||||
const organizations = await this.getOrganizations({});
|
||||
|
||||
// Load an organization if none exist
|
||||
if (!organizations.length) {
|
||||
const orgEntities = await getEntities(
|
||||
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||
);
|
||||
const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [
|
||||
orgEntities[0]
|
||||
]);
|
||||
|
||||
// TODO: Remove user once authenticated
|
||||
const userEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_DATA_PATH)
|
||||
);
|
||||
const savedUsers = await loadAndSaveData(User, this.dataSource, [
|
||||
userEntities[0]
|
||||
]);
|
||||
|
||||
const userOrganizationRelations = {
|
||||
member: savedUsers,
|
||||
organization: savedOrgs
|
||||
};
|
||||
|
||||
const userOrgEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
UserOrganization,
|
||||
this.dataSource,
|
||||
[userOrgEntities[0]],
|
||||
userOrganizationRelations
|
||||
);
|
||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +72,9 @@ export class Database {
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
||||
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||
const userRepository = this.dataSource.getRepository(User);
|
||||
const updateResult = await userRepository.update({ id: userId }, data);
|
||||
const updateResult = await userRepository.update({ id: user.id }, data);
|
||||
assert(updateResult.affected);
|
||||
|
||||
return updateResult.affected > 0;
|
||||
@@ -140,6 +114,13 @@ export class Database {
|
||||
return userOrgs;
|
||||
}
|
||||
|
||||
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
||||
const newUserOrganization = await userOrganizationRepository.save(data);
|
||||
|
||||
return newUserOrganization;
|
||||
}
|
||||
|
||||
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const projects = await projectRepository.find(options);
|
||||
@@ -455,11 +436,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProject (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<Project> {
|
||||
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
// TODO: Check if organization exists
|
||||
@@ -469,15 +446,13 @@ export class Database {
|
||||
// TODO: Set icon according to framework
|
||||
newProject.icon = '';
|
||||
|
||||
newProject.owner = Object.assign(new User(), {
|
||||
id: userId
|
||||
});
|
||||
newProject.owner = user;
|
||||
|
||||
newProject.organization = Object.assign(new Organization(), {
|
||||
id: organizationId
|
||||
});
|
||||
|
||||
newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;
|
||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
||||
|
||||
return projectRepository.save(newProject);
|
||||
}
|
||||
|
||||
@@ -87,11 +87,11 @@ export class Deployment {
|
||||
@Column('simple-json')
|
||||
applicationRecordData!: ApplicationRecord;
|
||||
|
||||
@Column('varchar')
|
||||
applicationDeploymentRequestId!: string;
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRequestId!: string | null;
|
||||
|
||||
@Column('simple-json')
|
||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRecordId!: string | null;
|
||||
|
||||
@@ -12,10 +12,15 @@ import { UserOrganization } from './UserOrganization';
|
||||
|
||||
@Entity()
|
||||
@Unique(['email'])
|
||||
@Unique(['ethAddress'])
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
// TODO: Set ethAddress as ID
|
||||
@Column()
|
||||
ethAddress!: string;
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { User } from './User';
|
||||
import { Organization } from './Organization';
|
||||
|
||||
enum Role {
|
||||
export enum Role {
|
||||
Owner = 'Owner',
|
||||
Maintainer = 'Maintainer',
|
||||
Reader = 'Reader',
|
||||
|
||||
@@ -19,9 +19,7 @@ const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||
|
||||
export const main = async (): Promise<void> => {
|
||||
// TODO: get config path using cli
|
||||
const { server, database, gitHub, registryConfig } = await getConfig<Config>(
|
||||
DEFAULT_CONFIG_FILE_PATH
|
||||
);
|
||||
const { server, database, gitHub, registryConfig, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
const app = new OAuthApp({
|
||||
clientType: OAUTH_CLIENT_TYPE,
|
||||
@@ -29,7 +27,7 @@ export const main = async (): Promise<void> => {
|
||||
clientSecret: gitHub.oAuth.clientSecret
|
||||
});
|
||||
|
||||
const db = new Database(database);
|
||||
const db = new Database(database, misc);
|
||||
await db.init();
|
||||
|
||||
const registry = new Registry(registryConfig);
|
||||
|
||||
@@ -108,7 +108,7 @@ export class Registry {
|
||||
log('Application record data:', applicationRecord);
|
||||
|
||||
// TODO: Discuss computation of CRN
|
||||
const crn = this.getCrn(packageJSON.name, appName);
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
|
||||
await this.registry.setName(
|
||||
@@ -137,16 +137,16 @@ export class Registry {
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
appName: string;
|
||||
packageJsonName: string;
|
||||
commitHash: string;
|
||||
repository: string;
|
||||
environmentVariables: { [key: string]: string };
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
packageJsonName: string,
|
||||
repository: string,
|
||||
environmentVariables: { [key: string]: string }
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
}> {
|
||||
const crn = this.getCrn(data.packageJsonName, data.appName);
|
||||
const crn = this.getCrn(data.appName);
|
||||
const records = await this.registry.resolveNames([crn]);
|
||||
const applicationRecord = records[0];
|
||||
|
||||
@@ -160,9 +160,9 @@ export class Registry {
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: `${data.deployment.project.name}-${data.deployment.id}`,
|
||||
|
||||
// TODO: Not set in test-progressive-web-app CI
|
||||
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||
|
||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||
@@ -174,7 +174,7 @@ export class Registry {
|
||||
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.commitHash
|
||||
repository_ref: data.deployment.commitHash
|
||||
})
|
||||
};
|
||||
|
||||
@@ -220,10 +220,8 @@ export class Registry {
|
||||
);
|
||||
}
|
||||
|
||||
getCrn (packageJsonName: string, appName: string): string {
|
||||
const [arg1] = packageJsonName.split('/');
|
||||
const authority = arg1.replace('@', '');
|
||||
|
||||
return `crn://${authority}/applications/${appName}`;
|
||||
getCrn (appName: string): string {
|
||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,26 +14,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
Query: {
|
||||
// TODO: add custom type for context
|
||||
user: (_: any, __: any, context: any) => {
|
||||
return service.getUser(context.userId);
|
||||
return context.user;
|
||||
},
|
||||
|
||||
organizations: async (_: any, __: any, context: any) => {
|
||||
return service.getOrganizationsByUserId(context.userId);
|
||||
organizations: async (_:any, __: any, context: any) => {
|
||||
return service.getOrganizationsByUserId(context.user);
|
||||
},
|
||||
|
||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getProjectById(projectId);
|
||||
},
|
||||
|
||||
projectsInOrganization: async (
|
||||
_: any,
|
||||
{ organizationSlug }: { organizationSlug: string },
|
||||
context: any
|
||||
) => {
|
||||
return service.getProjectsInOrganization(
|
||||
context.userId,
|
||||
organizationSlug
|
||||
);
|
||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
||||
return service.getProjectsInOrganization(context.user, organizationSlug);
|
||||
},
|
||||
|
||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||
@@ -51,12 +44,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getProjectMembersByProjectId(projectId);
|
||||
},
|
||||
|
||||
searchProjects: async (
|
||||
_: any,
|
||||
{ searchText }: { searchText: string },
|
||||
context: any
|
||||
) => {
|
||||
return service.searchProjects(context.userId, searchText);
|
||||
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
||||
return service.searchProjects(context.user, searchText);
|
||||
},
|
||||
|
||||
domains: async (
|
||||
@@ -78,10 +67,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.removeProjectMember(
|
||||
context.userId,
|
||||
projectMemberId
|
||||
);
|
||||
return await service.removeProjectMember(context.user, projectMemberId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -188,9 +174,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(
|
||||
await service.updateDeploymentToProd(context.userId, deploymentId)
|
||||
);
|
||||
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -206,11 +190,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.addProject(
|
||||
context.userId,
|
||||
organizationSlug,
|
||||
data
|
||||
);
|
||||
return await service.addProject(context.user, organizationSlug, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
throw err;
|
||||
@@ -235,9 +215,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(
|
||||
await service.redeployToProd(context.userId, deploymentId)
|
||||
);
|
||||
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -307,7 +285,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.authenticateGitHub(code, context.userId);
|
||||
return await service.authenticateGitHub(code, context.user);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@@ -316,9 +294,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
|
||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||
try {
|
||||
return service.unauthenticateGitHub(context.userId, {
|
||||
gitHubToken: null
|
||||
});
|
||||
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Router } from 'express';
|
||||
import { SiweMessage, generateNonce } from 'siwe';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/nonce', async (_, res) => {
|
||||
res.send(generateNonce());
|
||||
});
|
||||
|
||||
router.post('/validate', async (req, res) => {
|
||||
const { message, signature } = req.body;
|
||||
const { success, data } = await new SiweMessage(message).verify({
|
||||
signature
|
||||
});
|
||||
|
||||
if (success) {
|
||||
req.session.address = data.address;
|
||||
req.session.chainId = data.chainId;
|
||||
}
|
||||
|
||||
res.send({ success });
|
||||
});
|
||||
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.address && req.session.chainId) {
|
||||
res.send({ address: req.session.address, chainId: req.session.chainId });
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.send({ success: false });
|
||||
}
|
||||
res.send({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,22 +1,33 @@
|
||||
import debug from 'debug';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import { createServer } from 'http';
|
||||
import {
|
||||
ApolloServerPluginDrainHttpServer,
|
||||
ApolloServerPluginLandingPageLocalDefault
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
AuthenticationError
|
||||
} from 'apollo-server-core';
|
||||
import session from 'express-session';
|
||||
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
|
||||
import { ServerConfig } from './config';
|
||||
import { DEFAULT_GQL_PATH, USER_ID } from './constants';
|
||||
import { DEFAULT_GQL_PATH } from './constants';
|
||||
import githubRouter from './routes/github';
|
||||
import authRouter from './routes/auth';
|
||||
import { Service } from './service';
|
||||
|
||||
const log = debug('snowball:server');
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
address: string;
|
||||
chainId: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const createAndStartServer = async (
|
||||
serverConfig: ServerConfig,
|
||||
typeDefs: TypeSource,
|
||||
@@ -24,6 +35,7 @@ export const createAndStartServer = async (
|
||||
service: Service
|
||||
): Promise<ApolloServer> => {
|
||||
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
|
||||
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session;
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -39,9 +51,19 @@ export const createAndStartServer = async (
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
csrfPrevention: true,
|
||||
context: () => {
|
||||
// TODO: Use userId derived from auth token
|
||||
return { userId: USER_ID };
|
||||
context: async ({ req }) => {
|
||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||
|
||||
const { address } = req.session;
|
||||
|
||||
if (!address) {
|
||||
throw new AuthenticationError('Unauthorized: No active session');
|
||||
}
|
||||
|
||||
// Find/create user from ETH address in request session
|
||||
const user = await service.loadOrCreateUser(address);
|
||||
|
||||
return { user };
|
||||
},
|
||||
plugins: [
|
||||
// Proper shutdown for the HTTP server
|
||||
@@ -52,13 +74,49 @@ export const createAndStartServer = async (
|
||||
|
||||
await server.start();
|
||||
|
||||
app.use(cors({
|
||||
origin: appOriginUrl,
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
const sessionOptions: session.SessionOptions = {
|
||||
secret: secret,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||
// TODO: Set cookie maxAge and handle cookie expiry in frontend
|
||||
// maxAge: SESSION_COOKIE_MAX_AGE,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax'
|
||||
}
|
||||
};
|
||||
|
||||
if (domain) {
|
||||
sessionOptions.cookie!.domain = domain;
|
||||
}
|
||||
|
||||
if (trustProxy) {
|
||||
// trust first proxy
|
||||
app.set('trust proxy', 1);
|
||||
}
|
||||
|
||||
app.use(
|
||||
session(sessionOptions)
|
||||
);
|
||||
|
||||
server.applyMiddleware({
|
||||
app,
|
||||
path: gqlPath
|
||||
path: gqlPath,
|
||||
cors: {
|
||||
origin: [appOriginUrl],
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
|
||||
app.set('service', service);
|
||||
app.use(express.json());
|
||||
|
||||
app.set('service', service);
|
||||
app.use('/auth', authRouter);
|
||||
app.use('/api/github', githubRouter);
|
||||
|
||||
httpServer.listen(port, host, () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { User } from './entity/User';
|
||||
import { Registry } from './registry';
|
||||
import { GitHubConfig, RegistryConfig } from './config';
|
||||
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||
import { Role } from './entity/UserOrganization';
|
||||
|
||||
const log = debug('snowball:service');
|
||||
|
||||
@@ -160,6 +161,36 @@ export class Service {
|
||||
});
|
||||
}
|
||||
|
||||
async loadOrCreateUser (ethAddress: string): Promise<User> {
|
||||
// Get user by ETH address
|
||||
let user = await this.db.getUser({
|
||||
where: {
|
||||
ethAddress
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
const [org] = await this.db.getOrganizations({});
|
||||
assert(org, 'No organizations exists in database');
|
||||
|
||||
// Create user with new address
|
||||
user = await this.db.addUser({
|
||||
email: `${ethAddress}@example.com`,
|
||||
name: ethAddress,
|
||||
isVerified: true,
|
||||
ethAddress
|
||||
});
|
||||
|
||||
await this.db.addUserOrganization({
|
||||
member: user,
|
||||
organization: org,
|
||||
role: Role.Owner
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async getOctokit (userId: string): Promise<Octokit> {
|
||||
const user = await this.db.getUser({ where: { id: userId } });
|
||||
assert(
|
||||
@@ -170,8 +201,8 @@ export class Service {
|
||||
return new Octokit({ auth: user.gitHubToken });
|
||||
}
|
||||
|
||||
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
||||
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
|
||||
async getOrganizationsByUserId (user: User): Promise<Organization[]> {
|
||||
const dbOrganizations = await this.db.getOrganizationsByUserId(user.id);
|
||||
return dbOrganizations;
|
||||
}
|
||||
|
||||
@@ -180,14 +211,8 @@ export class Service {
|
||||
return dbProject;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization (
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
): Promise<Project[]> {
|
||||
const dbProjects = await this.db.getProjectsInOrganization(
|
||||
userId,
|
||||
organizationSlug
|
||||
);
|
||||
async getProjectsInOrganization (user: User, organizationSlug: string): Promise<Project[]> {
|
||||
const dbProjects = await this.db.getProjectsInOrganization(user.id, organizationSlug);
|
||||
return dbProjects;
|
||||
}
|
||||
|
||||
@@ -212,11 +237,8 @@ export class Service {
|
||||
return dbProjectMembers;
|
||||
}
|
||||
|
||||
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
|
||||
const dbProjects = await this.db.getProjectsBySearchText(
|
||||
userId,
|
||||
searchText
|
||||
);
|
||||
async searchProjects (user: User, searchText: string): Promise<Project[]> {
|
||||
const dbProjects = await this.db.getProjectsBySearchText(user.id, searchText);
|
||||
return dbProjects;
|
||||
}
|
||||
|
||||
@@ -269,20 +291,17 @@ export class Service {
|
||||
return newProjectMember;
|
||||
}
|
||||
|
||||
async removeProjectMember (
|
||||
userId: string,
|
||||
projectMemberId: string
|
||||
): Promise<boolean> {
|
||||
async removeProjectMember (user: User, projectMemberId: string): Promise<boolean> {
|
||||
const member = await this.db.getProjectMemberById(projectMemberId);
|
||||
|
||||
if (String(member.member.id) === userId) {
|
||||
if (String(member.member.id) === user.id) {
|
||||
throw new Error('Invalid operation: cannot remove self');
|
||||
}
|
||||
|
||||
const memberProject = member.project;
|
||||
assert(memberProject);
|
||||
|
||||
if (String(userId) === String(memberProject.owner.id)) {
|
||||
if (String(user.id) === String(memberProject.owner.id)) {
|
||||
return this.db.removeProjectMemberById(projectMemberId);
|
||||
} else {
|
||||
throw new Error('Invalid operation: not authorized');
|
||||
@@ -327,10 +346,7 @@ export class Service {
|
||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||
}
|
||||
|
||||
async updateDeploymentToProd (
|
||||
userId: string,
|
||||
deploymentId: string
|
||||
): Promise<Deployment> {
|
||||
async updateDeploymentToProd (user: User, deploymentId: string): Promise<Deployment> {
|
||||
const oldDeployment = await this.db.getDeployment({
|
||||
where: { id: deploymentId },
|
||||
relations: {
|
||||
@@ -347,16 +363,18 @@ export class Service {
|
||||
{ branch: oldDeployment.project.prodBranch }
|
||||
);
|
||||
|
||||
const octokit = await this.getOctokit(userId);
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
|
||||
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||
project: oldDeployment.project,
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: prodBranchDomains[0],
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage
|
||||
});
|
||||
const newDeployment = await this.createDeployment(user.id,
|
||||
octokit,
|
||||
{
|
||||
project: oldDeployment.project,
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: prodBranchDomains[0],
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage
|
||||
});
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
@@ -407,39 +425,15 @@ export class Service {
|
||||
repoUrl: recordData.repoUrl
|
||||
});
|
||||
|
||||
const environmentVariables =
|
||||
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
|
||||
environment: Environment.Production
|
||||
});
|
||||
|
||||
const environmentVariablesObj = environmentVariables.reduce(
|
||||
(acc, env) => {
|
||||
acc[env.key] = env.value;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string }
|
||||
);
|
||||
|
||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
||||
await this.registry.createApplicationDeploymentRequest({
|
||||
appName: repo,
|
||||
packageJsonName: packageJSON.name,
|
||||
commitHash: data.commitHash!,
|
||||
repository: recordData.repoUrl,
|
||||
environmentVariables: environmentVariablesObj
|
||||
});
|
||||
|
||||
// Update previous deployment with prod branch domain
|
||||
// TODO: Fix unique constraint error for domain
|
||||
await this.db.updateDeployment(
|
||||
{
|
||||
domainId: data.domain?.id
|
||||
},
|
||||
{
|
||||
if (data.domain) {
|
||||
await this.db.updateDeployment({
|
||||
domainId: data.domain.id
|
||||
}, {
|
||||
domain: null
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const newDeployment = await this.db.addDeployment({
|
||||
project: data.project,
|
||||
@@ -450,25 +444,37 @@ export class Service {
|
||||
status: DeploymentStatus.Building,
|
||||
applicationRecordId,
|
||||
applicationRecordData,
|
||||
applicationDeploymentRequestId,
|
||||
applicationDeploymentRequestData,
|
||||
domain: data.domain,
|
||||
createdBy: Object.assign(new User(), {
|
||||
id: userId
|
||||
})
|
||||
});
|
||||
|
||||
log(
|
||||
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`
|
||||
);
|
||||
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
|
||||
|
||||
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { environment: Environment.Production });
|
||||
|
||||
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
|
||||
acc[env.key] = env.value;
|
||||
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
|
||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
|
||||
{
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
packageJsonName: packageJSON.name,
|
||||
repository: recordData.repoUrl,
|
||||
environmentVariables: environmentVariablesObj
|
||||
});
|
||||
|
||||
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
|
||||
async addProject (
|
||||
userId: string,
|
||||
organizationSlug: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<Project | undefined> {
|
||||
async addProject (user: User, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
|
||||
const organization = await this.db.getOrganization({
|
||||
where: {
|
||||
slug: organizationSlug
|
||||
@@ -478,9 +484,9 @@ export class Service {
|
||||
throw new Error('Organization does not exist');
|
||||
}
|
||||
|
||||
const project = await this.db.addProject(userId, organization.id, data);
|
||||
const project = await this.db.addProject(user, organization.id, data);
|
||||
|
||||
const octokit = await this.getOctokit(userId);
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
const [owner, repo] = project.repository.split('/');
|
||||
|
||||
const {
|
||||
@@ -495,8 +501,7 @@ export class Service {
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||
|
||||
// Create deployment with prod branch and latest commit
|
||||
await this.createDeployment(
|
||||
userId,
|
||||
await this.createDeployment(user.id,
|
||||
octokit,
|
||||
{
|
||||
project,
|
||||
@@ -613,10 +618,7 @@ export class Service {
|
||||
return this.db.deleteDomainById(domainId);
|
||||
}
|
||||
|
||||
async redeployToProd (
|
||||
userId: string,
|
||||
deploymentId: string
|
||||
): Promise<Deployment> {
|
||||
async redeployToProd (user: User, deploymentId: string): Promise<Deployment> {
|
||||
const oldDeployment = await this.db.getDeployment({
|
||||
relations: {
|
||||
project: true,
|
||||
@@ -632,17 +634,19 @@ export class Service {
|
||||
throw new Error('Deployment not found');
|
||||
}
|
||||
|
||||
const octokit = await this.getOctokit(userId);
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
|
||||
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||
project: oldDeployment.project,
|
||||
// TODO: Put isCurrent field in project
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: oldDeployment.domain,
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage
|
||||
});
|
||||
const newDeployment = await this.createDeployment(user.id,
|
||||
octokit,
|
||||
{
|
||||
project: oldDeployment.project,
|
||||
// TODO: Put isCurrent field in project
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: oldDeployment.domain,
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage
|
||||
});
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
@@ -778,25 +782,17 @@ export class Service {
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
async authenticateGitHub (
|
||||
code: string,
|
||||
userId: string
|
||||
): Promise<{ token: string }> {
|
||||
const {
|
||||
authentication: { token }
|
||||
} = await this.oauthApp.createToken({
|
||||
async authenticateGitHub (code:string, user: User): Promise<{token: string}> {
|
||||
const { authentication: { token } } = await this.oauthApp.createToken({
|
||||
code
|
||||
});
|
||||
|
||||
await this.db.updateUser(userId, { gitHubToken: token });
|
||||
await this.db.updateUser(user, { gitHubToken: token });
|
||||
|
||||
return { token };
|
||||
}
|
||||
|
||||
async unauthenticateGitHub (
|
||||
userId: string,
|
||||
data: DeepPartial<User>
|
||||
): Promise<boolean> {
|
||||
return this.db.updateUser(userId, data);
|
||||
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||
return this.db.updateUser(user, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
[
|
||||
{
|
||||
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
||||
"name": "Snowball",
|
||||
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||
"email": "snowball@snowballtools.xyz",
|
||||
"isVerified": true
|
||||
"isVerified": true,
|
||||
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
},
|
||||
{
|
||||
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
||||
"name": "Alice Anderson",
|
||||
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
||||
"email": "alice@snowballtools.xyz",
|
||||
"isVerified": true
|
||||
"isVerified": true,
|
||||
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
|
||||
},
|
||||
{
|
||||
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
||||
"name": "Bob Banner",
|
||||
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
|
||||
"email": "bob@snowballtools.xyz",
|
||||
"isVerified": true
|
||||
"isVerified": true,
|
||||
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,17 +11,13 @@ const log = debug('snowball:initialize-registry');
|
||||
const DENOM = 'aphoton';
|
||||
const BOND_AMOUNT = '1000000000';
|
||||
|
||||
// TODO: Get authority names from args
|
||||
const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
|
||||
|
||||
async function main () {
|
||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
registryConfig.chainId
|
||||
);
|
||||
// TODO: Get authority names from args
|
||||
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||
|
||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||
log('bondId:', bondId);
|
||||
@@ -31,12 +27,8 @@ async function main () {
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
for await (const name of AUTHORITY_NAMES) {
|
||||
await registry.reserveAuthority(
|
||||
{ name },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
);
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||
log('Reserved authority name:', name);
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
|
||||
@@ -5,16 +5,14 @@ import path from 'path';
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH, PROJECT_DOMAIN } from '../src/constants';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||
|
||||
const log = debug('snowball:publish-deploy-records');
|
||||
|
||||
async function main () {
|
||||
const { registryConfig, database } = await getConfig<Config>(
|
||||
DEFAULT_CONFIG_FILE_PATH
|
||||
);
|
||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
@@ -42,7 +40,7 @@ async function main () {
|
||||
});
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`;
|
||||
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
||||
|
||||
const applicationDeploymentRecord = {
|
||||
type: 'ApplicationDeploymentRecord',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
||||
@@ -16,5 +16,10 @@
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
]
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
// eslint extension options
|
||||
"eslint.enable": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"css.customData": [".vscode/tailwind.json"],
|
||||
// prettier extension setting
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.rulers": [80],
|
||||
"editor.codeActionsOnSave": [
|
||||
"source.addMissingImports",
|
||||
"source.fixAll",
|
||||
"source.organizeImports"
|
||||
],
|
||||
// Show in vscode "Problems" tab when there are errors
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
// Use absolute import for typescript files
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="500" height="500" fill="#0F86F5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -5,11 +5,14 @@
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
@@ -18,10 +21,13 @@
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@web3modal/siwe": "^4.0.5",
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"downshift": "^8.2.3",
|
||||
"downshift": "^8.3.2",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"gql-client": "^1.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
@@ -38,10 +44,13 @@
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#2d89ef</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -2,42 +2,34 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<meta name="description" content="snowball tools dashboard" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="%PUBLIC_URL%/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Snowball</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Snowball Tools Dashboard",
|
||||
"short_name": "snowball tools",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
projectsRoutesWithoutSearch,
|
||||
} from './pages/org-slug/projects/routes';
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import { OctokitProvider } from './context/OctokitContext';
|
||||
import Index from './pages';
|
||||
import Login from './pages/Login';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -44,14 +44,14 @@ const router = createBrowserRouter([
|
||||
path: '/',
|
||||
element: <Index />,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<OctokitProvider>
|
||||
<RouterProvider router={router} />
|
||||
</OctokitProvider>
|
||||
);
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Progressive Web App (PWA)",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Kotlin",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "React Native",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "Swift",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"name": "Web app",
|
||||
"icon": "^"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
export default [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: '^',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Image Upload PWA',
|
||||
icon: '^',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Kotlin',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Swift',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
},
|
||||
];
|
||||
@@ -126,7 +126,8 @@ const DatePicker = ({
|
||||
crossOrigin={undefined}
|
||||
/>
|
||||
</PopoverHandler>
|
||||
<PopoverContent>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<PopoverContent placeholder={''}>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
@@ -145,19 +146,23 @@ const DatePicker = ({
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<div className="flex justify-end">
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full mr-2"
|
||||
variant="outlined"
|
||||
onClick={() => setIsOpen(false)}
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
placeholder={''}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const HorizontalLine = () => {
|
||||
return <hr className="h-px bg-gray-300 border-0" />;
|
||||
return <hr className="h-px bg-gray-100 border-0" />;
|
||||
};
|
||||
|
||||
export default HorizontalLine;
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
import React, { forwardRef, RefAttributes } from 'react';
|
||||
|
||||
import { Input, InputProps } from '@material-tailwind/react';
|
||||
import { SearchIcon } from './shared/CustomIcon';
|
||||
import { Input, InputProps } from './shared/Input';
|
||||
|
||||
const SearchBar: React.ForwardRefRenderFunction<
|
||||
HTMLInputElement,
|
||||
InputProps & RefAttributes<HTMLInputElement>
|
||||
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
||||
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
|
||||
return (
|
||||
<div className="relative flex w-full gap-2">
|
||||
<div className="relative flex w-full">
|
||||
<Input
|
||||
leftIcon={<SearchIcon />}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
containerProps={{
|
||||
className: 'min-w-[288px]',
|
||||
}}
|
||||
className="!border-t-blue-gray-300 pl-9 placeholder:text-blue-gray-300 focus:!border-blue-gray-300"
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none',
|
||||
}}
|
||||
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
crossOrigin={undefined}
|
||||
appearance={'borderless'}
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
/>
|
||||
<div className="!absolute left-3 top-[13px]">
|
||||
<i>^</i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,15 +2,26 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Organization } from 'gql-client';
|
||||
|
||||
import { Typography, Option } from '@material-tailwind/react';
|
||||
import { 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[]>([]);
|
||||
@@ -25,59 +36,101 @@ const Sidebar = () => {
|
||||
setSelectedOrgSlug(orgSlug);
|
||||
}, [orgSlug]);
|
||||
|
||||
const handleLogOut = useCallback(() => {
|
||||
disconnect();
|
||||
navigate('/login');
|
||||
}, [disconnect, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4">
|
||||
<div className="flex flex-col h-full p-4 pt-10">
|
||||
<div className="grow">
|
||||
<div>
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<h3 className="text-black text-2xl">Snowball</h3>
|
||||
</Link>
|
||||
</div>
|
||||
<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
|
||||
className="bg-white py-2"
|
||||
containerProps={{ className: 'h-14 border-none' }}
|
||||
labelProps={{ className: 'before:border-none after:border-none' }}
|
||||
className="bg-white rounded-lg shadow border-none"
|
||||
value={selectedOrgSlug}
|
||||
onChange={(value) => {
|
||||
setSelectedOrgSlug(value!);
|
||||
navigate(`/${value}`);
|
||||
}}
|
||||
selected={(_, index) => (
|
||||
<div className="flex gap-2">
|
||||
<div>^</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Application Logo"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<span>{organizations[index!]?.name}</span>
|
||||
<Typography>Organization</Typography>
|
||||
<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}>
|
||||
^ {org.name}
|
||||
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
|
||||
<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>
|
||||
<div>
|
||||
<NavLink
|
||||
to={`/${orgSlug}`}
|
||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
||||
>
|
||||
<Typography>Projects</Typography>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div>
|
||||
<NavLink
|
||||
to={`/${orgSlug}/settings`}
|
||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
||||
>
|
||||
<Typography>Settings</Typography>
|
||||
</NavLink>
|
||||
</div>
|
||||
<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">
|
||||
<div>Documentation</div>
|
||||
<div>Support</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>
|
||||
);
|
||||
|
||||
@@ -21,11 +21,15 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow">
|
||||
<div className="flex gap-2 p-2 items-center">
|
||||
<Avatar variant="rounded" src={project.icon || '/gray.png'} />
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
src={project.icon || '/gray.png'}
|
||||
placeholder={''}
|
||||
/>
|
||||
<div className="grow">
|
||||
<Link to={`projects/${project.id}`}>
|
||||
<Typography>{project.name}</Typography>
|
||||
<Typography color="gray" variant="small">
|
||||
<Typography placeholder={''}>{project.name}</Typography>
|
||||
<Typography color="gray" variant="small" placeholder={''}>
|
||||
{project.deployments[0]?.domain?.name ??
|
||||
'No Production Deployment'}
|
||||
</Typography>
|
||||
@@ -35,25 +39,27 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
||||
<MenuHandler>
|
||||
<button>...</button>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuItem>^ Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500">^ Delete project</MenuItem>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuItem placeholder={''}>^ Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500" placeholder={''}>
|
||||
^ Delete project
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="border-t-2 border-solid p-4 bg-gray-50">
|
||||
{project.deployments.length > 0 ? (
|
||||
<>
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
^ {project.deployments[0].commitMessage}
|
||||
</Typography>
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
{relativeTimeMs(project.deployments[0].createdAt)} on ^
|
||||
{project.deployments[0].branch}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
No Production deployment
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -71,12 +71,13 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
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">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
Suggestions
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -84,19 +85,25 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
<ListItem
|
||||
selected={highlightedIndex === index || selectedItem === item}
|
||||
key={item.id}
|
||||
placeholder={''}
|
||||
{...getItemProps({ item, index })}
|
||||
>
|
||||
<ListItemPrefix>
|
||||
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
|
||||
<ListItemPrefix placeholder={''}>
|
||||
<Avatar
|
||||
src={item.icon || '/gray.png'}
|
||||
variant="rounded"
|
||||
placeholder={''}
|
||||
/>
|
||||
</ListItemPrefix>
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray">
|
||||
<Typography variant="h6" color="blue-gray" placeholder={''}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
className="font-normal"
|
||||
placeholder={''}
|
||||
>
|
||||
{item.organization.name}
|
||||
</Typography>
|
||||
@@ -106,7 +113,9 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
</>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<Typography>^ No projects matching this name</Typography>
|
||||
<Typography placeholder={''}>
|
||||
^ No projects matching this name
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Button } from '@material-tailwind/react';
|
||||
import React from 'react';
|
||||
import OauthPopup from 'react-oauth-popup';
|
||||
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
GitIcon,
|
||||
EllipsesIcon,
|
||||
SnowballIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
const SCOPES = 'repo user';
|
||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||
@@ -14,7 +18,9 @@ interface ConnectAccountInterface {
|
||||
onAuth: (token: string) => void;
|
||||
}
|
||||
|
||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
onAuth: onToken,
|
||||
}: ConnectAccountInterface) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const handleCode = async (code: string) => {
|
||||
@@ -27,17 +33,26 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||
|
||||
// TODO: Use correct height
|
||||
return (
|
||||
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||
<div>^</div>
|
||||
<div>
|
||||
<p>Connect to your git account</p>
|
||||
<p>
|
||||
Once connected, you can import a repository from your
|
||||
<br />
|
||||
account or start with one of our templates.
|
||||
</p>
|
||||
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||
<div className="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>
|
||||
<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>
|
||||
</div>
|
||||
<div className="mt-2 flex">
|
||||
<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}
|
||||
@@ -46,11 +61,15 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button className="rounded-full mx-2">Connect to Github</Button>
|
||||
<Button>Connect to GitHub</Button>
|
||||
</OauthPopup>
|
||||
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
||||
<Button>Connect to Gitea</Button>
|
||||
</div>
|
||||
<ConnectAccountTabPanel />
|
||||
|
||||
{/* TODO: Add ConnectAccountTabPanel */}
|
||||
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||
<ConnectAccountTabPanel />
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Tabs, TabsHeader, Tab } from '@material-tailwind/react';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
|
||||
const ConnectAccountTabPanel = () => {
|
||||
const ConnectAccountTabPanel: React.FC = () => {
|
||||
return (
|
||||
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
|
||||
<TabsHeader className="grid grid-cols-2">
|
||||
<Tab className="row-span-1" value="import">
|
||||
Import a repository
|
||||
</Tab>
|
||||
<Tab className="row-span-2" value="template">
|
||||
Start with a template
|
||||
</Tab>
|
||||
</TabsHeader>
|
||||
{/* <TabsBody> */}
|
||||
{/* TODO: Add content */}
|
||||
{/* </TabsBody> */}
|
||||
<Tabs
|
||||
defaultValue="Connect Accounts Tab Panel"
|
||||
orientation="horizontal"
|
||||
className="mt-10"
|
||||
>
|
||||
<Tabs.List>
|
||||
{[
|
||||
{ title: 'Import a repository' },
|
||||
{ title: 'Start with a template' },
|
||||
].map(({ title }, index) => (
|
||||
<Tabs.Trigger value={title} key={index}>
|
||||
{title}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import ConfirmDialog from '../../shared/ConfirmDialog';
|
||||
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||
|
||||
const TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = searchParams.get('projectId');
|
||||
@@ -21,6 +22,14 @@ const Deploy = () => {
|
||||
navigate(`/${orgSlug}/projects/create`);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timerID = setTimeout(() => {
|
||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||
}, TIMEOUT_DURATION);
|
||||
|
||||
return () => clearInterval(timerID);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between mb-6">
|
||||
@@ -34,7 +43,12 @@ const Deploy = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={handleOpen} variant="outlined" size="sm">
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Cancel
|
||||
</Button>
|
||||
</div>
|
||||
@@ -46,7 +60,7 @@ const Deploy = () => {
|
||||
handleConfirm={handleCancel}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
This will halt the deployment and you will have to start the process
|
||||
from scratch.
|
||||
</Typography>
|
||||
@@ -74,14 +88,6 @@ const Deploy = () => {
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||
}}
|
||||
>
|
||||
VIEW DEMO
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,7 +62,12 @@ const DeployStep = ({
|
||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
||||
{processLogs.map((log, key) => {
|
||||
return (
|
||||
<Typography variant="small" color="gray" key={key}>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
key={key}
|
||||
placeholder={''}
|
||||
>
|
||||
{log}
|
||||
</Typography>
|
||||
);
|
||||
@@ -75,6 +80,7 @@ const DeployStep = ({
|
||||
toast.success('Logs copied');
|
||||
}}
|
||||
color="blue"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Copy log
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Chip, IconButton } from '@material-tailwind/react';
|
||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from '../../../utils/time';
|
||||
import { GitRepositoryDetails } from '../../../types';
|
||||
@@ -14,6 +15,7 @@ interface ProjectRepoCardProps {
|
||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
@@ -22,6 +24,7 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug!, {
|
||||
name: `${repository.owner!.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch!,
|
||||
@@ -30,7 +33,13 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
template: 'webapp',
|
||||
});
|
||||
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
if (Boolean(addProject)) {
|
||||
setIsLoading(false);
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
toast.error('Failed to create project');
|
||||
}
|
||||
}, [client, repository]);
|
||||
|
||||
return (
|
||||
@@ -53,9 +62,15 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
</div>
|
||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
||||
</div>
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm">{'>'}</IconButton>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm" placeholder={''}>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,11 +136,12 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Typography>No repository found</Typography>
|
||||
<Typography placeholder={''}>No repository found</Typography>
|
||||
<Button
|
||||
className="rounded-full mt-5"
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Reset filters
|
||||
</Button>
|
||||
|
||||
@@ -18,11 +18,15 @@ interface TemplateCardProps {
|
||||
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">
|
||||
<Typography className="grow" placeholder={''}>
|
||||
{template.icon} {template.name}
|
||||
</Typography>
|
||||
<div>
|
||||
<IconButton size="sm" className="rounded-full hidden group-hover:block">
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full hidden group-hover:block"
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Typography, IconButton } from '@material-tailwind/react';
|
||||
import { Typography, IconButton, Avatar } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from '../../../utils/time';
|
||||
import { GitCommitWithBranch } from '../../../types';
|
||||
@@ -11,21 +11,31 @@ interface ActivityCardProps {
|
||||
|
||||
const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
return (
|
||||
<div className="group flex hover:bg-gray-200 rounded mt-1">
|
||||
<div className="w-4">^</div>
|
||||
|
||||
<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>{activity.commit.author?.name}</Typography>
|
||||
<Typography variant="small" color="gray">
|
||||
<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">
|
||||
<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">
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full bg-gray-600"
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,9 @@ interface AssignDomainProps {
|
||||
|
||||
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader>Assign Domain</DialogHeader>
|
||||
<DialogBody>
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader placeholder={''}>Assign Domain</DialogHeader>
|
||||
<DialogBody placeholder={''}>
|
||||
In order to assign a domain to your production deployments, configure it
|
||||
in the{' '}
|
||||
{/* TODO: Fix selection of project settings tab on navigation to domains */}
|
||||
@@ -36,12 +36,13 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
theme={atomOneLight}
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
onClick={handleOpen}
|
||||
placeholder={''}
|
||||
>
|
||||
<span>Okay</span>
|
||||
</Button>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Typography,
|
||||
Chip,
|
||||
ChipProps,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
@@ -24,6 +25,7 @@ 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';
|
||||
|
||||
interface DeployDetailsCardProps {
|
||||
deployment: Deployment;
|
||||
@@ -87,52 +89,65 @@ const DeploymentDetailsCard = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
|
||||
<div className="col-span-2">
|
||||
<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 && (
|
||||
<Typography className=" basis-3/4">{deployment.url}</Typography>
|
||||
<Typography className="basis-3/4" placeholder={''}>
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
<Chip
|
||||
value={deployment.status}
|
||||
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
||||
variant="ghost"
|
||||
icon={<i>^</i>}
|
||||
/>
|
||||
</div>
|
||||
<Typography color="gray">
|
||||
<Typography color="gray" placeholder={''}>
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography color="gray">^ {deployment.branch}</Typography>
|
||||
<Typography color="gray">
|
||||
<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-1 flex items-center">
|
||||
<Typography color="gray" className="grow">
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
|
||||
<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>
|
||||
<MenuList placeholder={''}>
|
||||
<a href={deployment.url} target="_blank" rel="noreferrer">
|
||||
<MenuItem disabled={!Boolean(deployment.url)}>^ Visit</MenuItem>
|
||||
<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>
|
||||
@@ -145,6 +160,7 @@ const DeploymentDetailsCard = ({
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Redeploy to production
|
||||
</MenuItem>
|
||||
@@ -155,6 +171,7 @@ const DeploymentDetailsCard = ({
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Rollback to this version
|
||||
</MenuItem>
|
||||
@@ -173,17 +190,22 @@ const DeploymentDetailsCard = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small">
|
||||
<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}>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="blue"
|
||||
key={value.id}
|
||||
placeholder={''}
|
||||
>
|
||||
^ {value.name}
|
||||
</Typography>
|
||||
);
|
||||
@@ -202,16 +224,16 @@ const DeploymentDetailsCard = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small">
|
||||
<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">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
{deployment.domain?.name && (
|
||||
<Typography variant="small" color="blue">
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
{deployment.domain?.name}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -230,7 +252,7 @@ const DeploymentDetailsCard = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will replace your current
|
||||
deployment
|
||||
</Typography>
|
||||
@@ -248,10 +270,10 @@ const DeploymentDetailsCard = ({
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue">
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
^ {currentDeployment.domain?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 { formatAddress } from '../../../../utils/format';
|
||||
|
||||
interface DeploymentDialogBodyCardProps {
|
||||
deployment: Deployment;
|
||||
@@ -19,7 +20,7 @@ const DeploymentDialogBodyCard = ({
|
||||
deployment,
|
||||
}: DeploymentDialogBodyCardProps) => {
|
||||
return (
|
||||
<Card className="p-2 shadow-none">
|
||||
<Card className="p-2 shadow-none" placeholder={''}>
|
||||
{chip && (
|
||||
<Chip
|
||||
className={`w-fit normal-case font-normal`}
|
||||
@@ -29,17 +30,18 @@ const DeploymentDialogBodyCard = ({
|
||||
/>
|
||||
)}
|
||||
{deployment.url && (
|
||||
<Typography variant="small" className="text-black">
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {deployment.branch} ^{' '}
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</Typography>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -44,18 +44,18 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2 text-sm text-gray-600">
|
||||
<div className="col-span-2">
|
||||
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
|
||||
<div className="col-span-4">
|
||||
<SearchBar
|
||||
placeholder="Search branches"
|
||||
value={searchedBranch}
|
||||
onChange={(event) => setSearchedBranch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="col-span-2">
|
||||
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
||||
</div>
|
||||
<div className="col-span-1 relative">
|
||||
<div className="col-span-2 relative">
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
onChange={(value) => setSelectedStatus(value as StatusOptions)}
|
||||
@@ -77,6 +77,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</IconButton>
|
||||
|
||||
@@ -21,7 +21,9 @@ const AddEnvironmentVariableRow = ({
|
||||
return (
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small">Key</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.key`, {
|
||||
@@ -30,7 +32,9 @@ const AddEnvironmentVariableRow = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small">Value</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.value`, {
|
||||
@@ -43,6 +47,7 @@ const AddEnvironmentVariableRow = ({
|
||||
size="sm"
|
||||
onClick={() => onDelete()}
|
||||
disabled={isDeleteDisabled}
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
|
||||
@@ -61,23 +61,26 @@ const AddMemberDialog = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Add member</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4">
|
||||
<Typography variant="small">
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
We will send an invitation link to this email address.
|
||||
</Typography>
|
||||
<Typography variant="small">Email address</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Email address
|
||||
</Typography>
|
||||
<Input
|
||||
type="email"
|
||||
crossOrigin={undefined}
|
||||
@@ -85,8 +88,10 @@ const AddMemberDialog = ({
|
||||
required: 'email field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small">Permissions</Typography>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Permissions
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
You can change this later if required.
|
||||
</Typography>
|
||||
<Checkbox
|
||||
@@ -102,8 +107,13 @@ const AddMemberDialog = ({
|
||||
color="blue"
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -111,6 +121,7 @@ const AddMemberDialog = ({
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Send invite
|
||||
</Button>
|
||||
|
||||
@@ -53,20 +53,21 @@ const DeleteProjectDialog = ({
|
||||
}, [client, project, handleOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Delete project?</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2">
|
||||
<Typography variant="paragraph">
|
||||
<DialogBody className="flex flex-col gap-2" placeholder={''}>
|
||||
<Typography variant="paragraph" placeholder={''}>
|
||||
Deleting your project is irreversible. Enter your project’s
|
||||
name
|
||||
<span className="bg-blue-100 text-blue-700">({project.name})</span>
|
||||
@@ -80,12 +81,17 @@ const DeleteProjectDialog = ({
|
||||
validate: (value) => value === project.name,
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small" color="red">
|
||||
<Typography variant="small" color="red" placeholder={''}>
|
||||
^ Deleting your project is irreversible.
|
||||
</Typography>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -93,6 +99,7 @@ const DeleteProjectDialog = ({
|
||||
color="red"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Yes, Delete project
|
||||
</Button>
|
||||
|
||||
@@ -29,11 +29,11 @@ const DisplayEnvironmentVariables = ({
|
||||
</div>
|
||||
<Collapse open={openCollapse}>
|
||||
{variables.length === 0 ? (
|
||||
<Card className="bg-gray-300 flex items-center p-4">
|
||||
<Typography variant="small" className="text-black">
|
||||
<Card className="bg-gray-300 flex items-center p-4" placeholder={''}>
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
No environment variables added yet.
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once you add them, they’ll show up here.
|
||||
</Typography>
|
||||
</Card>
|
||||
|
||||
@@ -68,7 +68,7 @@ const DomainCard = ({
|
||||
<>
|
||||
<div className="flex justify-between py-3">
|
||||
<div className="flex justify-start gap-1">
|
||||
<Typography variant="h6">
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
<i>^</i> {domain.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
@@ -97,18 +97,20 @@ const DomainCard = ({
|
||||
<MenuHandler>
|
||||
<button className="border-2 rounded-full w-8 h-8">...</button>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuItem
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
setEditDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Edit domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Delete domain
|
||||
</MenuItem>
|
||||
@@ -127,7 +129,7 @@ const DomainCard = ({
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once deleted, the project{' '}
|
||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||
{project.name}
|
||||
@@ -140,15 +142,21 @@ const DomainCard = ({
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
|
||||
<Typography variant="small">Production</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Production
|
||||
</Typography>
|
||||
{domain.status === DomainStatus.Pending && (
|
||||
<Card className="bg-gray-200 p-4 text-sm">
|
||||
<Card className="bg-gray-200 p-4 text-sm" placeholder={''}>
|
||||
{refreshStatus === RefreshStatus.IDLE ? (
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ Add these records to your domain and refresh to check
|
||||
</Typography>
|
||||
) : refreshStatus === RefreshStatus.CHECKING ? (
|
||||
<Typography variant="small" className="text-blue-500">
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-blue-500"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Checking records for {domain.name}
|
||||
</Typography>
|
||||
) : (
|
||||
|
||||
@@ -122,27 +122,32 @@ const EditDomainDialog = ({
|
||||
}, [domain]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Edit domain</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4">
|
||||
<Typography variant="small">Domain name</Typography>
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain name
|
||||
</Typography>
|
||||
<Input crossOrigin={undefined} {...register('name')} />
|
||||
<Typography variant="small">Redirect to</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Redirect to
|
||||
</Typography>
|
||||
<Controller
|
||||
name="redirectedTo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select {...field} disabled={isDisableDropdown}>
|
||||
<Select {...field} disabled={isDisableDropdown} placeholder={''}>
|
||||
{redirectOptions.map((option, key) => (
|
||||
<Option key={key} value={option}>
|
||||
^ {option}
|
||||
@@ -154,14 +159,16 @@ const EditDomainDialog = ({
|
||||
{isDisableDropdown && (
|
||||
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
||||
<div>^</div>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain “{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
||||
redirects to this domain so you can not redirect this doman
|
||||
further.
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<Typography variant="small">Git branch</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Git branch
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register('branch', {
|
||||
@@ -174,13 +181,22 @@ const EditDomainDialog = ({
|
||||
}
|
||||
/>
|
||||
{!isValid && (
|
||||
<Typography variant="small" className="text-red-500">
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-red-500"
|
||||
placeholder={''}
|
||||
>
|
||||
We couldn't find this branch in the connected Git repository.
|
||||
</Typography>
|
||||
)}
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -188,6 +204,7 @@ const EditDomainDialog = ({
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
placeholder={''}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
|
||||
@@ -84,7 +84,9 @@ const EditEnvironmentVariableRow = ({
|
||||
<>
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small">Key</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
@@ -92,7 +94,9 @@ const EditEnvironmentVariableRow = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small">Value</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
@@ -114,6 +118,7 @@ const EditEnvironmentVariableRow = ({
|
||||
<IconButton
|
||||
onClick={handleSubmit(updateEnvironmentVariableHandler)}
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
{'S'}
|
||||
</IconButton>
|
||||
@@ -125,6 +130,7 @@ const EditEnvironmentVariableRow = ({
|
||||
reset();
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'C'}
|
||||
</IconButton>
|
||||
@@ -138,6 +144,7 @@ const EditEnvironmentVariableRow = ({
|
||||
onClick={() => {
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'E'}
|
||||
</IconButton>
|
||||
@@ -146,6 +153,7 @@ const EditEnvironmentVariableRow = ({
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
{'D'}
|
||||
</IconButton>
|
||||
@@ -162,7 +170,7 @@ const EditEnvironmentVariableRow = ({
|
||||
handleConfirm={removeEnvironmentVariableHandler}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Are you sure you want to delete the variable
|
||||
<span className="bg-blue-100">{variable.key}</span>?
|
||||
</Typography>
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
Option,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
|
||||
const PERMISSION_OPTIONS = [
|
||||
{
|
||||
@@ -48,6 +50,7 @@ const MemberCard = ({
|
||||
onRemoveProjectMember,
|
||||
onUpdateProjectMember,
|
||||
}: MemberCardProps) => {
|
||||
const [ethAddress, emailDomain] = member.email.split('@');
|
||||
const [selectedPermission, setSelectedPermission] = useState(
|
||||
permissions.join('+'),
|
||||
);
|
||||
@@ -79,8 +82,16 @@ const MemberCard = ({
|
||||
>
|
||||
<div>^</div>
|
||||
<div className="basis-1/2">
|
||||
{member.name && <Typography variant="small">{member.name}</Typography>}
|
||||
<Typography variant="small">{member.email}</Typography>
|
||||
{member.name && (
|
||||
<Tooltip content={member.name}>
|
||||
{formatAddress(member.name ?? '')}
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip content={member.email}>
|
||||
<p>
|
||||
{formatAddress(ethAddress)}@{emailDomain}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="basis-1/2">
|
||||
{!isPending ? (
|
||||
@@ -93,6 +104,7 @@ const MemberCard = ({
|
||||
selected={(_, index) => (
|
||||
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
||||
)}
|
||||
placeholder={''}
|
||||
>
|
||||
{DROPDOWN_OPTIONS.map((permission, key) => (
|
||||
<Option key={key} value={permission.value}>
|
||||
@@ -121,6 +133,7 @@ const MemberCard = ({
|
||||
onClick={() => {
|
||||
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
D
|
||||
</IconButton>
|
||||
@@ -141,9 +154,10 @@ const MemberCard = ({
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
Once removed, {member.name} ({member.email}) will not be able to
|
||||
access this project.
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once removed, {formatAddress(member.name ?? '')} (
|
||||
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
||||
this project.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
|
||||
@@ -17,14 +17,19 @@ const RepoConnectedSection = ({
|
||||
<div className="flex gap-4">
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<Typography variant="small">{linkedRepo.full_name}</Typography>
|
||||
<Typography variant="small">Connected just now</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
{linkedRepo.full_name}
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Connected just now
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setDisconnectRepoDialogOpen(true)}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Disconnect
|
||||
</Button>
|
||||
@@ -39,7 +44,7 @@ const RepoConnectedSection = ({
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Any data tied to your Git project may become misconfigured. Are you
|
||||
sure you want to continue?
|
||||
</Typography>
|
||||
|
||||
@@ -54,14 +54,18 @@ const SetupDomain = () => {
|
||||
className="flex flex-col gap-6 w-full"
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h5">Setup domain name</Typography>
|
||||
<Typography variant="small">
|
||||
<Typography variant="h5" placeholder={''}>
|
||||
Setup domain name
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Add your domain and setup redirects
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="w-auto">
|
||||
<Typography variant="small">Domain name</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain name
|
||||
</Typography>
|
||||
<Input
|
||||
type="text"
|
||||
variant="outlined"
|
||||
@@ -76,7 +80,7 @@ const SetupDomain = () => {
|
||||
|
||||
{isValid && (
|
||||
<div>
|
||||
<Typography>Primary domain</Typography>
|
||||
<Typography placeholder={''}>Primary domain</Typography>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Radio
|
||||
label={domainStr}
|
||||
@@ -108,6 +112,7 @@ const SetupDomain = () => {
|
||||
className="w-fit"
|
||||
color={isValid ? 'blue' : 'gray'}
|
||||
type="submit"
|
||||
placeholder={''}
|
||||
>
|
||||
<i>^</i> Next
|
||||
</Button>
|
||||
|
||||
@@ -23,6 +23,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
navigator.clipboard.writeText(webhookUrl);
|
||||
toast.success('Copied to clipboard');
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
C
|
||||
</Button>
|
||||
@@ -32,6 +33,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
@@ -48,7 +50,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<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>
|
||||
|
||||
@@ -9,7 +9,7 @@ const AsyncSelect = React.forwardRef((props: SelectProps, ref: any) => {
|
||||
|
||||
useEffect(() => setKey((preVal) => preVal + 1), [props]);
|
||||
|
||||
return <Select key={key} ref={ref} {...props} />;
|
||||
return <Select key={key} ref={ref} {...props} placeholder={''} />;
|
||||
});
|
||||
|
||||
AsyncSelect.displayName = 'AsyncSelect';
|
||||
|
||||
@@ -62,7 +62,7 @@ export const buttonTheme = tv(
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'bg-transparent',
|
||||
'bg-controls-tertiary',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cloneIcon } from 'utils/cloneIcon';
|
||||
/**
|
||||
* Represents the properties of a base button component.
|
||||
*/
|
||||
interface ButtonBaseProps {
|
||||
export interface ButtonBaseProps {
|
||||
/**
|
||||
* The optional left icon element for a component.
|
||||
* @type {ReactNode}
|
||||
|
||||
@@ -5,7 +5,7 @@ export const calendarTheme = tv({
|
||||
wrapper: [
|
||||
'max-w-[352px]',
|
||||
'bg-surface-floating',
|
||||
'shadow-calendar',
|
||||
'shadow-dropdown',
|
||||
'rounded-xl',
|
||||
],
|
||||
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Calendar as ReactCalendar,
|
||||
CalendarProps as ReactCalendarProps,
|
||||
} from 'react-calendar';
|
||||
import { Value } from 'react-calendar/dist/cjs/shared/types';
|
||||
import { CalendarTheme, calendarTheme } from './Calendar.theme';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
@@ -21,6 +20,8 @@ import {
|
||||
import './Calendar.css';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
export type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
const CALENDAR_VIEW = ['month', 'year', 'decade'] as const;
|
||||
export type CalendarView = (typeof CALENDAR_VIEW)[number];
|
||||
|
||||
|
||||
@@ -30,23 +30,31 @@ const ConfirmDialog = ({
|
||||
color,
|
||||
}: ConfirmDialogProp) => {
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Typography variant="h6">{dialogTitle} </Typography>
|
||||
<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>{children}</DialogBody>
|
||||
<DialogFooter className="flex justify-start gap-2">
|
||||
<Button variant="outlined" onClick={handleOpen}>
|
||||
<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}>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color={color}
|
||||
onClick={handleConfirm}
|
||||
placeholder={''}
|
||||
>
|
||||
{confirmButtonTitle}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRadioIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 16.5C13.1421 16.5 16.5 13.1421 16.5 9C16.5 4.85786 13.1421 1.5 9 1.5C4.85786 1.5 1.5 4.85786 1.5 9C1.5 13.1421 4.85786 16.5 9 16.5ZM7.93079 12.3663L12.3055 7.01944L11.1446 6.06958L7.81944 10.1336L6.37511 8.68932L5.31445 9.74998L7.93079 12.3663Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRoundFilledIcon = (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 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronDownIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20 9L13.4142 15.5858C12.6332 16.3668 11.3669 16.3668 10.5858 15.5858L4 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const EllipseIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon width="9" height="8" viewBox="0 0 9 8" fill="none" {...props}>
|
||||
<circle cx="4.5" cy="4" r="4" fill="#082F56" fillOpacity="0.1" />
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
import { EllipseIcon } from './EllipseIcon';
|
||||
|
||||
export const EllipsesIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon width="51" height="8" viewBox="0 0 51 8" fill="none" {...props}>
|
||||
<EllipseIcon />
|
||||
<EllipseIcon x="14" />
|
||||
<EllipseIcon x="28" />
|
||||
<EllipseIcon x="42" />
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const FolderIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
id="Icon"
|
||||
d="M2.0835 4.58341V14.5834C2.0835 15.5039 2.82969 16.2501 3.75016 16.2501H16.2502C17.1706 16.2501 17.9168 15.5039 17.9168 14.5834V7.08341C17.9168 6.16294 17.1706 5.41675 16.2502 5.41675H10.8921C10.3349 5.41675 9.81449 5.13825 9.50539 4.67458L8.82827 3.65891C8.51916 3.19525 7.99878 2.91675 7.44152 2.91675H3.75016C2.82969 2.91675 2.0835 3.66294 2.0835 4.58341Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const GitIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="37"
|
||||
height="36"
|
||||
viewBox="0 0 37 36"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M35.7782 16.4219L20.0791 0.723956C19.864 0.508762 19.6087 0.338053 19.3276 0.221583C19.0466 0.105114 18.7453 0.045166 18.4411 0.045166C18.1368 0.045166 17.8356 0.105114 17.5545 0.221583C17.2735 0.338053 17.0181 0.508762 16.8031 0.723956L13.5443 3.9843L17.6788 8.11882C18.1649 7.95374 18.6875 7.92797 19.1875 8.04442C19.6874 8.16088 20.1448 8.41491 20.5079 8.77778C20.8731 9.14329 21.128 9.60418 21.2435 10.1077C21.359 10.6113 21.3304 11.1372 21.161 11.6253L25.1473 15.6103C25.6355 15.4408 26.1616 15.4122 26.6653 15.5279C27.169 15.6437 27.6299 15.899 27.9952 16.2646C28.251 16.5204 28.454 16.8241 28.5925 17.1584C28.7309 17.4926 28.8022 17.8509 28.8022 18.2127C28.8022 18.5745 28.7309 18.9328 28.5925 19.267C28.454 19.6013 28.251 19.905 27.9952 20.1608C27.4779 20.6776 26.7766 20.9678 26.0455 20.9678C25.3143 20.9678 24.6131 20.6776 24.0958 20.1608C23.7116 19.7759 23.4497 19.286 23.3434 18.7526C23.237 18.2192 23.2907 17.6663 23.4979 17.1634L19.7805 13.4472V23.2287C20.1729 23.4225 20.5134 23.707 20.7739 24.0586C21.0345 24.4103 21.2075 24.8189 21.2786 25.2507C21.3497 25.6825 21.317 26.1251 21.183 26.5417C21.0491 26.9583 20.8178 27.337 20.5083 27.6465C20.2525 27.9023 19.9488 28.1053 19.6146 28.2438C19.2803 28.3822 18.922 28.4535 18.5602 28.4535C18.1984 28.4535 17.8402 28.3822 17.5059 28.2438C17.1716 28.1053 16.8679 27.9023 16.6121 27.6465C16.3562 27.3907 16.1532 27.0869 16.0147 26.7526C15.8762 26.4183 15.8049 26.06 15.8049 25.6982C15.8049 25.3363 15.8762 24.978 16.0147 24.6437C16.1532 24.3094 16.3562 24.0057 16.6121 23.7499C16.8699 23.4916 17.1763 23.2869 17.5137 23.1477V13.2762C17.1777 13.1378 16.8724 12.9344 16.6153 12.6777C16.3582 12.421 16.1543 12.116 16.0154 11.7802C15.8765 11.4445 15.8053 11.0846 15.8059 10.7212C15.8065 10.3579 15.8789 9.9982 16.0189 9.66291L11.9423 5.58552L1.17673 16.3483C0.742912 16.783 0.499268 17.372 0.499268 17.9862C0.499268 18.6003 0.742912 19.1893 1.17673 19.624L16.8766 35.3239C17.3113 35.7576 17.9002 36.0011 18.5143 36.0011C19.1283 36.0011 19.7172 35.7576 20.1519 35.3239L35.7782 19.6975C36.212 19.2629 36.4557 18.6738 36.4557 18.0597C36.4557 17.4456 36.212 16.8566 35.7782 16.4219Z"
|
||||
fill="#158FFF"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const InfoRoundFilledIcon = (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 11C10 10.4477 10.4477 10 11 10H12C12.5523 10 13 10.4477 13 11V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12C10.4477 12 10 11.5523 10 11ZM12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LifeBuoyIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
id="Icon"
|
||||
d="M4.5835 4.58325L7.35092 7.35068M7.35092 12.6492L4.5835 15.4166M12.6494 12.6492L15.4168 15.4166M15.4168 4.58325L12.6494 7.35068M17.9168 9.99992C17.9168 14.3722 14.3724 17.9166 10.0002 17.9166C5.62791 17.9166 2.0835 14.3722 2.0835 9.99992C2.0835 5.62766 5.62791 2.08325 10.0002 2.08325C14.3724 2.08325 17.9168 5.62766 17.9168 9.99992ZM13.7502 9.99992C13.7502 12.071 12.0712 13.7499 10.0002 13.7499C7.92909 13.7499 6.25016 12.071 6.25016 9.99992C6.25016 7.92885 7.92909 6.24992 10.0002 6.24992C12.0712 6.24992 13.7502 7.92885 13.7502 9.99992Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LoadingIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.0002 1.66669C10.4605 1.66669 10.8336 2.03978 10.8336 2.50002V5.00002C10.8336 5.46026 10.4605 5.83335 10.0002 5.83335C9.54 5.83335 9.1669 5.46026 9.1669 5.00002V2.50002C9.1669 2.03978 9.54 1.66669 10.0002 1.66669ZM15.8928 4.10746C16.2182 4.4329 16.2182 4.96054 15.8928 5.28597L14.125 7.05374C13.7996 7.37918 13.272 7.37918 12.9465 7.05374C12.6211 6.7283 12.6211 6.20067 12.9465 5.87523L14.7143 4.10746C15.0397 3.78203 15.5674 3.78203 15.8928 4.10746ZM4.10768 4.10746C4.43312 3.78203 4.96076 3.78203 5.28619 4.10746L7.05396 5.87523C7.3794 6.20067 7.3794 6.7283 7.05396 7.05374C6.72852 7.37918 6.20088 7.37918 5.87545 7.05374L4.10768 5.28597C3.78224 4.96054 3.78224 4.4329 4.10768 4.10746ZM1.66666 10.0006C1.66666 9.54035 2.03975 9.16725 2.49999 9.16725H4.99999C5.46023 9.16725 5.83332 9.54035 5.83332 10.0006C5.83332 10.4608 5.46023 10.8339 4.99999 10.8339H2.49999C2.03975 10.8339 1.66666 10.4608 1.66666 10.0006ZM14.1667 10.0006C14.1667 9.54035 14.5398 9.16725 15 9.16725H17.5C17.9602 9.16725 18.3333 9.54035 18.3333 10.0006C18.3333 10.4608 17.9602 10.8339 17.5 10.8339H15C14.5398 10.8339 14.1667 10.4608 14.1667 10.0006ZM7.05396 12.9463C7.3794 13.2717 7.3794 13.7994 7.05396 14.1248L5.28619 15.8926C4.96076 16.218 4.43312 16.218 4.10768 15.8926C3.78224 15.5671 3.78224 15.0395 4.10768 14.7141L5.87545 12.9463C6.20088 12.6209 6.72852 12.6209 7.05396 12.9463ZM12.9465 12.9463C13.272 12.6209 13.7996 12.6209 14.125 12.9463L15.8928 14.7141C16.2182 15.0395 16.2182 15.5671 15.8928 15.8926C15.5674 16.218 15.0397 16.218 14.7143 15.8926L12.9465 14.1248C12.6211 13.7994 12.6211 13.2717 12.9465 12.9463ZM10.0002 14.1667C10.4605 14.1667 10.8336 14.5398 10.8336 15V17.5C10.8336 17.9603 10.4605 18.3334 10.0002 18.3334C9.54 18.3334 9.1669 17.9603 9.1669 17.5V15C9.1669 14.5398 9.54 14.1667 10.0002 14.1667Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const NotificationBellIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
id="Icon"
|
||||
d="M13.3333 15.4166C12.7722 16.8882 11.4909 17.9166 9.99997 17.9166C8.50903 17.9166 7.22769 16.8882 6.66664 15.4166M4.80461 15.4166H15.1953C16.1978 15.4166 16.9735 14.5379 16.8491 13.5432L16.0885 7.45809C15.7047 4.38751 13.0944 2.08325 9.99997 2.08325C6.90549 2.08325 4.29527 4.38751 3.91145 7.45809L3.15081 13.5432C3.02647 14.5379 3.80211 15.4166 4.80461 15.4166Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const PencilIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8.625 3.125C8.90114 3.125 9.125 2.90114 9.125 2.625C9.125 2.34886 8.90114 2.125 8.625 2.125V3.125ZM15.875 9.375C15.875 9.09886 15.6511 8.875 15.375 8.875C15.0989 8.875 14.875 9.09886 14.875 9.375H15.875ZM3.44401 15.2115L3.67101 14.766L3.67101 14.766L3.44401 15.2115ZM2.78849 14.556L3.23399 14.329H3.23399L2.78849 14.556ZM14.556 15.2115L14.329 14.766L14.329 14.766L14.556 15.2115ZM15.2115 14.556L14.766 14.329L14.766 14.329L15.2115 14.556ZM2.78849 3.44401L2.34299 3.21702L2.78849 3.44401ZM3.44401 2.78849L3.21702 2.34299V2.34299L3.44401 2.78849ZM9.375 6.375L9.02145 6.02145C8.92768 6.11521 8.875 6.24239 8.875 6.375H9.375ZM9.375 8.625H8.875C8.875 8.90114 9.09886 9.125 9.375 9.125V8.625ZM11.625 8.625V9.125C11.7576 9.125 11.8848 9.07232 11.9786 8.97855L11.625 8.625ZM13.3768 2.37316L13.7304 2.72671V2.72671L13.3768 2.37316ZM15.4982 2.37316L15.8517 2.01961L15.8517 2.01961L15.4982 2.37316ZM15.6268 2.50184L15.2733 2.85539V2.85539L15.6268 2.50184ZM15.6268 4.62316L15.9804 4.97671V4.97671L15.6268 4.62316ZM12.975 14.875H5.025V15.875H12.975V14.875ZM3.125 12.975V5.025H2.125V12.975H3.125ZM5.025 3.125H8.625V2.125H5.025V3.125ZM14.875 9.375V12.975H15.875V9.375H14.875ZM5.025 14.875C4.59671 14.875 4.30556 14.8746 4.08052 14.8562C3.86131 14.8383 3.74921 14.8059 3.67101 14.766L3.21702 15.657C3.45969 15.7807 3.71804 15.8299 3.99909 15.8529C4.2743 15.8754 4.61321 15.875 5.025 15.875V14.875ZM2.125 12.975C2.125 13.3868 2.12461 13.7257 2.1471 14.0009C2.17006 14.282 2.21934 14.5403 2.34299 14.783L3.23399 14.329C3.19415 14.2508 3.16169 14.1387 3.14378 13.9195C3.12539 13.6944 3.125 13.4033 3.125 12.975H2.125ZM3.67101 14.766C3.48285 14.6701 3.32987 14.5172 3.23399 14.329L2.34299 14.783C2.53473 15.1593 2.8407 15.4653 3.21702 15.657L3.67101 14.766ZM12.975 15.875C13.3868 15.875 13.7257 15.8754 14.0009 15.8529C14.282 15.8299 14.5403 15.7807 14.783 15.657L14.329 14.766C14.2508 14.8059 14.1387 14.8383 13.9195 14.8562C13.6944 14.8746 13.4033 14.875 12.975 14.875V15.875ZM14.875 12.975C14.875 13.4033 14.8746 13.6944 14.8562 13.9195C14.8383 14.1387 14.8059 14.2508 14.766 14.329L15.657 14.783C15.7807 14.5403 15.8299 14.282 15.8529 14.0009C15.8754 13.7257 15.875 13.3868 15.875 12.975H14.875ZM14.783 15.657C15.1593 15.4653 15.4653 15.1593 15.657 14.783L14.766 14.329C14.6701 14.5172 14.5172 14.6701 14.329 14.766L14.783 15.657ZM3.125 5.025C3.125 4.59671 3.12539 4.30556 3.14378 4.08052C3.16169 3.86131 3.19415 3.74921 3.23399 3.67101L2.34299 3.21702C2.21934 3.45969 2.17006 3.71804 2.1471 3.99909C2.12461 4.2743 2.125 4.61321 2.125 5.025H3.125ZM5.025 2.125C4.61321 2.125 4.2743 2.12461 3.99909 2.1471C3.71804 2.17006 3.45969 2.21934 3.21702 2.34299L3.67101 3.23399C3.74921 3.19415 3.86131 3.16169 4.08052 3.14378C4.30556 3.12539 4.59671 3.125 5.025 3.125V2.125ZM3.23399 3.67101C3.32987 3.48285 3.48285 3.32987 3.67101 3.23399L3.21702 2.34299C2.84069 2.53473 2.53473 2.84069 2.34299 3.21702L3.23399 3.67101ZM8.875 6.375V8.625H9.875V6.375H8.875ZM9.375 9.125H11.625V8.125H9.375V9.125ZM9.72855 6.72855L13.7304 2.72671L13.0233 2.01961L9.02145 6.02145L9.72855 6.72855ZM15.1446 2.72671L15.2733 2.85539L15.9804 2.14829L15.8517 2.01961L15.1446 2.72671ZM15.2733 4.26961L11.2714 8.27145L11.9786 8.97855L15.9804 4.97671L15.2733 4.26961ZM15.2733 2.85539C15.6638 3.24592 15.6638 3.87908 15.2733 4.26961L15.9804 4.97671C16.7614 4.19566 16.7614 2.92933 15.9804 2.14829L15.2733 2.85539ZM13.7304 2.72671C14.1209 2.33619 14.7541 2.33619 15.1446 2.72671L15.8517 2.01961C15.0707 1.23856 13.8043 1.23856 13.0233 2.01961L13.7304 2.72671Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const QuestionMarkRoundIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.99984 2.50008C5.8577 2.50008 2.49984 5.85795 2.49984 10.0001C2.49984 14.1422 5.8577 17.5001 9.99984 17.5001C14.142 17.5001 17.4998 14.1422 17.4998 10.0001C17.4998 5.85795 14.142 2.50008 9.99984 2.50008ZM1.6665 10.0001C1.6665 5.39771 5.39746 1.66675 9.99984 1.66675C14.6022 1.66675 18.3332 5.39771 18.3332 10.0001C18.3332 14.6025 14.6022 18.3334 9.99984 18.3334C5.39746 18.3334 1.6665 14.6025 1.6665 10.0001Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.58317 6.66675C8.89281 6.66675 8.33317 7.22639 8.33317 7.91675C8.33317 8.14687 8.14662 8.33342 7.9165 8.33342C7.68639 8.33342 7.49984 8.14687 7.49984 7.91675C7.49984 6.76616 8.43258 5.83342 9.58317 5.83342H10.4869C11.5986 5.83342 12.4998 6.73462 12.4998 7.84631C12.4998 8.55337 12.1289 9.20858 11.5226 9.57236L11.0234 9.87187C10.6469 10.0978 10.4165 10.5047 10.4165 10.9437V12.0834C10.4165 12.3135 10.23 12.5001 9.99984 12.5001C9.76972 12.5001 9.58317 12.3135 9.58317 12.0834V10.9437C9.58317 10.2119 9.96713 9.5338 10.5946 9.15729L11.0938 8.85778C11.4491 8.6446 11.6665 8.26065 11.6665 7.84631C11.6665 7.19486 11.1384 6.66675 10.4869 6.66675H9.58317Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.58317 13.7501C9.58317 13.52 9.76972 13.3334 9.99984 13.3334C10.23 13.3334 10.4165 13.52 10.4165 13.7501C10.4165 13.9802 10.23 14.1667 9.99984 14.1667C9.76972 14.1667 9.58317 13.9802 9.58317 13.7501Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const SettingsSlidersIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
id="Icon"
|
||||
d="M11.2498 5.83341H2.9165M11.2498 5.83341C11.2498 4.22196 12.555 2.91675 14.1665 2.91675C15.778 2.91675 17.0832 4.22196 17.0832 5.83341C17.0832 7.44487 15.778 8.75008 14.1665 8.75008C12.555 8.75008 11.2498 7.44487 11.2498 5.83341ZM17.0832 14.1667H10.4165M10.4165 14.1667C10.4165 15.7782 9.1113 17.0834 7.49984 17.0834C5.88838 17.0834 4.58317 15.7782 4.58317 14.1667M10.4165 14.1667C10.4165 12.5553 9.1113 11.2501 7.49984 11.2501C5.88838 11.2501 4.58317 12.5553 4.58317 14.1667M4.58317 14.1667H2.9165"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const SnowballIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="41"
|
||||
height="28"
|
||||
viewBox="0 0 41 28"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.2898 0.0141679C16.9879 0.197656 19.8512 2.8136 22.4926 5.37543C25.1675 7.96975 27.9859 10.7641 28.1852 14.4608C28.3924 18.3031 26.397 21.9641 23.5274 24.5678C20.7709 27.0689 17.0322 27.8722 13.2898 27.9763C9.40947 28.0842 5.13223 27.9321 2.45841 25.1539C-0.178535 22.414 0.162951 18.2384 0.282169 14.4608C0.396158 10.8489 0.681118 7.11475 3.11295 4.41388C5.67064 1.57324 9.44481 -0.176604 13.2898 0.0141679Z"
|
||||
fill="#4BA4F7"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.4897 0.0139543C21.1879 0.197442 24.0511 2.81338 26.6926 5.37521C29.3675 7.96953 32.1858 10.7639 32.3852 14.4606C32.5923 18.3029 30.597 21.9638 27.7274 24.5676C24.9709 27.0687 21.2321 27.872 17.4897 27.9761C13.6094 28.084 9.33219 27.9319 6.65836 25.1537C4.02142 22.4138 4.3629 18.2382 4.48212 14.4606C4.59611 10.8487 4.88107 7.11454 7.3129 4.41367C9.87059 1.57303 13.6448 -0.176818 17.4897 0.0139543Z"
|
||||
fill="#8AC4FA"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.6899 0.0139543C25.3881 0.197442 28.2513 2.81338 30.8928 5.37521C33.5677 7.96953 36.386 10.7639 36.5853 14.4606C36.7925 18.3029 34.7972 21.9638 31.9276 24.5676C29.1711 27.0687 25.4323 27.872 21.6899 27.9761C17.8096 28.084 13.5324 27.9319 10.8586 25.1537C8.22161 22.4138 8.5631 18.2382 8.68232 14.4606C8.7963 10.8487 9.08126 7.11454 11.5131 4.41367C14.0708 1.57303 17.845 -0.176818 21.6899 0.0139543Z"
|
||||
fill="#CAE4FD"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M25.8899 0.0139543C29.588 0.197442 32.4513 2.81338 35.0927 5.37521C37.7676 7.96953 40.586 10.7639 40.7853 14.4606C40.9925 18.3029 38.9971 21.9638 36.1275 24.5676C33.371 27.0687 29.6323 27.872 25.8899 27.9761C22.0096 28.084 17.7323 27.9319 15.0585 25.1537C12.4216 22.4138 12.763 18.2382 12.8823 14.4606C12.9963 10.8487 13.2812 7.11454 15.7131 4.41367C18.2707 1.57303 22.0449 -0.176818 25.8899 0.0139543Z"
|
||||
fill="white"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -9,3 +9,19 @@ export * from './WarningIcon';
|
||||
export * from './SearchIcon';
|
||||
export * from './CrossIcon';
|
||||
export * from './GlobeIcon';
|
||||
export * from './CalendarIcon';
|
||||
export * from './CheckRoundFilledIcon';
|
||||
export * from './InfoRoundFilledIcon';
|
||||
export * from './LoadingIcon';
|
||||
export * from './FolderIcon';
|
||||
export * from './SettingsSlidersIcon';
|
||||
export * from './LifeBuoyIcon';
|
||||
export * from './QuestionMarkRoundIcon';
|
||||
export * from './GitIcon';
|
||||
export * from './EllipseIcon';
|
||||
export * from './EllipsesIcon';
|
||||
export * from './SnowballIcon';
|
||||
export * from './NotificationBellIcon';
|
||||
export * from './PencilIcon';
|
||||
export * from './CheckRadioIcon';
|
||||
export * from './ChevronDownIcon';
|
||||
|
||||