Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b555812a | ||
|
|
6f64513fc8 | ||
|
|
e1c2a8ce2c | ||
|
|
134d4ad316 | ||
|
|
cdd2bd2d89 | ||
|
|
aa3fa5d573 | ||
|
|
7fbfdaf85e | ||
|
|
dce9a61e6c | ||
|
|
57599b4542 | ||
|
|
71fac7ffa2 | ||
|
|
05dd75a38c | ||
|
|
f2472f4ce9 | ||
|
|
fbfe2a47d6 | ||
|
|
c06d76ed74 | ||
|
|
4d7ccae0e5 | ||
|
|
52512beaa2 | ||
|
|
a1be980976 | ||
|
|
c4d3dc2fb5 | ||
|
|
9d7ab3abac | ||
|
|
8353c1ecbe | ||
|
|
7c1d1b5923 | ||
|
|
c33a4e5574 | ||
|
|
6c79ed37fa | ||
|
|
a51765dae5 | ||
|
|
534871a7ae | ||
|
|
17640d3133 | ||
|
|
bba0b57bed | ||
|
|
c34e66aa93 | ||
|
|
b61682dd20 | ||
|
|
bcb6ac241b | ||
|
|
4a9f517d85 | ||
|
|
6d1135645b | ||
|
|
e5748e768c | ||
|
|
9158e3a840 | ||
|
|
8bd1e17fd2 | ||
|
|
3998b60888 | ||
|
|
c5bace0660 | ||
|
|
a033a8a7b3 | ||
|
|
420080b1af | ||
|
|
f95a64546d | ||
|
|
3214cfc1d4 | ||
|
|
07b21a835e | ||
|
|
15ba278bbc | ||
|
|
364c62783d | ||
|
|
b9573474a8 | ||
|
|
ea9a56eb65 | ||
|
|
05bd766133 | ||
|
|
0f18bc978e | ||
|
|
519e318190 | ||
|
|
63969ae25a | ||
|
|
b449c299dc | ||
|
|
2a35ec1cd5 | ||
|
|
be90fc76c1 | ||
|
|
3fa60f3cdf | ||
|
|
3d9aedeb7e | ||
|
|
096318cf13 | ||
|
|
27ef859075 | ||
|
|
5152952a45 | ||
|
|
ef26f9b39e | ||
|
|
d486f44cfe | ||
|
|
5c9c7575f2 | ||
|
|
59a164f3f8 | ||
|
|
bc52b34462 | ||
|
|
5aefda1248 | ||
|
|
42bdd21089 | ||
|
|
e5a00016c1 | ||
|
|
13b912d318 | ||
|
|
059863c4b9 | ||
|
|
58906844cc | ||
|
|
9f0a2ad548 | ||
|
|
bd10e2cb35 | ||
|
|
eb32385cf3 | ||
|
|
aebb20b987 | ||
|
|
8a2b51952f | ||
|
|
94f46f9621 | ||
|
|
e751addcce | ||
|
|
c01f8fdabf | ||
|
|
2b60114dab | ||
|
|
9a1c0e8338 | ||
|
|
1b038476c7 | ||
|
|
4a78eb13f6 | ||
|
|
41bcb2e7d0 |
@@ -0,0 +1,61 @@
|
||||
name: Deploy Snowball frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY_USER_KEY: ${{ secrets.REGISTRY_USER_KEY }}
|
||||
REGISTRY_BOND_ID: ${{ secrets.REGISTRY_BOND_ID }}
|
||||
DEPLOYER_LRN: lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
|
||||
AUTHORITY: laconic-deploy
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Download yarn
|
||||
run: |
|
||||
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
|
||||
chmod +x /usr/local/bin/yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
- name: Set up environment
|
||||
run: |
|
||||
# Create a .env file with the necessary variables
|
||||
echo "REGISTRY_BOND_ID=$REGISTRY_BOND_ID" > packages/deployer/.env
|
||||
echo "DEPLOYER_LRN=$DEPLOYER_LRN" >> packages/deployer/.env
|
||||
echo "AUTHORITY=$AUTHORITY" >> packages/deployer/.env
|
||||
|
||||
# Create a config file with necessary endpoints and secrets
|
||||
cat > packages/deployer/config.yml <<EOF
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||
userKey: $REGISTRY_USER_KEY
|
||||
bondId: $REGISTRY_BOND_ID
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
EOF
|
||||
|
||||
- name: Run deploy script
|
||||
run: |
|
||||
cd packages/deployer
|
||||
./deploy-frontend.sh
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -14,6 +15,10 @@ jobs:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download yarn
|
||||
run: |
|
||||
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
|
||||
chmod +x /usr/local/bin/yarn
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Generate and Deploy Docs
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for proper branch detection
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Update TypeDoc config for current branch
|
||||
run: |
|
||||
CURRENT_BRANCH=${GITHUB_REF#refs/heads/}
|
||||
echo "Current branch: $CURRENT_BRANCH"
|
||||
# Update gitRevision in typedoc.json to use the current branch
|
||||
sed -i "s|\"gitRevision\": \"qrigin/[^\"]*\"|\"gitRevision\": \"qrigin/$CURRENT_BRANCH\"|" typedoc.mjs
|
||||
|
||||
- name: Generate documentation
|
||||
run: yarn typedoc
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
folder: docs
|
||||
clean: true
|
||||
token: ${{ secrets.ACTIONS_ONLY }} # Use the ACTION_ONLY token
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Test webapp deployment
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test_app_deployment:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Test webapp deployment
|
||||
run: ./packages/deployer/test/test-webapp-deployment-undeployment.sh
|
||||
- name: Notify Vulcanize Slack on CI failure
|
||||
if: ${{ always() && github.ref_name == 'main' }}
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: 'failure'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS_WEBHOOK }}
|
||||
- name: Notify DeepStack Slack on CI failure
|
||||
if: ${{ always() && github.ref_name == 'main' }}
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: 'failure'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS_WEBHOOK }}
|
||||
@@ -3,6 +3,7 @@ yarn-error.log
|
||||
.yarnrc.yml
|
||||
.yarn/
|
||||
.yarnrc
|
||||
.cursor
|
||||
|
||||
packages/backend/environments/local.toml
|
||||
packages/backend/dev/
|
||||
@@ -10,3 +11,8 @@ packages/frontend/dist/
|
||||
|
||||
# ignore all .DS_Store files
|
||||
**/.DS_Store
|
||||
.vscode
|
||||
|
||||
# TypeDoc generated documentation
|
||||
docs/
|
||||
.cursor/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
# Documentation Guide for Snowball Tools
|
||||
|
||||
This guide explains how to write and generate documentation for the Snowball Tools project.
|
||||
|
||||
## TSDoc and TypeDoc
|
||||
|
||||
We use [TSDoc](https://tsdoc.org/) for documenting our TypeScript code and [TypeDoc](https://typedoc.org/) for generating API documentation from those comments.
|
||||
|
||||
## Writing Documentation
|
||||
|
||||
### Basic Comment Structure
|
||||
|
||||
TSDoc comments start with `/**` and end with `*/`. Each line within the comment block typically starts with a `*`.
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* This is a TSDoc comment.
|
||||
*/
|
||||
```
|
||||
|
||||
### Documenting Functions
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Calculates the sum of two numbers.
|
||||
*
|
||||
* @param a - The first number
|
||||
* @param b - The second number
|
||||
* @returns The sum of a and b
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = add(1, 2);
|
||||
* console.log(result); // 3
|
||||
* ```
|
||||
*/
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
### Documenting Classes
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Represents a user in the system.
|
||||
*
|
||||
* @remarks
|
||||
* This class is used throughout the application to represent user data.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const user = new User('John', 'Doe');
|
||||
* console.log(user.fullName); // "John Doe"
|
||||
* ```
|
||||
*/
|
||||
class User {
|
||||
/**
|
||||
* Creates a new User instance.
|
||||
*
|
||||
* @param firstName - The user's first name
|
||||
* @param lastName - The user's last name
|
||||
*/
|
||||
constructor(
|
||||
/** The user's first name */
|
||||
public firstName: string,
|
||||
/** The user's last name */
|
||||
public lastName: string
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the user's full name.
|
||||
*/
|
||||
get fullName(): string {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Documenting Interfaces
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Configuration options for the application.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
interface AppConfig {
|
||||
/**
|
||||
* The port number the server should listen on.
|
||||
* @default 3000
|
||||
*/
|
||||
port: number;
|
||||
|
||||
/**
|
||||
* The host the server should bind to.
|
||||
* @default "localhost"
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* Whether to enable debug mode.
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Common TSDoc Tags
|
||||
|
||||
| Tag | Description |
|
||||
|-----|-------------|
|
||||
| `@param` | Documents a function parameter |
|
||||
| `@returns` | Documents the return value |
|
||||
| `@throws` | Documents exceptions that might be thrown |
|
||||
| `@example` | Provides an example of usage |
|
||||
| `@remarks` | Adds additional information |
|
||||
| `@deprecated` | Marks an item as deprecated |
|
||||
| `@see` | Refers to related documentation |
|
||||
| `@default` | Documents the default value |
|
||||
| `@public`, `@protected`, `@private` | Visibility modifiers |
|
||||
| `@internal` | Marks an item as internal (not part of the public API) |
|
||||
| `@beta` | Marks an item as in beta stage |
|
||||
| `@alpha` | Marks an item as in alpha stage |
|
||||
| `@experimental` | Marks an item as experimental |
|
||||
|
||||
## Generating Documentation
|
||||
|
||||
To generate documentation for the project, run:
|
||||
|
||||
```bash
|
||||
yarn docs
|
||||
```
|
||||
|
||||
This will create a `docs` directory with the generated documentation.
|
||||
|
||||
To watch for changes and regenerate documentation automatically:
|
||||
|
||||
```bash
|
||||
yarn docs:watch
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Document Public APIs**: Always document public APIs thoroughly.
|
||||
2. **Include Examples**: Provide examples for complex functions or classes.
|
||||
3. **Be Concise**: Keep documentation clear and to the point.
|
||||
4. **Use Proper Grammar**: Use proper grammar and punctuation.
|
||||
5. **Update Documentation**: Keep documentation in sync with code changes.
|
||||
6. **Document Parameters**: Document all parameters, including their types and purpose.
|
||||
7. **Document Return Values**: Document what a function returns.
|
||||
8. **Document Exceptions**: Document any exceptions that might be thrown.
|
||||
|
||||
## Example Files
|
||||
|
||||
For reference, check out these example files that demonstrate proper TSDoc usage:
|
||||
|
||||
- `packages/backend/src/utils/tsdoc-example.ts`
|
||||
- `packages/frontend/src/utils/tsdoc-example.ts`
|
||||
|
||||
## Resources
|
||||
|
||||
- [TSDoc Official Documentation](https://tsdoc.org/)
|
||||
- [TypeDoc Official Documentation](https://typedoc.org/)
|
||||
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
||||
@@ -1,23 +0,0 @@
|
||||
# snowballtools-base
|
||||
|
||||
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install dependencies
|
||||
|
||||
In the root of the project, run:
|
||||
|
||||
```zsh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Build backend
|
||||
|
||||
```zsh
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
### Environment variables, running the development server, and deployment
|
||||
|
||||
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.
|
||||
@@ -14,17 +14,17 @@ VITE_SERVER_URL = 'LACONIC_HOSTED_CONFIG_server_url'
|
||||
VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
||||
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_next_app_templaterepo'
|
||||
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
|
||||
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
|
||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
||||
VITE_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
|
||||
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
||||
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
||||
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
||||
VITE_TURNKEY_ORGANIZATION_ID = 'LACONIC_HOSTED_CONFIG_turnkey_organization_id'
|
||||
EOF
|
||||
|
||||
yarn || exit 1
|
||||
yarn build || exit 1
|
||||
yarn build --ignore backend || exit 1
|
||||
|
||||
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Snowball Tools API Documentation
|
||||
|
||||
This documentation is automatically generated using [TypeDoc](https://typedoc.org/) from TSDoc comments in the codebase.
|
||||
|
||||
## Packages
|
||||
|
||||
The monorepo contains the following packages:
|
||||
|
||||
- **frontend**: The frontend web application
|
||||
- **backend**: The backend API server
|
||||
- **gql-client**: GraphQL client library
|
||||
- **deployer**: Deployment utilities
|
||||
|
||||
## How to Use This Documentation
|
||||
|
||||
The documentation is organized by packages and modules. You can navigate through the sidebar to explore the different parts of the codebase.
|
||||
|
||||
## Contributing to Documentation
|
||||
|
||||
To contribute to the documentation:
|
||||
|
||||
1. Add TSDoc comments to your code using the JSDoc syntax
|
||||
2. Run `yarn docs` to generate the documentation
|
||||
3. Preview the documentation in the `docs` directory
|
||||
|
||||
## TSDoc Comment Examples
|
||||
|
||||
### Function Documentation
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Calculates the sum of two numbers.
|
||||
*
|
||||
* @param a - The first number
|
||||
* @param b - The second number
|
||||
* @returns The sum of a and b
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = add(1, 2);
|
||||
* console.log(result); // 3
|
||||
* ```
|
||||
*/
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
### Class Documentation
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Represents a user in the system.
|
||||
*
|
||||
* @remarks
|
||||
* This class is used throughout the application to represent user data.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const user = new User('John', 'Doe');
|
||||
* console.log(user.fullName); // "John Doe"
|
||||
* ```
|
||||
*/
|
||||
class User {
|
||||
/**
|
||||
* Creates a new User instance.
|
||||
*
|
||||
* @param firstName - The user's first name
|
||||
* @param lastName - The user's last name
|
||||
*/
|
||||
constructor(
|
||||
/** The user's first name */
|
||||
public firstName: string,
|
||||
/** The user's last name */
|
||||
public lastName: string
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the user's full name.
|
||||
*/
|
||||
get fullName(): string {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interface Documentation
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Configuration options for the application.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
interface AppConfig {
|
||||
/**
|
||||
* The port number the server should listen on.
|
||||
* @default 3000
|
||||
*/
|
||||
port: number;
|
||||
|
||||
/**
|
||||
* The host the server should bind to.
|
||||
* @default "localhost"
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* Whether to enable debug mode.
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean;
|
||||
}
|
||||
```
|
||||
@@ -5,14 +5,30 @@
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@microsoft/tsdoc": "^0.15.1",
|
||||
"chalk": "^4.1.2",
|
||||
"concurrently": "^8.2.0",
|
||||
"depcheck": "^1.4.2",
|
||||
"husky": "^8.0.3",
|
||||
"lerna": "^8.0.0",
|
||||
"patch-package": "^8.0.0"
|
||||
"lerna": "^8.2.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"tsdoc": "^0.0.4",
|
||||
"typedoc": "^0.27.9",
|
||||
"typedoc-plugin-markdown": "^4.4.2",
|
||||
"typedoc-unhoax-theme": "^0.4.6"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"build": "lerna run build --stream",
|
||||
"lint": "lerna run lint --stream"
|
||||
}
|
||||
}
|
||||
"lint": "lerna run lint --stream",
|
||||
"start": "yarn kill:ports && yarn dev",
|
||||
"dev": "yarn && yarn build --ignore frontend && concurrently --names \"BACKEND,FRONTEND\" --prefix-colors \"blue.bold,green.bold\" --prefix \"[{name}]\" \"yarn start:backend\" \"yarn start:frontend\"",
|
||||
"start:backend": "yarn workspace backend start",
|
||||
"start:frontend": "yarn workspace frontend dev",
|
||||
"kill:ports": "node scripts/kill-ports.js",
|
||||
"docs": "yarn typedoc",
|
||||
"docs:watch": "yarn typedoc --watch"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ yarn build --ignore frontend
|
||||
|
||||
#### Local
|
||||
|
||||
Copy the `envionments/local.toml.example` file to `envionments/local.toml`:
|
||||
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
||||
|
||||
```zsh
|
||||
cp envionments/local.toml.example envionments/local.toml
|
||||
cp environments/local.toml.example environments/local.toml
|
||||
```
|
||||
|
||||
#### Staging environment variables
|
||||
@@ -44,7 +44,7 @@ yarn start
|
||||
|
||||
## Deployment
|
||||
|
||||
Clone the deployer repository:
|
||||
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
||||
|
||||
```zsh
|
||||
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
||||
secret = ""
|
||||
# Frontend webapp URL origin
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
@@ -19,6 +22,7 @@
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
gqlEndpoint = "http://localhost:9473/api"
|
||||
chainId = "laconic_9000-1"
|
||||
@@ -26,9 +30,14 @@
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
amount = "200000"
|
||||
denom = "aphoton"
|
||||
gas = "750000"
|
||||
gas = ""
|
||||
fees = ""
|
||||
gasPrice = "1alnt"
|
||||
|
||||
[misc]
|
||||
projectDomain = "apps.snowballtools.com"
|
||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||
[auction]
|
||||
commitFee = "100000"
|
||||
commitsDuration = "120s"
|
||||
revealFee = "100000"
|
||||
revealsDuration = "120s"
|
||||
denom = "alnt"
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.11",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
"@snowballtools/laconic-sdk": "^0.1.17",
|
||||
"@turnkey/sdk-server": "^0.1.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
@@ -27,6 +27,7 @@
|
||||
"nanoid": "3",
|
||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||
"octokit": "^3.1.2",
|
||||
"openpgp": "^6.0.1",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"semver": "^7.6.0",
|
||||
"toml": "^3.0.0",
|
||||
@@ -53,7 +54,7 @@
|
||||
"@types/cookie-session": "^2.0.49",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"better-sqlite3": "^9.4.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"prettier": "^3.1.1",
|
||||
"workspace": "^0.0.1-preview.1"
|
||||
|
||||
@@ -34,16 +34,21 @@ export interface RegistryConfig {
|
||||
privateKey: string;
|
||||
bondId: string;
|
||||
fetchDeploymentRecordDelay: number;
|
||||
checkAuctionStatusDelay: number;
|
||||
authority: string;
|
||||
fee: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
gas: string;
|
||||
fees: string;
|
||||
gasPrice: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MiscConfig {
|
||||
projectDomain: string;
|
||||
export interface AuctionConfig {
|
||||
commitFee: string;
|
||||
commitsDuration: string;
|
||||
revealFee: string;
|
||||
revealsDuration: string;
|
||||
denom: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@@ -51,7 +56,7 @@ export interface Config {
|
||||
database: DatabaseConfig;
|
||||
gitHub: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
misc: MiscConfig;
|
||||
auction: AuctionConfig;
|
||||
turnkey: {
|
||||
apiBaseUrl: string;
|
||||
apiPublicKey: string;
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
DeepPartial,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
FindOptionsWhere
|
||||
FindOptionsWhere,
|
||||
IsNull,
|
||||
Not
|
||||
} from 'typeorm';
|
||||
import path from 'path';
|
||||
import debug from 'debug';
|
||||
@@ -11,16 +13,18 @@ import assert from 'assert';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||
|
||||
import { DatabaseConfig, MiscConfig } from './config';
|
||||
import { DatabaseConfig } from './config';
|
||||
import { User } from './entity/User';
|
||||
import { Organization } from './entity/Organization';
|
||||
import { Project } from './entity/Project';
|
||||
import { Deployment } from './entity/Deployment';
|
||||
import { Deployment, DeploymentStatus } from './entity/Deployment';
|
||||
import { ProjectMember } from './entity/ProjectMember';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { getEntities, loadAndSaveData } from './utils';
|
||||
import { UserOrganization } from './entity/UserOrganization';
|
||||
import { Deployer } from './entity/Deployer';
|
||||
import { DNSRecordAttributes } from './types';
|
||||
|
||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||
|
||||
@@ -31,9 +35,8 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
||||
// TODO: Fix order of methods
|
||||
export class Database {
|
||||
private dataSource: DataSource;
|
||||
private projectDomain: string;
|
||||
|
||||
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
||||
constructor({ dbPath }: DatabaseConfig) {
|
||||
this.dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: dbPath,
|
||||
@@ -41,38 +44,49 @@ export class Database {
|
||||
synchronize: true,
|
||||
logging: false
|
||||
});
|
||||
|
||||
this.projectDomain = projectDomain;
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
async init(): Promise<void> {
|
||||
await this.dataSource.initialize();
|
||||
log('database initialized');
|
||||
|
||||
const organizations = await this.getOrganizations({});
|
||||
let organizations = await this.getOrganizations({});
|
||||
|
||||
// Load an organization if none exist
|
||||
if (!organizations.length) {
|
||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
||||
organizations = await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
||||
}
|
||||
|
||||
// Hotfix for updating old DB data
|
||||
if (organizations[0].slug === 'snowball-tools-1') {
|
||||
const [orgEntity] = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
|
||||
await this.updateOrganization(
|
||||
organizations[0].id,
|
||||
{
|
||||
slug: orgEntity.slug as string,
|
||||
name: orgEntity.name as string
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
||||
async getUser(options: FindOneOptions<User>): Promise<User | null> {
|
||||
const userRepository = this.dataSource.getRepository(User);
|
||||
const user = await userRepository.findOne(options);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async addUser (data: DeepPartial<User>): Promise<User> {
|
||||
async addUser(data: DeepPartial<User>): Promise<User> {
|
||||
const userRepository = this.dataSource.getRepository(User);
|
||||
const user = await userRepository.save(data);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser (user: User, 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: user.id }, data);
|
||||
assert(updateResult.affected);
|
||||
@@ -80,7 +94,7 @@ export class Database {
|
||||
return updateResult.affected > 0;
|
||||
}
|
||||
|
||||
async getOrganizations (
|
||||
async getOrganizations(
|
||||
options: FindManyOptions<Organization>
|
||||
): Promise<Organization[]> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
@@ -89,7 +103,7 @@ export class Database {
|
||||
return organizations;
|
||||
}
|
||||
|
||||
async getOrganization (
|
||||
async getOrganization(
|
||||
options: FindOneOptions<Organization>
|
||||
): Promise<Organization | null> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
@@ -98,7 +112,7 @@ export class Database {
|
||||
return organization;
|
||||
}
|
||||
|
||||
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
||||
async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
|
||||
const userOrgs = await organizationRepository.find({
|
||||
@@ -114,21 +128,29 @@ export class Database {
|
||||
return userOrgs;
|
||||
}
|
||||
|
||||
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||
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[]> {
|
||||
async updateOrganization(organizationId: string, data: DeepPartial<Organization>): Promise<boolean> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
const updateResult = await organizationRepository.update({ id: organizationId }, data);
|
||||
assert(updateResult.affected);
|
||||
|
||||
return updateResult.affected > 0;
|
||||
}
|
||||
|
||||
async getProjects(options: FindManyOptions<Project>): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const projects = await projectRepository.find(options);
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getProjectById (projectId: string): Promise<Project | null> {
|
||||
async getProjectById(projectId: string): Promise<Project | null> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
const project = await projectRepository
|
||||
@@ -136,11 +158,12 @@ export class Database {
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
'deployments.isCurrent = true AND deployments.isCanonical = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoinAndSelect('deployments.deployer', 'deployer')
|
||||
.leftJoinAndSelect('project.owner', 'owner')
|
||||
.leftJoinAndSelect('project.deployers', 'deployers')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.where('project.id = :projectId', {
|
||||
projectId
|
||||
@@ -150,7 +173,25 @@ export class Database {
|
||||
return project;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization (
|
||||
async allProjectsWithoutDeployments(): Promise<Project[]> {
|
||||
const allProjects = await this.getProjects({
|
||||
where: {
|
||||
auctionId: Not(IsNull()),
|
||||
},
|
||||
relations: ['deployments'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const projects = allProjects.filter(project => {
|
||||
if (project.deletedAt !== null) return false;
|
||||
|
||||
return project.deployments.length === 0;
|
||||
});
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization(
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
): Promise<Project[]> {
|
||||
@@ -161,9 +202,8 @@ export class Database {
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
'deployments.isCurrent = true AND deployments.isCanonical = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.leftJoin('project.organization', 'organization')
|
||||
.where(
|
||||
@@ -181,7 +221,7 @@ export class Database {
|
||||
/**
|
||||
* Get deployments with specified filter
|
||||
*/
|
||||
async getDeployments (
|
||||
async getDeployments(
|
||||
options: FindManyOptions<Deployment>
|
||||
): Promise<Deployment[]> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
@@ -190,12 +230,12 @@ export class Database {
|
||||
return deployments;
|
||||
}
|
||||
|
||||
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
||||
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
||||
return this.getDeployments({
|
||||
relations: {
|
||||
project: true,
|
||||
domain: true,
|
||||
createdBy: true
|
||||
createdBy: true,
|
||||
deployer: true,
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
@@ -208,7 +248,26 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
async getDeployment (
|
||||
async getNonCanonicalDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
||||
return this.getDeployments({
|
||||
relations: {
|
||||
project: true,
|
||||
createdBy: true,
|
||||
deployer: true,
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
isCanonical: false
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getDeployment(
|
||||
options: FindOneOptions<Deployment>
|
||||
): Promise<Deployment | null> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
@@ -217,14 +276,14 @@ export class Database {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const domains = await domainRepository.find(options);
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
|
||||
const id = nanoid();
|
||||
@@ -238,7 +297,7 @@ export class Database {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId (
|
||||
async getProjectMembersByProjectId(
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository =
|
||||
@@ -259,7 +318,7 @@ export class Database {
|
||||
return projectMembers;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
async getEnvironmentVariablesByProjectId(
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
@@ -278,7 +337,7 @@ export class Database {
|
||||
return environmentVariables;
|
||||
}
|
||||
|
||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||
async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
@@ -293,7 +352,7 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async updateProjectMemberById (
|
||||
async updateProjectMemberById(
|
||||
projectMemberId: string,
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<boolean> {
|
||||
@@ -307,7 +366,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProjectMember (
|
||||
async addProjectMember(
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<ProjectMember> {
|
||||
const projectMemberRepository =
|
||||
@@ -317,7 +376,7 @@ export class Database {
|
||||
return newProjectMember;
|
||||
}
|
||||
|
||||
async addEnvironmentVariables (
|
||||
async addEnvironmentVariables(
|
||||
data: DeepPartial<EnvironmentVariable>[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
@@ -328,7 +387,7 @@ export class Database {
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable (
|
||||
async updateEnvironmentVariable(
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
@@ -342,7 +401,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteEnvironmentVariable (
|
||||
async deleteEnvironmentVariable(
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
@@ -358,7 +417,7 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||
async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
@@ -381,7 +440,7 @@ export class Database {
|
||||
return projectMemberWithProject[0];
|
||||
}
|
||||
|
||||
async getProjectsBySearchText (
|
||||
async getProjectsBySearchText(
|
||||
userId: string,
|
||||
searchText: string
|
||||
): Promise<Project[]> {
|
||||
@@ -403,14 +462,14 @@ export class Database {
|
||||
return projects;
|
||||
}
|
||||
|
||||
async updateDeploymentById (
|
||||
async updateDeploymentById(
|
||||
deploymentId: string,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
return this.updateDeployment({ id: deploymentId }, data);
|
||||
}
|
||||
|
||||
async updateDeployment (
|
||||
async updateDeployment(
|
||||
criteria: FindOptionsWhere<Deployment>,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
@@ -420,7 +479,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async updateDeploymentsByProjectIds (
|
||||
async updateDeploymentsByProjectIds(
|
||||
projectIds: string[],
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
@@ -436,7 +495,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteDeploymentById (deploymentId: string): Promise<boolean> {
|
||||
async deleteDeploymentById(deploymentId: string): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const deployment = await deploymentRepository.findOneOrFail({
|
||||
where: {
|
||||
@@ -449,7 +508,7 @@ export class Database {
|
||||
return Boolean(deleteResult);
|
||||
}
|
||||
|
||||
async addProject (user: User, 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
|
||||
@@ -465,12 +524,16 @@ export class Database {
|
||||
id: organizationId
|
||||
});
|
||||
|
||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
||||
|
||||
return projectRepository.save(newProject);
|
||||
}
|
||||
|
||||
async updateProjectById (
|
||||
async saveProject(project: Project): Promise<Project> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
return projectRepository.save(project);
|
||||
}
|
||||
|
||||
async updateProjectById(
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<boolean> {
|
||||
@@ -483,7 +546,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteProjectById (projectId: string): Promise<boolean> {
|
||||
async deleteProjectById(projectId: string): Promise<boolean> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const project = await projectRepository.findOneOrFail({
|
||||
where: {
|
||||
@@ -499,7 +562,7 @@ export class Database {
|
||||
return Boolean(deleteResult);
|
||||
}
|
||||
|
||||
async deleteDomainById (domainId: string): Promise<boolean> {
|
||||
async deleteDomainById(domainId: string): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
|
||||
const deleteResult = await domainRepository.softDelete({ id: domainId });
|
||||
@@ -511,21 +574,21 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async addDomain (data: DeepPartial<Domain>): Promise<Domain> {
|
||||
async addDomain(data: DeepPartial<Domain>): Promise<Domain> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const newDomain = await domainRepository.save(data);
|
||||
|
||||
return newDomain;
|
||||
}
|
||||
|
||||
async getDomain (options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||
async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const domain = await domainRepository.findOne(options);
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
async updateDomainById (
|
||||
async updateDomainById(
|
||||
domainId: string,
|
||||
data: DeepPartial<Domain>
|
||||
): Promise<boolean> {
|
||||
@@ -535,7 +598,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (
|
||||
async getDomainsByProjectId(
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<Domain>
|
||||
): Promise<Domain[]> {
|
||||
@@ -555,4 +618,67 @@ export class Database {
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
async getOldestDomainByProjectId(
|
||||
projectId: string,
|
||||
): Promise<Domain | null> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
|
||||
const domain = await domainRepository.findOne({
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
},
|
||||
order: {
|
||||
createdAt: 'ASC'
|
||||
}
|
||||
});
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
async getLatestDNSRecordByProjectId(
|
||||
projectId: string,
|
||||
): Promise<DNSRecordAttributes | null> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
|
||||
const deployment = await deploymentRepository.findOne({
|
||||
where: {
|
||||
project: {
|
||||
id: projectId,
|
||||
},
|
||||
status: DeploymentStatus.Ready,
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
});
|
||||
|
||||
if (deployment === null) {
|
||||
throw new Error(`No deployment found for project ${projectId}`);
|
||||
}
|
||||
|
||||
return deployment.dnsRecordData;
|
||||
}
|
||||
|
||||
async addDeployer(data: DeepPartial<Deployer>): Promise<Deployer> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||
const newDomain = await deployerRepository.save(data);
|
||||
|
||||
return newDomain;
|
||||
}
|
||||
|
||||
async getDeployers(): Promise<Deployer[]> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||
const deployers = await deployerRepository.find();
|
||||
return deployers;
|
||||
}
|
||||
|
||||
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||
const deployer = await deployerRepository.findOne({ where: { deployerLrn } });
|
||||
|
||||
return deployer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Entity, PrimaryColumn, Column, ManyToMany } from 'typeorm';
|
||||
import { Project } from './Project';
|
||||
|
||||
@Entity()
|
||||
export class Deployer {
|
||||
@PrimaryColumn('varchar')
|
||||
deployerLrn!: string;
|
||||
|
||||
@Column('varchar')
|
||||
deployerId!: string;
|
||||
|
||||
@Column('varchar')
|
||||
deployerApiUrl!: string;
|
||||
|
||||
@Column('varchar')
|
||||
baseDomain!: string;
|
||||
|
||||
@Column('varchar', { nullable: true})
|
||||
publicKey!: string | null;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
minimumPayment!: string | null;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
paymentAddress!: string | null;
|
||||
|
||||
@ManyToMany(() => Project, (project) => project.deployers)
|
||||
projects!: Project[];
|
||||
}
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
import { Project } from './Project';
|
||||
import { Domain } from './Domain';
|
||||
import { User } from './User';
|
||||
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
||||
import { Deployer } from './Deployer';
|
||||
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes, DNSRecordAttributes } from '../types';
|
||||
|
||||
export enum Environment {
|
||||
Production = 'Production',
|
||||
@@ -33,21 +34,22 @@ export interface ApplicationDeploymentRequest {
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
lrn?: string;
|
||||
auction?: string;
|
||||
config: string;
|
||||
meta: string;
|
||||
payment?: string;
|
||||
dns?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRemovalRequest {
|
||||
type: string;
|
||||
version: string;
|
||||
deployment: string;
|
||||
auction?: string;
|
||||
payment?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRemovalRequest {
|
||||
type: string;
|
||||
version: string;
|
||||
deployment: string;
|
||||
}
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string;
|
||||
@@ -76,13 +78,6 @@ export class Deployment {
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project;
|
||||
|
||||
@Column({ nullable: true })
|
||||
domainId!: string | null;
|
||||
|
||||
@OneToOne(() => Domain)
|
||||
@JoinColumn({ name: 'domainId' })
|
||||
domain!: Domain | null;
|
||||
|
||||
@Column('varchar')
|
||||
branch!: string;
|
||||
|
||||
@@ -112,19 +107,26 @@ export class Deployment {
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
||||
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRequestId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
|
||||
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRecordId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
dnsRecordData!: DNSRecordAttributes | null;
|
||||
|
||||
@ManyToOne(() => Deployer)
|
||||
@JoinColumn({ name: 'deployerLrn' })
|
||||
deployer!: Deployer;
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
@@ -133,6 +135,9 @@ export class Deployment {
|
||||
@Column('boolean', { default: false })
|
||||
isCurrent!: boolean;
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isCanonical!: boolean;
|
||||
|
||||
@Column({
|
||||
enum: DeploymentStatus
|
||||
})
|
||||
@@ -147,7 +152,7 @@ export class Deployment {
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,16 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
DeleteDateColumn
|
||||
DeleteDateColumn,
|
||||
JoinTable,
|
||||
ManyToMany
|
||||
} from 'typeorm';
|
||||
|
||||
import { User } from './User';
|
||||
import { Organization } from './Organization';
|
||||
import { ProjectMember } from './ProjectMember';
|
||||
import { Deployment } from './Deployment';
|
||||
import { Deployer } from './Deployer';
|
||||
|
||||
@Entity()
|
||||
export class Project {
|
||||
@@ -46,6 +49,20 @@ export class Project {
|
||||
@Column('text', { default: '' })
|
||||
description!: string;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
auctionId!: string | null;
|
||||
|
||||
// Tx hash for sending coins from snowball to deployer
|
||||
@Column('varchar', { nullable: true })
|
||||
txHash!: string | null;
|
||||
|
||||
@ManyToMany(() => Deployer, (deployer) => (deployer.projects))
|
||||
@JoinTable()
|
||||
deployers!: Deployer[]
|
||||
|
||||
@Column('boolean', { default: false, nullable: true })
|
||||
fundsReleased!: boolean;
|
||||
|
||||
// TODO: Compute template & framework in import repository
|
||||
@Column('varchar', { nullable: true })
|
||||
template!: string | null;
|
||||
@@ -53,6 +70,10 @@ export class Project {
|
||||
@Column('varchar', { nullable: true })
|
||||
framework!: string | null;
|
||||
|
||||
// Address of the user who created the project i.e. requested deployments
|
||||
@Column('varchar')
|
||||
paymentAddress!: string;
|
||||
|
||||
@Column({
|
||||
type: 'simple-array'
|
||||
})
|
||||
@@ -61,9 +82,6 @@ export class Project {
|
||||
@Column('varchar')
|
||||
icon!: string;
|
||||
|
||||
@Column('varchar')
|
||||
subDomain!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const log = debug('snowball:server');
|
||||
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||
|
||||
export const main = async (): Promise<void> => {
|
||||
const { server, database, gitHub, registryConfig, misc } = await getConfig();
|
||||
const { server, database, gitHub, registryConfig } = await getConfig();
|
||||
|
||||
const app = new OAuthApp({
|
||||
clientType: OAUTH_CLIENT_TYPE,
|
||||
@@ -25,7 +25,7 @@ export const main = async (): Promise<void> => {
|
||||
clientSecret: gitHub.oAuth.clientSecret,
|
||||
});
|
||||
|
||||
const db = new Database(database, misc);
|
||||
const db = new Database(database);
|
||||
await db.init();
|
||||
|
||||
const registry = new Registry(registryConfig);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import debug from 'debug';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Octokit } from 'octokit';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import * as openpgp from 'openpgp';
|
||||
|
||||
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
|
||||
import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import {
|
||||
@@ -12,49 +16,53 @@ import {
|
||||
ApplicationDeploymentRequest,
|
||||
ApplicationDeploymentRemovalRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
||||
import { sleep } from './utils';
|
||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord, RegistryRecord } from './types';
|
||||
import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
||||
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer';
|
||||
const SLEEP_DURATION = 1000;
|
||||
|
||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||
// TODO: Move registry code to registry-sdk/watcher-ts
|
||||
export class Registry {
|
||||
private registry: LaconicRegistry;
|
||||
private registryConfig: RegistryConfig;
|
||||
|
||||
constructor (registryConfig: RegistryConfig) {
|
||||
constructor(registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig;
|
||||
|
||||
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
|
||||
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
registryConfig.chainId
|
||||
{ chainId: registryConfig.chainId, gasPrice }
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationRecord ({
|
||||
appName,
|
||||
packageJSON,
|
||||
async createApplicationRecord({
|
||||
octokit,
|
||||
repository,
|
||||
commitHash,
|
||||
appType,
|
||||
repoUrl
|
||||
}: {
|
||||
appName: string;
|
||||
packageJSON: PackageJSON;
|
||||
octokit: Octokit
|
||||
repository: string;
|
||||
commitHash: string;
|
||||
appType: string;
|
||||
repoUrl: string;
|
||||
}): Promise<{
|
||||
applicationRecordId: string;
|
||||
applicationRecordData: ApplicationRecord;
|
||||
}> {
|
||||
// Use laconic-sdk to publish record
|
||||
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
|
||||
// Use registry-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords(
|
||||
@@ -87,7 +95,7 @@ export class Registry {
|
||||
repository_ref: commitHash,
|
||||
repository: [repoUrl],
|
||||
app_type: appType,
|
||||
name: appName,
|
||||
name: repo,
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
@@ -100,68 +108,151 @@ export class Registry {
|
||||
...(packageJSON.version && { app_version: packageJSON.version })
|
||||
};
|
||||
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationRecord,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
);
|
||||
const result = await this.publishRecord(applicationRecord);
|
||||
|
||||
log(`Published application record ${result.id}`);
|
||||
log('Application record data:', applicationRecord);
|
||||
|
||||
// TODO: Discuss computation of CRN
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
// TODO: Discuss computation of LRN
|
||||
const lrn = this.getLrn(repo);
|
||||
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{
|
||||
cid: result.data.id,
|
||||
crn: `${crn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
applicationRecordId: result.data.id,
|
||||
applicationRecordId: result.id,
|
||||
applicationRecordData: applicationRecord
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
async createApplicationDeploymentAuction(
|
||||
appName: string,
|
||||
octokit: Octokit,
|
||||
auctionParams: AuctionParams,
|
||||
data: DeepPartial<Deployment>,
|
||||
): Promise<{
|
||||
applicationDeploymentAuctionId: string;
|
||||
}> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
|
||||
await this.createApplicationRecord({
|
||||
octokit,
|
||||
repository: data.project.repository,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
});
|
||||
|
||||
const lrn = this.getLrn(appName);
|
||||
const config = await getConfig();
|
||||
const auctionConfig = config.auction;
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const auctionResult = await registryTransactionWithRetry(() =>
|
||||
this.registry.createProviderAuction(
|
||||
{
|
||||
commitFee: auctionConfig.commitFee,
|
||||
commitsDuration: auctionConfig.commitsDuration,
|
||||
revealFee: auctionConfig.revealFee,
|
||||
revealsDuration: auctionConfig.revealsDuration,
|
||||
denom: auctionConfig.denom,
|
||||
maxPrice: auctionParams.maxPrice,
|
||||
numProviders: auctionParams.numProviders,
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
if (!auctionResult.auction) {
|
||||
throw new Error('Error creating auction');
|
||||
}
|
||||
|
||||
// Create record of type applicationDeploymentAuction and publish
|
||||
const applicationDeploymentAuction = {
|
||||
application: lrn,
|
||||
auction: auctionResult.auction.id,
|
||||
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
||||
};
|
||||
|
||||
const result = await this.publishRecord(applicationDeploymentAuction);
|
||||
|
||||
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
||||
log(`Application deployment auction record published: ${result.id}`);
|
||||
log('Application deployment auction data:', applicationDeploymentAuction);
|
||||
|
||||
return {
|
||||
applicationDeploymentAuctionId: auctionResult.auction.id,
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest(data: {
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
repository: string,
|
||||
auctionId?: string | null,
|
||||
lrn: string,
|
||||
apiUrl: string,
|
||||
environmentVariables: { [key: string]: string },
|
||||
dns: string,
|
||||
requesterAddress: string,
|
||||
publicKey: string,
|
||||
payment?: string | null
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
}> {
|
||||
const crn = this.getCrn(data.appName);
|
||||
const records = await this.registry.resolveNames([crn]);
|
||||
const lrn = this.getLrn(data.appName);
|
||||
const records = await this.registry.resolveNames([lrn]);
|
||||
const applicationRecord = records[0];
|
||||
|
||||
if (!applicationRecord) {
|
||||
throw new Error(`No record found for ${crn}`);
|
||||
throw new Error(`No record found for ${lrn}`);
|
||||
}
|
||||
|
||||
let hash;
|
||||
if (Object.keys(data.environmentVariables).length !== 0) {
|
||||
hash = await this.generateConfigHash(
|
||||
data.environmentVariables,
|
||||
data.requesterAddress,
|
||||
data.publicKey,
|
||||
data.apiUrl,
|
||||
);
|
||||
}
|
||||
|
||||
// Create record of type ApplicationDeploymentRequest and publish
|
||||
@@ -169,48 +260,86 @@ export class Registry {
|
||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${lrn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: data.dns,
|
||||
|
||||
// TODO: Not set in test-progressive-web-app CI
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||
|
||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||
config: JSON.stringify({
|
||||
env: data.environmentVariables
|
||||
}),
|
||||
config: JSON.stringify(hash ? { ref: hash } : {}),
|
||||
meta: JSON.stringify({
|
||||
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
||||
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.deployment.commitHash
|
||||
})
|
||||
}),
|
||||
deployer: data.lrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment }),
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
);
|
||||
log(`Application deployment request record published: ${result.data.id}`);
|
||||
|
||||
const result = await this.publishRecord(applicationDeploymentRequest);
|
||||
|
||||
log(`Application deployment request record published: ${result.id}`);
|
||||
log('Application deployment request data:', applicationDeploymentRequest);
|
||||
|
||||
return {
|
||||
applicationDeploymentRequestId: result.data.id,
|
||||
applicationDeploymentRequestId: result.id,
|
||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||
};
|
||||
}
|
||||
|
||||
async getAuctionWinningDeployerRecords(
|
||||
auctionId: string
|
||||
): Promise<DeployerRecord[]> {
|
||||
const records = await this.registry.getAuctionsByIds([auctionId]);
|
||||
const auctionResult = records[0];
|
||||
|
||||
let deployerRecords = [];
|
||||
const { winnerAddresses } = auctionResult;
|
||||
|
||||
for (const auctionWinner of winnerAddresses) {
|
||||
const records = await this.getDeployerRecordsByFilter({
|
||||
paymentAddress: auctionWinner,
|
||||
});
|
||||
|
||||
const newRecords = records.filter(record => {
|
||||
return record.names !== null && record.names.length > 0;
|
||||
});
|
||||
|
||||
for (const record of newRecords) {
|
||||
if (record.id) {
|
||||
deployerRecords.push(record);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deployerRecords;
|
||||
}
|
||||
|
||||
async releaseDeployerFunds(
|
||||
auctionId: string
|
||||
): Promise<any> {
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const auction = await registryTransactionWithRetry(() =>
|
||||
this.registry.releaseFunds(
|
||||
{
|
||||
auctionId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
return auction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords for deployments
|
||||
*/
|
||||
async getDeploymentRecords (
|
||||
async getDeploymentRecords(
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRecord[]> {
|
||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||
@@ -222,20 +351,32 @@ export class Registry {
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||
// Filter records with ApplicationDeploymentRequestId ID
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationRecordId === record.attributes.application &&
|
||||
record.attributes.url.includes(deployment.id)
|
||||
deployment.applicationDeploymentRequestId === record.attributes.request
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch WebappDeployer Records by filter
|
||||
*/
|
||||
async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise<DeployerRecord[]> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: WEBAPP_DEPLOYER_RECORD_TYPE,
|
||||
...filter
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords by filter
|
||||
*/
|
||||
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||
@@ -248,7 +389,7 @@ export class Registry {
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||
*/
|
||||
async getDeploymentRemovalRecords (
|
||||
async getDeploymentRemovalRecords(
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRemovalRecord[]> {
|
||||
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||
@@ -269,8 +410,19 @@ export class Registry {
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRemovalRequest (data: {
|
||||
/**
|
||||
* Fetch record by Id
|
||||
*/
|
||||
async getRecordById(id: string): Promise<RegistryRecord | null> {
|
||||
const [record] = await this.registry.getRecordsByIds([id]);
|
||||
return record ?? null;
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRemovalRequest(data: {
|
||||
deploymentId: string;
|
||||
deployerLrn: string;
|
||||
auctionId?: string | null;
|
||||
payment?: string | null;
|
||||
}): Promise<{
|
||||
applicationDeploymentRemovalRequestId: string;
|
||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||
@@ -278,30 +430,143 @@ export class Registry {
|
||||
const applicationDeploymentRemovalRequest = {
|
||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
deployment: data.deploymentId
|
||||
deployment: data.deploymentId,
|
||||
deployer: data.deployerLrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment }),
|
||||
};
|
||||
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRemovalRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
const result = await this.publishRecord(
|
||||
applicationDeploymentRemovalRequest,
|
||||
);
|
||||
|
||||
log(`Application deployment removal request record published: ${result.data.id}`);
|
||||
log(`Application deployment removal request record published: ${result.id}`);
|
||||
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
||||
|
||||
return {
|
||||
applicationDeploymentRemovalRequestId: result.data.id,
|
||||
applicationDeploymentRemovalRequestId: result.id,
|
||||
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
||||
};
|
||||
}
|
||||
|
||||
getCrn (appName: string): string {
|
||||
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
|
||||
if (auctionIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const auctions = await this.registry.getAuctionsByIds(auctionIds);
|
||||
|
||||
const completedAuctions = auctions
|
||||
.filter((auction: { id: string, status: string }) => auction.status === 'completed')
|
||||
.map((auction: { id: string, status: string }) => auction.id);
|
||||
|
||||
return completedAuctions;
|
||||
}
|
||||
|
||||
async publishRecord(recordData: any): Promise<any> {
|
||||
const fee = parseGasAndFees(
|
||||
this.registryConfig.fee.gas,
|
||||
this.registryConfig.fee.fees,
|
||||
);
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: recordData,
|
||||
bondId: this.registryConfig.bondId,
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee,
|
||||
),
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getRecordsByName(name: string): Promise<any> {
|
||||
return this.registry.resolveNames([name]);
|
||||
}
|
||||
|
||||
async getAuctionData(auctionId: string): Promise<any> {
|
||||
return this.registry.getAuctionsByIds([auctionId]);
|
||||
}
|
||||
|
||||
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const account = await this.getAccount();
|
||||
const laconicClient = await this.registry.getLaconicClient(account);
|
||||
const txResponse: DeliverTxResponse =
|
||||
await registryTransactionWithRetry(() =>
|
||||
laconicClient.sendTokens(account.address, receiverAddress,
|
||||
[
|
||||
{
|
||||
denom: 'alnt',
|
||||
amount
|
||||
}
|
||||
],
|
||||
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
|
||||
);
|
||||
|
||||
return txResponse;
|
||||
}
|
||||
|
||||
async getAccount(): Promise<Account> {
|
||||
const account = new Account(Buffer.from(this.registryConfig.privateKey, 'hex'));
|
||||
await account.init();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
||||
const account = await this.getAccount();
|
||||
const laconicClient = await this.registry.getLaconicClient(account);
|
||||
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash);
|
||||
|
||||
return txResponse;
|
||||
}
|
||||
|
||||
getLrn(appName: string): string {
|
||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
}
|
||||
|
||||
async generateConfigHash(
|
||||
environmentVariables: { [key: string]: string },
|
||||
requesterAddress: string,
|
||||
pubKey: string,
|
||||
url: string,
|
||||
): Promise<string> {
|
||||
// Config to be encrypted
|
||||
const config = {
|
||||
authorized: [requesterAddress],
|
||||
config: { env: environmentVariables },
|
||||
};
|
||||
|
||||
// Serialize the config
|
||||
const serialized = JSON.stringify(config, null, 2);
|
||||
|
||||
const armoredKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n${pubKey}\n\n-----END PGP PUBLIC KEY BLOCK-----`;
|
||||
const publicKey = await openpgp.readKey({ armoredKey });
|
||||
|
||||
// Encrypt the config
|
||||
const encrypted = await openpgp.encrypt({
|
||||
message: await openpgp.createMessage({ text: serialized }),
|
||||
encryptionKeys: publicKey,
|
||||
format: 'binary',
|
||||
});
|
||||
|
||||
// Get the hash after uploading encrypted config
|
||||
const response = await fetch(`${url}/upload/config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: encrypted,
|
||||
});
|
||||
|
||||
const configHash = await response.json();
|
||||
|
||||
return configHash.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Permission } from './entity/ProjectMember';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { Project } from './entity/Project';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { AddProjectFromTemplateInput } from './types';
|
||||
import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types';
|
||||
|
||||
const log = debug('snowball:resolver');
|
||||
|
||||
@@ -22,8 +22,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getOrganizationsByUserId(context.user);
|
||||
},
|
||||
|
||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getProjectById(projectId);
|
||||
project: async (_: any, { projectId }: { projectId: string }, context: any) => {
|
||||
return service.getProjectById(context.user, projectId);
|
||||
},
|
||||
|
||||
projectsInOrganization: async (
|
||||
@@ -38,7 +38,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
},
|
||||
|
||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getDeploymentsByProjectId(projectId);
|
||||
return service.getNonCanonicalDeploymentsByProjectId(projectId);
|
||||
},
|
||||
|
||||
environmentVariables: async (
|
||||
@@ -69,6 +69,39 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
) => {
|
||||
return service.getDomainsByProjectId(projectId, filter);
|
||||
},
|
||||
|
||||
getAuctionData: async (
|
||||
_: any,
|
||||
{ auctionId }: { auctionId: string },
|
||||
) => {
|
||||
return service.getAuctionData(auctionId);
|
||||
},
|
||||
|
||||
deployers: async (_: any, __: any, context: any) => {
|
||||
return service.getDeployers();
|
||||
},
|
||||
|
||||
address: async (_: any, __: any, context: any) => {
|
||||
return service.getAddress();
|
||||
},
|
||||
|
||||
verifyTx: async (
|
||||
_: any,
|
||||
{
|
||||
txHash,
|
||||
amount,
|
||||
senderAddress,
|
||||
}: { txHash: string; amount: string; senderAddress: string },
|
||||
) => {
|
||||
return service.verifyTx(txHash, amount, senderAddress);
|
||||
},
|
||||
|
||||
latestDNSRecord: async (
|
||||
_: any,
|
||||
{ projectId }: { projectId: string },
|
||||
) => {
|
||||
return service.getLatestDNSRecordByProjectId(projectId);
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
@@ -203,7 +236,16 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
}: { organizationSlug: string; data: AddProjectFromTemplateInput },
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
data: AddProjectFromTemplateInput;
|
||||
lrn: string;
|
||||
auctionParams: AuctionParams;
|
||||
environmentVariables: EnvironmentVariables[];
|
||||
},
|
||||
context: any,
|
||||
) => {
|
||||
try {
|
||||
@@ -211,6 +253,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
@@ -223,11 +268,27 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
data: DeepPartial<Project>;
|
||||
lrn: string;
|
||||
auctionParams: AuctionParams,
|
||||
environmentVariables: EnvironmentVariables[];
|
||||
},
|
||||
context: any,
|
||||
) => {
|
||||
try {
|
||||
return await service.addProject(context.user, organizationSlug, data);
|
||||
return await service.addProject(
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
throw err;
|
||||
|
||||
@@ -5,19 +5,6 @@ import { authenticateUser, createUser } from '../turnkey-backend';
|
||||
|
||||
const router = Router();
|
||||
|
||||
//
|
||||
// Access Code
|
||||
//
|
||||
router.post('/accesscode', async (req, res) => {
|
||||
console.log('Access Code', req.body);
|
||||
const { accesscode } = req.body;
|
||||
if (accesscode === '44444') {
|
||||
return res.send({ isValid: true });
|
||||
} else {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Turnkey
|
||||
//
|
||||
@@ -40,7 +27,7 @@ router.post('/register', async (req, res) => {
|
||||
userEmail: email,
|
||||
userName: email.split('@')[0],
|
||||
});
|
||||
req.session.userId = user.id;
|
||||
req.session.address = user.id;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
@@ -52,7 +39,7 @@ router.post('/authenticate', async (req, res) => {
|
||||
signedWhoamiRequest,
|
||||
);
|
||||
if (user) {
|
||||
req.session.userId = user.id;
|
||||
req.session.address = user.id;
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
@@ -60,11 +47,10 @@ router.post('/authenticate', async (req, res) => {
|
||||
});
|
||||
|
||||
//
|
||||
// Lit
|
||||
// SIWE Auth
|
||||
//
|
||||
|
||||
router.post('/validate', async (req, res) => {
|
||||
const { message, signature, action } = req.body;
|
||||
const { message, signature } = req.body;
|
||||
const { success, data } = await new SiweMessage(message).verify({
|
||||
signature,
|
||||
});
|
||||
@@ -75,23 +61,20 @@ router.post('/validate', async (req, res) => {
|
||||
const service: Service = req.app.get('service');
|
||||
const user = await service.getUserByEthAddress(data.address);
|
||||
|
||||
if (action === 'signup') {
|
||||
if (user) {
|
||||
return res.send({ success: false, error: 'user_already_exists' });
|
||||
}
|
||||
if (!user) {
|
||||
const newUser = await service.createUser({
|
||||
ethAddress: data.address,
|
||||
email: '',
|
||||
name: '',
|
||||
email: `${data.address}@example.com`,
|
||||
subOrgId: '',
|
||||
turnkeyWalletId: '',
|
||||
});
|
||||
req.session.userId = newUser.id;
|
||||
} else if (action === 'login') {
|
||||
if (!user) {
|
||||
return res.send({ success: false, error: 'user_not_found' });
|
||||
}
|
||||
req.session.userId = user.id;
|
||||
|
||||
// SIWESession from the web3modal library requires both address and chain ID
|
||||
req.session.address = newUser.id;
|
||||
req.session.chainId = data.chainId;
|
||||
} else {
|
||||
req.session.address = user.id;
|
||||
req.session.chainId = data.chainId;
|
||||
}
|
||||
|
||||
res.send({ success });
|
||||
@@ -101,19 +84,14 @@ router.post('/validate', async (req, res) => {
|
||||
// General
|
||||
//
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.userId) {
|
||||
if (req.session.address && req.session.chainId) {
|
||||
res.send({
|
||||
userId: req.session.userId,
|
||||
address: req.session.address,
|
||||
chainId: req.session.chainId
|
||||
});
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
// This is how you clear cookie-session
|
||||
(req as any).session = null;
|
||||
res.send({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Router } from 'express';
|
||||
const router = Router();
|
||||
|
||||
router.get('/version', async (req, res) => {
|
||||
return res.send({ version: '0.0.8' });
|
||||
return res.send({ version: '0.0.9' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -22,6 +22,13 @@ enum DeploymentStatus {
|
||||
Deleting
|
||||
}
|
||||
|
||||
enum AuctionStatus {
|
||||
completed
|
||||
reveal
|
||||
commit
|
||||
expired
|
||||
}
|
||||
|
||||
enum DomainStatus {
|
||||
Live
|
||||
Pending
|
||||
@@ -65,8 +72,13 @@ type Project {
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
description: String
|
||||
deployers: [Deployer!]
|
||||
auctionId: String
|
||||
fundsReleased: Boolean
|
||||
template: String
|
||||
framework: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
webhooks: [String!]
|
||||
members: [ProjectMember!]
|
||||
environmentVariables: [EnvironmentVariable!]
|
||||
@@ -74,7 +86,7 @@ type Project {
|
||||
updatedAt: String!
|
||||
organization: Organization!
|
||||
icon: String
|
||||
subDomain: String
|
||||
baseDomains: [String!]
|
||||
}
|
||||
|
||||
type ProjectMember {
|
||||
@@ -88,13 +100,16 @@ type ProjectMember {
|
||||
|
||||
type Deployment {
|
||||
id: String!
|
||||
domain: Domain
|
||||
branch: String!
|
||||
commitHash: String!
|
||||
commitMessage: String!
|
||||
url: String
|
||||
environment: Environment!
|
||||
deployer: Deployer
|
||||
applicationDeploymentRequestId: String
|
||||
applicationDeploymentRecordData: AppDeploymentRecordAttributes
|
||||
isCurrent: Boolean!
|
||||
baseDomain: String
|
||||
status: DeploymentStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
@@ -120,6 +135,17 @@ type EnvironmentVariable {
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Deployer {
|
||||
deployerLrn: String!
|
||||
deployerId: String!
|
||||
deployerApiUrl: String!
|
||||
minimumPayment: String
|
||||
paymentAddress: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
baseDomain: String
|
||||
}
|
||||
|
||||
type AuthResult {
|
||||
token: String!
|
||||
}
|
||||
@@ -136,6 +162,8 @@ input AddProjectFromTemplateInput {
|
||||
owner: String!
|
||||
name: String!
|
||||
isPrivate: Boolean!
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input AddProjectInput {
|
||||
@@ -143,6 +171,8 @@ input AddProjectInput {
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
template: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
@@ -182,6 +212,69 @@ input FilterDomainsInput {
|
||||
status: DomainStatus
|
||||
}
|
||||
|
||||
type Fee {
|
||||
type: String!
|
||||
quantity: String!
|
||||
}
|
||||
|
||||
type Bid {
|
||||
auctionId: String!
|
||||
bidderAddress: String!
|
||||
status: String!
|
||||
commitHash: String!
|
||||
commitTime: String
|
||||
commitFee: Fee
|
||||
revealTime: String
|
||||
revealFee: Fee
|
||||
bidAmount: Fee
|
||||
}
|
||||
|
||||
type Auction {
|
||||
id: String!
|
||||
kind: String!
|
||||
status: String!
|
||||
ownerAddress: String!
|
||||
createTime: String!
|
||||
commitsEndTime: String!
|
||||
revealsEndTime: String!
|
||||
commitFee: Fee!
|
||||
revealFee: Fee!
|
||||
minimumBid: Fee
|
||||
winnerAddresses: [String!]!
|
||||
winnerBids: [Fee!]
|
||||
winnerPrice: Fee
|
||||
maxPrice: Fee
|
||||
numProviders: Int!
|
||||
fundsReleased: Boolean!
|
||||
bids: [Bid!]!
|
||||
}
|
||||
|
||||
type DNSRecordAttributes {
|
||||
name: String
|
||||
value: String
|
||||
request: String
|
||||
resourceType: String
|
||||
version: String
|
||||
}
|
||||
|
||||
type AppDeploymentRecordAttributes {
|
||||
application: String
|
||||
auction: String
|
||||
deployer: String
|
||||
dns: String
|
||||
meta: String
|
||||
name: String
|
||||
request: String
|
||||
type: String
|
||||
url: String
|
||||
version: String
|
||||
}
|
||||
|
||||
input AuctionParams {
|
||||
maxPrice: String,
|
||||
numProviders: Int,
|
||||
}
|
||||
|
||||
type Query {
|
||||
user: User!
|
||||
organizations: [Organization!]
|
||||
@@ -192,7 +285,12 @@ type Query {
|
||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||
projectMembers(projectId: String!): [ProjectMember!]
|
||||
searchProjects(searchText: String!): [Project!]
|
||||
getAuctionData(auctionId: String!): Auction!
|
||||
latestDNSRecord(projectId: String!): DNSRecordAttributes
|
||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||
deployers: [Deployer]
|
||||
address: String!
|
||||
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@@ -215,8 +313,17 @@ type Mutation {
|
||||
addProjectFromTemplate(
|
||||
organizationSlug: String!
|
||||
data: AddProjectFromTemplateInput
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(
|
||||
organizationSlug: String!
|
||||
data: AddProjectInput!
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||
redeployToProd(deploymentId: String!): Boolean!
|
||||
deleteProject(projectId: String!): Boolean!
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
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,
|
||||
AuthenticationError,
|
||||
} from 'apollo-server-core';
|
||||
import cookieSession from 'cookie-session';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import cors from 'cors';
|
||||
import debug from 'debug';
|
||||
import express from 'express';
|
||||
import session from 'express-session';
|
||||
import { createServer } from 'http';
|
||||
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
|
||||
import { ServerConfig } from './config';
|
||||
import { DEFAULT_GQL_PATH } from './constants';
|
||||
import githubRouter from './routes/github';
|
||||
import authRouter from './routes/auth';
|
||||
import githubRouter from './routes/github';
|
||||
import stagingRouter from './routes/staging';
|
||||
import { Service } from './service';
|
||||
|
||||
const log = debug('snowball:server');
|
||||
|
||||
// Set cookie expiration to 1 month in milliseconds
|
||||
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
userId: string;
|
||||
address: string;
|
||||
chainId: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,14 +58,13 @@ export const createAndStartServer = async (
|
||||
context: async ({ req }) => {
|
||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||
|
||||
const { userId } = req.session;
|
||||
const { address } = req.session;
|
||||
|
||||
if (!userId) {
|
||||
if (!address) {
|
||||
throw new AuthenticationError('Unauthorized: No active session');
|
||||
}
|
||||
|
||||
const user = await service.getUser(userId);
|
||||
|
||||
const user = await service.getUser(address);
|
||||
return { user };
|
||||
},
|
||||
plugins: [
|
||||
@@ -80,20 +83,25 @@ export const createAndStartServer = async (
|
||||
}),
|
||||
);
|
||||
|
||||
const sessionOptions: session.SessionOptions = {
|
||||
secret: secret,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
domain: domain || undefined,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||
}
|
||||
};
|
||||
|
||||
if (trustProxy) {
|
||||
// trust first proxy
|
||||
app.set('trust proxy', 1);
|
||||
}
|
||||
|
||||
app.use(
|
||||
cookieSession({
|
||||
secret: secret,
|
||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
|
||||
maxAge: 23 * 60 * 60 * 1000,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||
domain: domain || undefined,
|
||||
}),
|
||||
session(sessionOptions) as unknown as express.RequestHandler
|
||||
);
|
||||
|
||||
server.applyMiddleware({
|
||||
@@ -108,9 +116,9 @@ export const createAndStartServer = async (
|
||||
app.use(express.json());
|
||||
|
||||
app.set('service', service);
|
||||
app.use('/auth', authRouter);
|
||||
app.use('/api/github', githubRouter);
|
||||
app.use('/staging', stagingRouter);
|
||||
app.use('/auth', authRouter as express.RequestHandler);
|
||||
app.use('/api/github', githubRouter as express.RequestHandler);
|
||||
app.use('/staging', stagingRouter as express.RequestHandler);
|
||||
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface GitPushEventPayload {
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
application: string;
|
||||
auction: string;
|
||||
deployer: string;
|
||||
dns: string;
|
||||
meta: string;
|
||||
name: string;
|
||||
@@ -38,6 +40,22 @@ export interface AppDeploymentRecordAttributes {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface DNSRecordAttributes {
|
||||
name: string;
|
||||
value: string;
|
||||
request: string;
|
||||
resourceType: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface RegistryDNSRecordAttributes {
|
||||
name: string;
|
||||
value: string;
|
||||
request: string;
|
||||
resource_type: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRemovalRecordAttributes {
|
||||
deployment: string;
|
||||
request: string;
|
||||
@@ -45,7 +63,7 @@ export interface AppDeploymentRemovalRecordAttributes {
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface RegistryRecord {
|
||||
export interface RegistryRecord {
|
||||
id: string;
|
||||
names: string[] | null;
|
||||
owners: string[];
|
||||
@@ -62,10 +80,45 @@ export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRemovalRecordAttributes;
|
||||
}
|
||||
|
||||
export interface DNSRecord extends RegistryRecord {
|
||||
attributes: RegistryDNSRecordAttributes
|
||||
}
|
||||
|
||||
export interface AddProjectFromTemplateInput {
|
||||
templateOwner: string;
|
||||
templateRepo: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
isPrivate: boolean;
|
||||
paymentAddress: string;
|
||||
txHash: string;
|
||||
}
|
||||
|
||||
export interface AuctionParams {
|
||||
maxPrice: string,
|
||||
numProviders: number,
|
||||
}
|
||||
|
||||
export interface EnvironmentVariables {
|
||||
environments: string[],
|
||||
key: string,
|
||||
value: string,
|
||||
}
|
||||
|
||||
export interface DeployerRecord {
|
||||
id: string;
|
||||
names: string[];
|
||||
owners: string[];
|
||||
bondId: string;
|
||||
createTime: string;
|
||||
expiryTime: string;
|
||||
attributes: {
|
||||
apiUrl: string;
|
||||
minimumPayment: string | null;
|
||||
name: string;
|
||||
paymentAddress: string;
|
||||
publicKey: string;
|
||||
type: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import fs from 'fs-extra';
|
||||
import { Octokit } from 'octokit';
|
||||
import path from 'path';
|
||||
import toml from 'toml';
|
||||
import debug from 'debug';
|
||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { Config } from './config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||
import { PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:utils');
|
||||
|
||||
@@ -76,3 +80,64 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const getRepoDetails = async (
|
||||
octokit: Octokit,
|
||||
repository: string,
|
||||
commitHash: string | undefined,
|
||||
): Promise<{
|
||||
repo: string;
|
||||
packageJSON: PackageJSON;
|
||||
repoUrl: string;
|
||||
}> => {
|
||||
const [owner, repo] = repository.split('/');
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'package.json',
|
||||
ref: commitHash,
|
||||
});
|
||||
|
||||
if (!packageJSONData) {
|
||||
throw new Error('Package.json file not found');
|
||||
}
|
||||
|
||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
const repoUrl = (
|
||||
await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
).data.html_url;
|
||||
|
||||
return {
|
||||
repo,
|
||||
packageJSON,
|
||||
repoUrl
|
||||
};
|
||||
}
|
||||
|
||||
// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs
|
||||
export const registryTransactionWithRetry = async (
|
||||
txMethod: () => Promise<any>
|
||||
): Promise<any> => {
|
||||
try {
|
||||
return await txMethod();
|
||||
} catch (error: any) {
|
||||
if (!error.message.includes('account sequence mismatch')) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(`Transaction failed due to account sequence mismatch. Retrying...`);
|
||||
|
||||
try {
|
||||
return await txMethod();
|
||||
} catch (retryError: any) {
|
||||
throw new Error(`Transaction failed again after retry: ${retryError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* This file demonstrates proper TSDoc usage for the Snowball Tools project.
|
||||
*
|
||||
* @packageDocumentation
|
||||
* @module Utils
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration options for the application.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface AppConfig {
|
||||
/**
|
||||
* The port number the server should listen on.
|
||||
* @default 3000
|
||||
*/
|
||||
port: number;
|
||||
|
||||
/**
|
||||
* The host the server should bind to.
|
||||
* @default "localhost"
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* Whether to enable debug mode.
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user in the system.
|
||||
*
|
||||
* @remarks
|
||||
* This class is used throughout the application to represent user data.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const user = new User('John', 'Doe');
|
||||
* console.log(user.fullName); // "John Doe"
|
||||
* ```
|
||||
*/
|
||||
export class User {
|
||||
/**
|
||||
* Creates a new User instance.
|
||||
*
|
||||
* @param firstName - The user's first name
|
||||
* @param lastName - The user's last name
|
||||
*/
|
||||
constructor(
|
||||
/** The user's first name */
|
||||
public firstName: string,
|
||||
/** The user's last name */
|
||||
public lastName: string
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the user's full name.
|
||||
*
|
||||
* @returns The user's full name as a string
|
||||
*/
|
||||
get fullName(): string {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's name.
|
||||
*
|
||||
* @param firstName - The new first name
|
||||
* @param lastName - The new last name
|
||||
* @returns The updated user instance
|
||||
*
|
||||
* @throws {Error} If either name is empty
|
||||
*/
|
||||
updateName(firstName: string, lastName: string): User {
|
||||
if (!firstName || !lastName) {
|
||||
throw new Error('First name and last name cannot be empty');
|
||||
}
|
||||
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sum of two numbers.
|
||||
*
|
||||
* @param a - The first number
|
||||
* @param b - The second number
|
||||
* @returns The sum of a and b
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = add(1, 2);
|
||||
* console.log(result); // 3
|
||||
* ```
|
||||
*/
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date according to the specified format.
|
||||
*
|
||||
* @param date - The date to format
|
||||
* @param format - The format string (default: 'YYYY-MM-DD')
|
||||
* @returns The formatted date string
|
||||
*
|
||||
* @remarks
|
||||
* This function uses a simple format string where:
|
||||
* - YYYY: 4-digit year
|
||||
* - MM: 2-digit month
|
||||
* - DD: 2-digit day
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const date = new Date('2023-01-15');
|
||||
* const formatted = formatDate(date);
|
||||
* console.log(formatted); // "2023-01-15"
|
||||
* ```
|
||||
*/
|
||||
export function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', year.toString())
|
||||
.replace('MM', month)
|
||||
.replace('DD', day);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of log level for the application.
|
||||
*/
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
/**
|
||||
* A simple logger utility.
|
||||
*
|
||||
* @remarks
|
||||
* This logger provides methods for different log levels and can be configured
|
||||
* to filter logs based on minimum level.
|
||||
*/
|
||||
export class Logger {
|
||||
/**
|
||||
* Creates a new Logger instance.
|
||||
*
|
||||
* @param name - The name of the logger
|
||||
* @param minLevel - The minimum log level to display (default: 'info')
|
||||
*/
|
||||
constructor(
|
||||
private name: string,
|
||||
private minLevel: LogLevel = 'info'
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Logs a debug message.
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param data - Optional data to include
|
||||
*/
|
||||
debug(message: string, data?: unknown): void {
|
||||
this.log('debug', message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an info message.
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param data - Optional data to include
|
||||
*/
|
||||
info(message: string, data?: unknown): void {
|
||||
this.log('info', message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message.
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param data - Optional data to include
|
||||
*/
|
||||
warn(message: string, data?: unknown): void {
|
||||
this.log('warn', message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error message.
|
||||
*
|
||||
* @param message - The message to log
|
||||
* @param error - Optional error to include
|
||||
*/
|
||||
error(message: string, error?: Error): void {
|
||||
this.log('error', message, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to handle logging.
|
||||
*
|
||||
* @param level - The log level
|
||||
* @param message - The message to log
|
||||
* @param data - Optional data to include
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private log(level: LogLevel, message: string, data?: unknown): void {
|
||||
const levels: Record<LogLevel, number> = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3
|
||||
};
|
||||
|
||||
if (levels[level] >= levels[this.minLevel]) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const formattedMessage = `[${timestamp}] [${level.toUpperCase()}] [${this.name}] ${message}`;
|
||||
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
console.debug(formattedMessage, data || '');
|
||||
break;
|
||||
case 'info':
|
||||
console.info(formattedMessage, data || '');
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(formattedMessage, data || '');
|
||||
break;
|
||||
case 'error':
|
||||
console.error(formattedMessage, data || '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ import * as fs from 'fs/promises';
|
||||
import debug from 'debug';
|
||||
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
const log = debug('snowball:delete-database');
|
||||
|
||||
@@ -13,7 +11,7 @@ const deleteFile = async (filePath: string) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
const config = await getConfig();
|
||||
|
||||
deleteFile(config.database.dbPath);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||
"name": "Snowball Tools",
|
||||
"slug": "snowball-tools-1"
|
||||
},
|
||||
{
|
||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
||||
"name": "Laconic",
|
||||
"slug": "laconic-2"
|
||||
"name": "Deploy Tools",
|
||||
"slug": "deploy-tools"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,8 +16,6 @@ import {
|
||||
getEntities,
|
||||
loadAndSaveData
|
||||
} from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
const log = debug('snowball:initialize-database');
|
||||
|
||||
@@ -156,7 +154,7 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
const config = await getConfig();
|
||||
const isDbPresent = await checkFileExists(config.database.dbPath);
|
||||
|
||||
if (!isDbPresent) {
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { Config } from '../src/config';
|
||||
import { getConfig } from '../src/utils';
|
||||
|
||||
const log = debug('snowball:initialize-registry');
|
||||
|
||||
const DENOM = 'aphoton';
|
||||
const DENOM = 'alnt';
|
||||
const BOND_AMOUNT = '1000000000';
|
||||
|
||||
async function main () {
|
||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
const { registryConfig } = await getConfig();
|
||||
|
||||
// TODO: Get authority names from args
|
||||
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||
|
||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, {chainId: registryConfig.chainId});
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||
log('bondId:', bondId);
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
await registry.createBond(
|
||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
fee
|
||||
);
|
||||
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee);
|
||||
log('Reserved authority name:', name);
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
fee
|
||||
);
|
||||
log(`Bond ${bondId} set for authority ${name}`);
|
||||
}
|
||||
|
||||
@@ -2,22 +2,20 @@ import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
||||
|
||||
const log = debug('snowball:publish-deploy-records');
|
||||
|
||||
async function main () {
|
||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
async function main() {
|
||||
const { registryConfig, database } = await getConfig();
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
registryConfig.chainId
|
||||
{ chainId: registryConfig.chainId }
|
||||
);
|
||||
|
||||
const dataSource = new DataSource({
|
||||
@@ -40,7 +38,7 @@ async function main () {
|
||||
});
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
||||
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`;
|
||||
|
||||
const applicationDeploymentRecord = {
|
||||
type: 'ApplicationDeploymentRecord',
|
||||
@@ -61,6 +59,8 @@ async function main () {
|
||||
url
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
@@ -68,12 +68,12 @@ async function main () {
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
registryConfig.fee
|
||||
fee
|
||||
);
|
||||
|
||||
// Remove deployment for project subdomain if deployment is for production environment
|
||||
if (deployment.environment === Environment.Production) {
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`;
|
||||
|
||||
await registry.setRecord(
|
||||
{
|
||||
@@ -82,12 +82,12 @@ async function main () {
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
registryConfig.fee
|
||||
fee
|
||||
);
|
||||
}
|
||||
|
||||
log('Application deployment record data:', applicationDeploymentRecord);
|
||||
log(`Application deployment record published: ${result.data.id}`);
|
||||
log(`Application deployment record published: ${result.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,20 @@ import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
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-deployment-removal-records');
|
||||
|
||||
async function main () {
|
||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
const { registryConfig, database } = await getConfig();
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
registryConfig.chainId
|
||||
{ chainId: registryConfig.chainId }
|
||||
);
|
||||
|
||||
const dataSource = new DataSource({
|
||||
@@ -47,6 +45,8 @@ async function main () {
|
||||
request: deployment.applicationDeploymentRemovalRequestId,
|
||||
}
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
@@ -54,11 +54,11 @@ async function main () {
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
registryConfig.fee
|
||||
fee
|
||||
);
|
||||
|
||||
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
||||
log(`Application deployment removal record published: ${result.data.id}`);
|
||||
log(`Application deployment removal record published: ${result.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
REGISTRY_BOND_ID=
|
||||
DEPLOYER_LRN=
|
||||
AUTHORITY=
|
||||
@@ -0,0 +1 @@
|
||||
records/*.yml
|
||||
@@ -17,7 +17,7 @@
|
||||
```bash
|
||||
./deploy-frontend.staging.sh
|
||||
```
|
||||
|
||||
|
||||
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||
|
||||
```bash
|
||||
@@ -51,14 +51,14 @@
|
||||
|
||||
```bash
|
||||
# Account balance
|
||||
yarn laconic cns account get
|
||||
yarn laconic registry account get
|
||||
|
||||
# Bond balance
|
||||
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
||||
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||
```
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
services:
|
||||
cns:
|
||||
registry:
|
||||
restEndpoint: 'http://console.laconic.com:1317'
|
||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
||||
chainId: laconic_9000-1
|
||||
gas: 1200000
|
||||
fees: 200000aphoton
|
||||
gas:
|
||||
fees:
|
||||
gasPrice: 1
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
services:
|
||||
cns:
|
||||
restEndpoint: http://console.laconic.com:1317
|
||||
gqlEndpoint: http://console.laconic.com:9473/api
|
||||
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
||||
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
chainId: laconic_9000-1
|
||||
gas: 1200000
|
||||
fees: 200000aphoton
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||
userKey:
|
||||
bondId:
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
# Get the latest commit hash for a branch
|
||||
BRANCH_NAME="main"
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
|
||||
|
||||
echo "Latest commit hash for branch $BRANCH_NAME: $LATEST_HASH"
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
@@ -13,40 +21,17 @@ PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat >./records/application-record.yml <<EOF
|
||||
record:
|
||||
@@ -55,16 +40,16 @@ record:
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: snowballtools-base-frontend
|
||||
name: deploy-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully."
|
||||
echo "Files generated successfully"
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
@@ -75,17 +60,17 @@ echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
@@ -93,16 +78,16 @@ if [ $rc -ne 0 ]; then
|
||||
fi
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_CRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
@@ -110,14 +95,57 @@ if [ $rc -ne 0 ]; then
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get payment address for deployer
|
||||
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||
|
||||
echo "Paying address: $paymentAddress with amount $paymentAmount..."
|
||||
|
||||
# Pay deployer if paymentAmount is not null
|
||||
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||
|
||||
# Extract the transaction hash
|
||||
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||
echo "Paid deployer with txHash as $txHash"
|
||||
|
||||
else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy.laconic.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.laconic.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23li4NtYybQlF6u5Dk
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||
meta:
|
||||
note: Added @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
payment: $txHash
|
||||
EOF
|
||||
|
||||
RECORD_FILE=records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.staging.yml
|
||||
REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./staging-records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
dns: dashboard.staging.apps.snowballtools.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
|
||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
|
||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat >./staging-records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: staging-snowballtools-base-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully."
|
||||
|
||||
RECORD_FILE=staging-records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
exit $rc
|
||||
fi
|
||||
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_CRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
exit $rc
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RECORD_FILE=staging-records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
fi
|
||||
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat >./records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully"
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
exit $rc
|
||||
fi
|
||||
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
exit $rc
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get payment address for deployer
|
||||
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||
# Pay deployer if paymentAmount is not null
|
||||
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||
|
||||
# Extract the transaction hash
|
||||
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||
echo "Paid deployer with txHash as $txHash"
|
||||
|
||||
else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
payment: $txHash
|
||||
EOF
|
||||
|
||||
RECORD_FILE=records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
fi
|
||||
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
@@ -4,6 +4,6 @@
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@snowballtools/laconic-registry-cli": "^0.1.13"
|
||||
"@cerc-io/laconic-registry-cli": "^0.2.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: "1.0.0"
|
||||
name: snowballtools-base-frontend@0.1.8
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
@@ -1,8 +0,0 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.2
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
name: snowballtools-base-frontend
|
||||
app_version: 0.1.8
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
|
||||
# Generate application-deployment-removal-request.yml
|
||||
REMOVAL_REQUEST_RECORD_FILE=records/application-deployment-removal-request.yml
|
||||
# TODO: Pass deployment record ID as arg
|
||||
DEPLOYMENT_RECORD_ID=bafyreidjho77xeczaqpyawhc4wbpm5it5atibtuxk6ost6vnpu2svlp3ka
|
||||
|
||||
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
deployer: $DEPLOYER_LRN
|
||||
deployment: $DEPLOYMENT_RECORD_ID
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
EOF
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Deployment checks
|
||||
RETRY_INTERVAL=30
|
||||
MAX_RETRIES=20
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_removal_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count+1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord found"
|
||||
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
||||
echo $REMOVAL_RECORD_ID
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deployment removal successful"
|
||||
@@ -1,24 +0,0 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: staging-snowballtools-base-frontend@0.0.0
|
||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@0.0.0
|
||||
dns: dashboard.staging.apps.snowballtools.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
|
||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
|
||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||
meta:
|
||||
note: Added by Snowball @ Sat Jun 22 02:49:45 UTC 2024
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
|
||||
@@ -1,8 +0,0 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.1
|
||||
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
name: staging-snowballtools-base-frontend
|
||||
app_version: 0.0.0
|
||||
@@ -22,7 +22,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
|
||||
APP_NAME=deployment-test-app
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
@@ -51,7 +51,7 @@ record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: $APP_NAME@$PACKAGE_VERSION
|
||||
application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||
application: lrn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
@@ -67,31 +67,31 @@ EOF
|
||||
echo "Record files generated successfully."
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
|
||||
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record exists for REGISTRY_APP_CRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||
# Check if record exists for REGISTRY_APP_LRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
|
||||
@@ -102,7 +102,7 @@ MAX_RETRIES=20
|
||||
# Check that a ApplicationDeploymentRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
@@ -140,7 +140,7 @@ fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
||||
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=5
|
||||
retry_interval=10
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "200" ]; then
|
||||
@@ -170,14 +170,14 @@ record:
|
||||
EOF
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
|
||||
@@ -3,15 +3,14 @@ VITE_SERVER_URL='http://localhost:8000'
|
||||
VITE_GITHUB_CLIENT_ID=
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app"
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
|
||||
|
||||
VITE_WALLET_CONNECT_ID=
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO="snowball-tools/starter.nextjs-react-tailwind"
|
||||
|
||||
VITE_LIT_RELAY_API_KEY=
|
||||
|
||||
VITE_ALCHEMY_API_KEY=
|
||||
|
||||
VITE_BUGSNAG_API_KEY=
|
||||
|
||||
VITE_PASSKEY_WALLET_RPID=
|
||||
VITE_TURNKEY_API_BASE_URL=
|
||||
VITE_TURNKEY_ORGANIZATION_ID=
|
||||
|
||||
VITE_LACONICD_CHAIN_ID=
|
||||
VITE_WALLET_IFRAME_URL=
|
||||
|
||||
@@ -16,10 +16,14 @@
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local.example
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*storybook.log
|
||||
*storybook.log
|
||||
|
||||
.cursor/
|
||||
@@ -44,7 +44,7 @@ yarn dev
|
||||
|
||||
## Deployment
|
||||
|
||||
From the root of the project, run:
|
||||
From the root of the project,
|
||||
|
||||
### Staging
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark dark:bg-background dark:text-foreground">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="snowball tools dashboard" />
|
||||
<meta name="description" content="laconic tools dashboard" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Snowball</title>
|
||||
<title>Laconic</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
@@ -16,21 +16,40 @@
|
||||
"@bugsnag/browser-performance": "^2.4.1",
|
||||
"@bugsnag/js": "^7.22.7",
|
||||
"@bugsnag/plugin-react": "^7.22.7",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@snowballtools/auth": "^0.2.0",
|
||||
"@snowballtools/auth-lit": "^0.2.0",
|
||||
"@snowballtools/js-sdk": "^0.1.1",
|
||||
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
|
||||
"@hookform/resolvers": "^4.1.0",
|
||||
"@mui/material": "^6.1.3",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
||||
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
||||
"@snowballtools/types": "^0.2.0",
|
||||
@@ -42,36 +61,48 @@
|
||||
"@turnkey/http": "^2.10.0",
|
||||
"@turnkey/sdk-react": "^0.1.0",
|
||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
||||
"@web3modal/siwe": "^4.0.5",
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"@web3modal/siwe": "4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "^8.3.2",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"ethers": "^5.6.2",
|
||||
"framer-motion": "^11.0.8",
|
||||
"gql-client": "^1.0.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"luxon": "^3.4.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"octokit": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-calendar": "^4.8.0",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropdown": "^1.11.0",
|
||||
"react-hook-form": "^7.49.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-oauth-popup": "^1.0.5",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "^2.1.4",
|
||||
"recharts": "^2.15.1",
|
||||
"siwe": "2.1.4",
|
||||
"sonner": "^2.0.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"vaul": "^1.1.2",
|
||||
"viem": "^2.7.11",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.3.3",
|
||||
@@ -86,7 +117,7 @@
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/uuid": "^9.0.8",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="4" fill="#29292E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0494 24.6233C18.8425 21.8302 20.5713 17.973 20.5706 13.7142C20.5717 13.1361 20.5396 12.5645 20.4762 12L12 12.0008L12.0003 28.2867C11.9996 30.2608 12.7522 32.2356 14.2578 33.7411C15.7633 35.2466 17.7395 36.0001 19.7139 35.9991L19.7134 35.9996L36 36L35.9995 27.5227C35.4362 27.4605 34.8645 27.4285 34.2852 27.4284C30.0275 27.4289 26.1701 29.1577 23.377 31.9507C21.3446 33.9321 18.0858 33.9325 16.0785 31.9252C14.0722 29.9191 14.0715 26.6593 16.0494 24.6233ZM34.2419 13.7624C31.9012 11.4217 28.0982 11.4208 25.7566 13.7624C23.4151 16.1038 23.4159 19.9067 25.7566 22.2473C28.0986 24.5892 31.9004 24.5889 34.2419 22.2473C36.5835 19.9059 36.5839 16.1042 34.2419 13.7624Z" fill="#FBFBFB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 892 B |
@@ -1 +1,10 @@
|
||||
<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>
|
||||
<svg width="115" height="20" viewBox="0 0 115 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.37388 10.5194C5.70149 8.19185 7.14225 4.97748 7.1416 1.42853C7.14246 0.94681 7.11586 0.470456 7.063 0L-0.000488281 0.000643078L-0.000273922 13.5723C-0.000917354 15.2174 0.62632 16.863 1.88091 18.1175C3.1356 19.3721 4.78235 20.0001 6.42772 19.9993L6.42729 19.9997L19.9995 20L19.999 12.9355C19.5296 12.8838 19.0532 12.857 18.5704 12.8569C15.0224 12.8574 11.8079 14.298 9.48026 16.6255C7.78654 18.2768 5.07093 18.2771 3.39812 16.6043C1.72638 14.9325 1.72562 12.2161 3.37388 10.5194ZM18.5344 1.46863C16.5837 -0.481929 13.4146 -0.48268 11.4633 1.46863C9.512 3.41984 9.51276 6.58895 11.4633 8.53941C13.415 10.491 16.5831 10.4907 18.5344 8.53941C20.4857 6.5882 20.4861 3.42016 18.5344 1.46863Z" fill="#FBFBFB"/>
|
||||
<path d="M31.4741 18.5838H39.2552V16.3302H34.075V1.41351H31.4741V18.5838Z" fill="#FBFBFB"/>
|
||||
<path d="M49.8108 1.41351H45.4976L40.9893 18.5838H43.6769L44.8039 14.2913H50.3744L51.5014 18.5838H54.3191L49.8108 1.41351ZM45.3458 12.145L47.6 3.2593H47.6866L49.8541 12.145H45.3458Z" fill="#FBFBFB"/>
|
||||
<path d="M62.9292 8.06885H65.9636C65.9636 3.17534 64.3813 1.07196 60.6967 1.07196C56.8169 1.07196 55.1479 3.73341 55.1479 9.97909C55.1479 16.2462 56.8169 18.9291 60.6967 18.9291C64.3813 18.9291 65.9636 16.8901 65.9853 12.1468H62.9508C62.9292 15.8599 62.474 16.7828 60.6967 16.7828C58.6593 16.7828 58.1607 15.4307 58.1824 9.97909C58.1824 4.54896 58.6809 3.19678 60.6967 3.21823C62.474 3.21823 62.9292 4.18413 62.9292 8.06885Z" fill="#FBFBFB"/>
|
||||
<path d="M73.7781 1.07209C77.7229 1.09364 79.4135 3.77643 79.4135 10.0007C79.4135 16.2249 77.7229 18.9078 73.7781 18.9292C69.8117 18.9507 68.1211 16.2678 68.1211 10.0007C68.1211 3.73354 69.8117 1.05064 73.7781 1.07209ZM71.1555 10.0007C71.1555 15.4308 71.6757 16.783 73.7781 16.783C75.8589 16.783 76.3791 15.4308 76.3791 10.0007C76.3791 4.54909 75.8589 3.19691 73.7781 3.21847C71.6757 3.23992 71.1555 4.59209 71.1555 10.0007Z" fill="#FBFBFB"/>
|
||||
<path d="M85.0819 18.5624L82.481 18.5838V1.41351H87.0544L91.3243 15.4073H91.3676V1.41351H93.968V18.5838H89.677L85.1254 3.51689H85.0819V18.5624Z" fill="#FBFBFB"/>
|
||||
<path d="M100.468 1.41351H97.8677V18.5838H100.468V1.41351Z" fill="#FBFBFB"/>
|
||||
<path d="M111.139 8.06885H114.174C114.174 3.17534 112.591 1.07196 108.906 1.07196C105.028 1.07196 103.358 3.73341 103.358 9.97909C103.358 16.2462 105.028 18.9291 108.906 18.9291C112.591 18.9291 114.174 16.8901 114.195 12.1468H111.161C111.139 15.8599 110.684 16.7828 108.906 16.7828C106.869 16.7828 106.371 15.4307 106.393 9.97909C106.393 4.54896 106.891 3.19678 108.906 3.21823C110.684 3.21823 111.139 4.18413 111.139 8.06885Z" fill="#FBFBFB"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Snowball Tools Dashboard",
|
||||
"name": "Laconic Tools Dashboard",
|
||||
"short_name": "snowball tools",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -1,82 +1,171 @@
|
||||
import { useEffect } from 'react';
|
||||
import ProjectSearchLayout from '@/layouts/ProjectSearch';
|
||||
import ProjectsScreen from '@/pages/org-slug/ProjectsScreen';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import Projects from './pages/org-slug';
|
||||
import Settings from './pages/org-slug/Settings';
|
||||
import {
|
||||
projectsRoutesWithSearch,
|
||||
projectsRoutesWithoutSearch,
|
||||
} from './pages/org-slug/projects/routes';
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import { LoadingOverlay } from './components/loading/loading-overlay';
|
||||
import { DashboardLayout } from './layouts/DashboardLayout';
|
||||
import RootLayout from './layouts/RootLayout';
|
||||
import Index from './pages';
|
||||
import AuthPage from './pages/AuthPage';
|
||||
import { DashboardLayout } from './pages/org-slug/layout';
|
||||
import Web3Provider from 'context/Web3Provider';
|
||||
import { baseUrl } from 'utils/constants';
|
||||
import BuyPrepaidService from './pages/BuyPrepaidService';
|
||||
import OnboardingDemoPage from './pages/OnboardingDemoPage';
|
||||
import OnboardingPage from './pages/OnboardingPage';
|
||||
import {
|
||||
projectsRoutesWithoutSearch,
|
||||
projectsRoutesWithSearch,
|
||||
} from './pages/org-slug/projects/project-routes';
|
||||
import Settings from './pages/org-slug/Settings';
|
||||
import { BASE_URL } from './utils/constants';
|
||||
|
||||
/**
|
||||
* IframeLoader component that ensures wallet iframe is loaded
|
||||
* before rendering children components that depend on it.
|
||||
*
|
||||
* TEMPORARY SOLUTION: This is a quick fix for iframe loading issues.
|
||||
*
|
||||
* TODO: Future Refactoring Plan (Medium effort, 4-8 hours):
|
||||
* - Move iframe management directly into WalletContextProvider
|
||||
* - Handle multiple wallet-related iframes in a single location
|
||||
*/
|
||||
const IframeLoader = ({ children }: { children: React.ReactNode }) => {
|
||||
const [iframeLoaded, setIframeLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const createIframe = () => {
|
||||
// Check if iframe already exists
|
||||
let iframe = document.getElementById(
|
||||
'wallet-iframe',
|
||||
) as HTMLIFrameElement;
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.id = 'wallet-iframe';
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = `${window.location.origin}/wallet-iframe.html`;
|
||||
|
||||
iframe.onload = () => {
|
||||
setIframeLoaded(true);
|
||||
};
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
} else {
|
||||
// If iframe already exists, consider it loaded
|
||||
setIframeLoaded(true);
|
||||
}
|
||||
};
|
||||
|
||||
createIframe();
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
const iframe = document.getElementById('wallet-iframe');
|
||||
if (iframe && iframe.parentNode) {
|
||||
iframe.parentNode.removeChild(iframe);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!iframeLoaded) {
|
||||
return <LoadingOverlay />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// Wrap RootLayout with IframeLoader
|
||||
const LoaderWrappedRootLayout = () => (
|
||||
<IframeLoader>
|
||||
<RootLayout />
|
||||
</IframeLoader>
|
||||
);
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: ':orgSlug',
|
||||
element: <DashboardLayout />,
|
||||
element: <LoaderWrappedRootLayout />,
|
||||
children: [
|
||||
{
|
||||
element: <ProjectSearchLayout />,
|
||||
path: ':orgSlug',
|
||||
element: <DashboardLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <Projects />,
|
||||
element: <ProjectSearchLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <ProjectsScreen />,
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
children: projectsRoutesWithSearch,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
element: <Settings />,
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
children: projectsRoutesWithSearch,
|
||||
children: projectsRoutesWithoutSearch,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
element: <Settings />,
|
||||
path: '/',
|
||||
element: <Index />,
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
children: projectsRoutesWithoutSearch,
|
||||
path: '/login',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
{
|
||||
path: '/buy-prepaid-service',
|
||||
element: <BuyPrepaidService />,
|
||||
errorElement: <div>Something went wrong!</div>,
|
||||
},
|
||||
{
|
||||
path: '/onboarding',
|
||||
element: <OnboardingPage />,
|
||||
},
|
||||
{
|
||||
path: '/onboarding-demo',
|
||||
element: <OnboardingDemoPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <Index />,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
/**
|
||||
* Main application component.
|
||||
* Sets up routing and error handling.
|
||||
* @returns {JSX.Element} The rendered application.
|
||||
*/
|
||||
function App() {
|
||||
// Hacky way of checking session
|
||||
// TODO: Handle redirect backs
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${baseUrl}/auth/session`, {
|
||||
fetch(`${BASE_URL}/auth/session`, {
|
||||
credentials: 'include',
|
||||
}).then((res) => {
|
||||
const path = window.location.pathname;
|
||||
const publicPaths = ['/login', '/onboarding', '/onboarding-demo', '/'];
|
||||
console.log(res);
|
||||
|
||||
if (res.status !== 200) {
|
||||
localStorage.clear();
|
||||
const path = window.location.pathname;
|
||||
if (path !== '/login' && path !== '/signup') {
|
||||
if (!publicPaths.includes(path)) {
|
||||
window.location.pathname = '/login';
|
||||
}
|
||||
} else {
|
||||
if (path === '/login') {
|
||||
window.location.pathname = '/deploy-tools';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Web3Provider>
|
||||
<RouterProvider router={router} />
|
||||
</Web3Provider>
|
||||
<RouterProvider router={router} fallbackElement={<LoadingOverlay />} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO,
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO,
|
||||
} from 'utils/constants';
|
||||
} from '@/utils/constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: 'pwa',
|
||||
icon: 'web',
|
||||
repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
@@ -20,23 +21,9 @@ export default [
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Swift',
|
||||
icon: 'swift',
|
||||
repoFullName: '',
|
||||
isComingSoon: true,
|
||||
name: 'Next.js + React + TailwindCSS',
|
||||
icon: 'web',
|
||||
repoFullName: `${VITE_GITHUB_NEXT_APP_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = React.PropsWithChildren<{
|
||||
className?: string;
|
||||
snowZIndex?: number;
|
||||
}>;
|
||||
|
||||
export const CloudyFlow = ({ className, children, snowZIndex }: Props) => {
|
||||
return (
|
||||
<div className={`bg-sky-100 relative ${className || ''}`}>
|
||||
{children}
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ zIndex: snowZIndex || 0 }}
|
||||
>
|
||||
<div className="w-[3.72px] h-[3.72px] left-[587px] top-[147px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.72px] h-[4.72px] left-[742px] top-[336px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.49px] h-[3.49px] left-[36px] top-[68px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[55px] top-[114px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[1334px] top-[63px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.53px] h-[3.53px] left-[988px] top-[108px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.65px] h-[2.65px] left-[1380px] top-[16px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.60px] h-[3.60px] left-[1284px] top-[95px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[1191px] top-[376px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[1182px] top-[257px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.41px] h-[2.41px] left-[627px] top-[26px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.71px] h-[5.71px] left-[30px] top-[33px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.09px] h-[4.09px] left-[425px] top-[386px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.38px] h-[3.38px] left-[394px] top-[29px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.70px] h-[4.70px] left-[817px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[1194px] top-[332px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.89px] h-[4.89px] left-[811px] top-[76px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.25px] h-[4.25px] left-[458px] top-[366px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[936px] top-[46px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[64px] top-[132px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[763px] top-[10px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.67px] h-[3.67px] left-[861px] top-[106px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.62px] h-[3.62px] left-[710px] top-[278px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[1069px] top-[329px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.92px] h-[2.92px] left-[1286px] top-[299px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[219px] top-[269px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.39px] h-[2.39px] left-[817px] top-[121px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[168px] top-[320px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.94px] h-[5.94px] left-[419px] top-[244px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[604px] top-[309px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[1098px] top-[379px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.85px] h-[5.85px] left-[644px] top-[352px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[1361px] top-[349px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.84px] h-[2.84px] left-[1299px] top-[194px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.51px] h-[4.51px] left-[468px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.73px] h-[2.73px] left-[1084px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.43px] h-[3.43px] left-[1271px] top-[28px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.25px] h-[2.25px] left-[106px] top-[197px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[122px] top-[173px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[343px] top-[345px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[433px] top-[40px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[904px] top-[350px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.42px] h-[4.42px] left-[1066px] top-[349px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[904px] top-[317px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.54px] h-[5.54px] left-[501px] top-[336px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1149px] top-[206px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[235px] top-[362px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[1246px] top-[1px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[788px] top-[6px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[527px] top-[365px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.13px] h-[4.13px] left-[201px] top-[53px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[765px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1254px] top-[30px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[107px] top-[316px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.72px] h-[5.72px] left-[1305px] top-[8px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.46px] h-[5.46px] left-[102px] top-[316px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[1322px] top-[334px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[1370px] top-[317px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.55px] h-[5.55px] left-[945px] top-[258px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.24px] h-[2.24px] left-[266px] top-[362px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[987px] top-[156px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.46px] h-[3.46px] left-[10px] top-[168px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.67px] h-[5.67px] left-[441px] top-[291px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.07px] h-[4.07px] left-[962px] top-[364px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[599px] top-[293px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.41px] h-[4.41px] left-[358px] top-[163px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.31px] h-[2.31px] left-[670px] top-[182px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[621px] top-[257px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.16px] h-[2.16px] left-[48px] top-[322px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.91px] h-[5.91px] left-[491px] top-[5px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[1139px] top-[274px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[24px] top-[177px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1166px] top-[316px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5px] h-[5px] left-[445px] top-[326px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.01px] h-[3.01px] left-[438px] top-[252px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.14px] h-[4.14px] left-[554px] top-[131px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[1010px] top-[116px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[437px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[948px] top-[27px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.87px] h-[2.87px] left-[826px] top-[20px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.89px] h-[3.89px] left-[1222px] top-[112px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[796px] top-[395px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[272px] top-[103px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.12px] h-[4.12px] left-[76px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[226px] top-[276px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.03px] h-[3.03px] left-[723px] top-[197px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1259px] top-[17px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1244px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.45px] h-[4.45px] left-[118px] top-[128px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[490px] top-[204px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.93px] h-[4.93px] left-[552px] top-[38px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.56px] h-[5.56px] left-[115px] top-[303px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.35px] h-[2.35px] left-[509px] top-[278px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.24px] h-[5.24px] left-[804px] top-[389px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[1013px] top-[50px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1183px] top-[95px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[278px] top-[181px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.22px] h-[3.22px] left-[1316px] top-[282px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[736px] top-[119px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[483px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1135px] top-[19px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.64px] h-[3.64px] left-[39px] top-[126px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[237px] top-[369px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1156px] top-[126px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.78px] h-[2.78px] left-[1295px] top-[74px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[76px] top-[227px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.61px] h-[3.61px] left-[108px] top-[89px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[191px] top-[167px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.18px] h-[4.18px] left-[164px] top-[117px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.15px] h-[5.15px] left-[533px] top-[261px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[327px] top-[157px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.74px] h-[5.74px] left-[1242px] top-[122px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.22px] h-[4.22px] left-[129px] top-[265px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.30px] h-[2.30px] left-[1305px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.70px] h-[2.70px] left-[1235px] top-[120px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.15px] h-[2.15px] left-[596px] top-[103px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.17px] h-[2.17px] left-[483px] top-[233px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.09px] h-[5.09px] left-[706px] top-[188px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[141px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.20px] h-[4.20px] left-[48px] top-[124px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[1095px] top-[201px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.21px] h-[3.21px] left-[730px] top-[185px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.61px] h-[2.61px] left-[722px] top-[319px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.28px] h-[2.28px] left-[444px] top-[26px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.49px] h-[4.49px] left-[355px] top-[212px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1280px] top-[312px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.23px] h-[4.23px] left-[1114px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.48px] h-[3.48px] left-[729px] top-[117px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[647px] top-[276px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.16px] h-[4.16px] left-[365px] top-[116px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[94px] top-[194px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[2px] top-[84px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.43px] h-[4.43px] left-[1382px] top-[23px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.38px] h-[5.38px] left-[857px] top-[284px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.77px] h-[2.77px] left-[1228px] top-[385px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.65px] h-[4.65px] left-[165px] top-[184px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[568px] top-[354px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[1303px] top-[371px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[235px] top-[188px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.84px] h-[3.84px] left-[902px] top-[211px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[367px] top-[161px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[855px] top-[394px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[383px] top-[47px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.39px] h-[4.39px] left-[1313px] top-[165px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[697px] top-[327px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[646px] top-[370px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.13px] h-[3.13px] left-[728px] top-[122px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[203px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[424px] top-[121px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[1358px] top-[176px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.18px] h-[3.18px] left-[1212px] top-[24px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.23px] h-[5.23px] left-[260px] top-[217px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[1204px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.47px] h-[3.47px] left-[1163px] top-[159px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.77px] h-[5.77px] left-[1257px] top-[115px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.31px] h-[5.31px] left-[222px] top-[356px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.43px] h-[5.43px] left-[1141px] top-[349px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.62px] h-[5.62px] left-[683px] top-[81px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.91px] h-[3.91px] left-[269px] top-[3px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[305px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.41px] h-[5.41px] left-[530px] top-[94px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.64px] h-[4.64px] left-[730px] top-[301px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[716px] top-[14px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.77px] h-[4.77px] left-[544px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[357px] top-[281px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.42px] h-[2.42px] left-[1346px] top-[112px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.42px] h-[3.42px] left-[671px] top-[150px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.40px] h-[4.40px] left-[1324px] top-[268px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.21px] h-[5.21px] left-[1028px] top-[376px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.27px] h-[4.27px] left-[499px] top-[50px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.35px] h-[4.35px] left-[543px] top-[359px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.25px] h-[5.25px] left-[1245px] top-[296px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.52px] h-[5.52px] left-[360px] top-[98px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[741px] top-[358px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.90px] h-[3.90px] left-[1262px] top-[184px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.75px] h-[5.75px] left-[552px] top-[335px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.95px] h-[4.95px] left-[120px] top-[178px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1337px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.43px] h-[2.43px] left-[233px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[218px] top-[322px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.68px] h-[3.68px] left-[984px] top-[8px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[832px] top-[55px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.93px] h-[3.93px] left-[1105px] top-[209px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[957px] top-[23px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.33px] h-[2.33px] left-[1066px] top-[390px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[737px] top-[118px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.18px] h-[5.18px] left-[202px] top-[19px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.05px] h-[5.05px] left-[466px] top-[17px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[144px] top-[153px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[233px] top-[330px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1 h-1 left-[730px] top-[179px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[1156px] top-[342px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.22px] h-[5.22px] left-[1275px] top-[204px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[38px] top-[343px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.14px] h-[5.14px] left-[867px] top-[113px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.19px] h-[2.19px] left-[1277px] top-[314px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[1136px] top-[197px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[34px] top-[226px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.93px] h-[5.93px] left-[727px] top-[272px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[277px] top-[43px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,172 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
DayPicker,
|
||||
SelectSingleEventHandler,
|
||||
DateRange,
|
||||
} from 'react-day-picker';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverHandler,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import HorizontalLine from './HorizontalLine';
|
||||
|
||||
// https://www.material-tailwind.com/docs/react/plugins/date-picker#date-picker
|
||||
const DAY_PICKER_CLASS_NAMES = {
|
||||
caption: 'flex justify-center py-2 mb-4 relative items-center',
|
||||
caption_label: 'text-sm font-medium text-gray-900',
|
||||
nav: 'flex items-center',
|
||||
nav_button:
|
||||
'h-6 w-6 bg-transparent hover:bg-blue-gray-50 p-1 rounded-md transition-colors duration-300',
|
||||
nav_button_previous: 'absolute left-1.5',
|
||||
nav_button_next: 'absolute right-1.5',
|
||||
table: 'w-full border-collapse',
|
||||
head_row: 'flex font-medium text-gray-900',
|
||||
head_cell: 'm-0.5 w-9 font-normal text-sm',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: 'text-gray-600 rounded-md h-9 w-9 text-center text-sm p-0 m-0.5 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-gray-900/20 [&:has([aria-selected].day-outside)]:text-white [&:has([aria-selected])]:bg-gray-900/50 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
|
||||
day: 'h-9 w-9 p-0 font-normal',
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'rounded-md bg-gray-900 text-white hover:bg-gray-900 hover:text-white focus:bg-gray-900 focus:text-white',
|
||||
day_today: 'rounded-md bg-gray-200 text-gray-900',
|
||||
day_outside:
|
||||
'day-outside text-gray-500 opacity-50 aria-selected:bg-gray-500 aria-selected:text-gray-900 aria-selected:bg-opacity-10',
|
||||
day_disabled: 'text-gray-500 opacity-50',
|
||||
day_hidden: 'invisible',
|
||||
};
|
||||
|
||||
type SingleDateHandler = (value: Date) => void;
|
||||
type RangeDateHandler = (value: DateRange) => void;
|
||||
|
||||
interface SingleDatePickerProps {
|
||||
mode: 'single';
|
||||
selected?: Date;
|
||||
onSelect: SingleDateHandler;
|
||||
}
|
||||
|
||||
interface RangeDatePickerProps {
|
||||
mode: 'range';
|
||||
selected?: DateRange;
|
||||
onSelect: RangeDateHandler;
|
||||
}
|
||||
|
||||
const DatePicker = ({
|
||||
mode = 'single',
|
||||
selected,
|
||||
onSelect,
|
||||
}: SingleDatePickerProps | RangeDatePickerProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [rangeSelected, setRangeSelected] = useState<DateRange>();
|
||||
|
||||
const inputValue = useMemo(() => {
|
||||
if (mode === 'single') {
|
||||
return selected ? format(selected as Date, 'PPP') : 'Select Date';
|
||||
}
|
||||
|
||||
if (mode === 'range') {
|
||||
const selectedRange = selected as DateRange | undefined;
|
||||
return selectedRange && selectedRange.from && selectedRange.to
|
||||
? format(selectedRange.from, 'PP') +
|
||||
'-' +
|
||||
format(selectedRange.to, 'PP')
|
||||
: 'All time';
|
||||
}
|
||||
}, [selected, mode]);
|
||||
|
||||
const handleSingleSelect = useCallback<SelectSingleEventHandler>((value) => {
|
||||
if (value) {
|
||||
(onSelect as SingleDateHandler)(value);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRangeSelect = useCallback(() => {
|
||||
if (rangeSelected?.to) {
|
||||
(onSelect as RangeDateHandler)(rangeSelected);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [rangeSelected]);
|
||||
|
||||
const components = {
|
||||
IconLeft: ({ ...props }) => (
|
||||
<i {...props} className="h-4 w-4 stroke-2">
|
||||
{'<'}
|
||||
</i>
|
||||
),
|
||||
IconRight: ({ ...props }) => (
|
||||
<i {...props} className="h-4 w-4 stroke-2">
|
||||
{'>'}
|
||||
</i>
|
||||
),
|
||||
};
|
||||
|
||||
const commonDayPickerProps = {
|
||||
components,
|
||||
className: 'border-0',
|
||||
classNames: DAY_PICKER_CLASS_NAMES,
|
||||
showOutsideDays: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottom"
|
||||
open={isOpen}
|
||||
handler={(value) => setIsOpen(value)}
|
||||
>
|
||||
<PopoverHandler>
|
||||
<Input onChange={() => null} value={inputValue} />
|
||||
</PopoverHandler>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
{/* @ts-ignore */}
|
||||
<PopoverContent>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
onSelect={handleSingleSelect}
|
||||
selected={selected as Date}
|
||||
{...commonDayPickerProps}
|
||||
/>
|
||||
)}
|
||||
{mode === 'range' && (
|
||||
<>
|
||||
<DayPicker
|
||||
mode="range"
|
||||
onSelect={setRangeSelected}
|
||||
selected={rangeSelected as DateRange}
|
||||
{...commonDayPickerProps}
|
||||
/>
|
||||
<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)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -9,6 +9,14 @@ export interface Option {
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Dropdown component.
|
||||
* @interface DropdownProps
|
||||
* @property {Option[]} options - The list of options to display in the dropdown.
|
||||
* @property {(arg: ReactDropdownOption) => void} onChange - Callback fired when an option is selected.
|
||||
* @property {string} [placeholder] - Placeholder text for the dropdown.
|
||||
* @property {Option} [value] - The currently selected option.
|
||||
*/
|
||||
interface DropdownProps {
|
||||
options: Option[];
|
||||
onChange: (arg: ReactDropdownOption) => void;
|
||||
@@ -16,6 +24,22 @@ interface DropdownProps {
|
||||
value?: Option;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown component that wraps the ReactDropdown library.
|
||||
*
|
||||
* @component
|
||||
* @param {DropdownProps} props - The props for the Dropdown component.
|
||||
* @returns {React.ReactElement} A dropdown element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Dropdown
|
||||
* options={[{ value: '1', label: 'One' }, { value: '2', label: 'Two' }]}
|
||||
* onChange={(option) => console.log(option)}
|
||||
* placeholder="Select an option"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
const Dropdown = ({ placeholder, options, onChange, value }: DropdownProps) => {
|
||||
return (
|
||||
<ReactDropdown
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { Duration } from 'luxon';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
/**
|
||||
* Props for the FormatMillisecond component.
|
||||
* @interface FormatMilliSecondProps
|
||||
* @property {number} time - The time in milliseconds to format.
|
||||
*/
|
||||
export interface FormatMilliSecondProps
|
||||
extends ComponentPropsWithoutRef<'div'> {
|
||||
time: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that formats a given time in milliseconds into a human-readable format.
|
||||
*
|
||||
* @component
|
||||
* @param {FormatMilliSecondProps} props - The props for the FormatMillisecond component.
|
||||
* @returns {React.ReactElement} A formatted time element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <FormatMillisecond time={3600000} />
|
||||
* ```
|
||||
*/
|
||||
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
|
||||
const formatTime = Duration.fromMillis(time)
|
||||
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* A simple horizontal line component.
|
||||
*
|
||||
* @component
|
||||
* @returns {React.ReactElement} A horizontal line element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <HorizontalLine />
|
||||
* ```
|
||||
*/
|
||||
const HorizontalLine = () => {
|
||||
return <hr className="h-px bg-gray-100 border-0" />;
|
||||
};
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Heading } from './shared/Heading';
|
||||
|
||||
/**
|
||||
* Props for the Logo component.
|
||||
* @interface LogoProps
|
||||
* @property {string} [orgSlug] - The organization slug used for the link.
|
||||
*/
|
||||
interface LogoProps {
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders the Snowball logo with a link to the organization's page.
|
||||
*
|
||||
* @component
|
||||
* @param {LogoProps} props - The props for the Logo component.
|
||||
* @returns {React.ReactElement} A logo element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Logo orgSlug="my-organization" />
|
||||
* ```
|
||||
*/
|
||||
export const Logo = ({ orgSlug }: LogoProps) => {
|
||||
return (
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<div className="flex items-center gap-3 px-0 lg:px-2">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Snowball Logo"
|
||||
className="lg:h-10 lg:w-10 h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<Heading className="lg:text-[24px] text-[19px] font-semibold">
|
||||
Snowball
|
||||
</Heading>
|
||||
<img src="/logo.svg" alt="Snowball Logo" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
import React, { forwardRef, RefAttributes } from 'react';
|
||||
|
||||
import { SearchIcon } from './shared/CustomIcon';
|
||||
import { Input, InputProps } from './shared/Input';
|
||||
import { IconInput, type IconInputProps } from '@/components/ui/extended/input-w-icons';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* A search bar component with an icon input.
|
||||
*
|
||||
* @component
|
||||
* @param {InputProps & RefAttributes<HTMLInputElement>} props - The props for the SearchBar component.
|
||||
* @returns {React.ReactElement} A search bar element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <SearchBar value="search term" onChange={(e) => console.log(e.target.value)} />
|
||||
* ```
|
||||
*/
|
||||
const SearchBar: React.ForwardRefRenderFunction<
|
||||
HTMLInputElement,
|
||||
InputProps & RefAttributes<HTMLInputElement>
|
||||
IconInputProps & RefAttributes<HTMLInputElement>
|
||||
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative flex w-full">
|
||||
<Input
|
||||
leftIcon={<SearchIcon />}
|
||||
<IconInput
|
||||
leftIcon={<Search className="text-foreground-secondary" />}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
appearance="borderless"
|
||||
// appearance="borderless"
|
||||
className="w-full lg:w-[459px]"
|
||||
{...props}
|
||||
ref={ref}
|
||||
|
||||
@@ -4,17 +4,42 @@ const COLOR_COMPLETED = '#059669';
|
||||
const COLOR_ACTIVE = '#CFE6FC';
|
||||
const COLOR_NOT_STARTED = '#F1F5F9';
|
||||
|
||||
/**
|
||||
* Represents a step in the stepper.
|
||||
* @interface StepperValue
|
||||
* @property {number} step - The step number.
|
||||
* @property {string} route - The route associated with the step.
|
||||
* @property {string} label - The label for the step.
|
||||
*/
|
||||
interface StepperValue {
|
||||
step: number;
|
||||
route: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Stepper component.
|
||||
* @interface StepperProps
|
||||
* @property {number} activeStep - The currently active step.
|
||||
* @property {StepperValue[]} stepperValues - The values for each step.
|
||||
*/
|
||||
interface StepperProps {
|
||||
activeStep: number;
|
||||
stepperValues: StepperValue[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A stepper component that displays a series of steps with different states.
|
||||
*
|
||||
* @component
|
||||
* @param {StepperProps} props - The props for the Stepper component.
|
||||
* @returns {React.ReactElement} A stepper element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Stepper activeStep={1} stepperValues={[{ step: 1, route: '/step1', label: 'Step 1' }]} />
|
||||
* ```
|
||||
*/
|
||||
const Stepper = ({ activeStep, stepperValues }: StepperProps) => {
|
||||
return (
|
||||
<StepperNav
|
||||
@@ -24,8 +49,8 @@ const Stepper = ({ activeStep, stepperValues }: StepperProps) => {
|
||||
<div
|
||||
className={`text-sm ${
|
||||
activeStep === stepperValue.step
|
||||
? 'text-black font-semibold'
|
||||
: 'text-gray-600'
|
||||
? 'text-black font-semibold dark:text-foreground'
|
||||
: 'text-gray-600 dark:text-foreground-secondary'
|
||||
}`}
|
||||
>
|
||||
{stepperValue.label}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useStopwatch } from 'react-timer-hook';
|
||||
|
||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
@@ -10,17 +11,40 @@ const setStopWatchOffset = (time: string) => {
|
||||
return currentTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the Stopwatch component.
|
||||
* @interface StopwatchProps
|
||||
* @property {Date} offsetTimestamp - The initial timestamp for the stopwatch.
|
||||
* @property {boolean} isPaused - Whether the stopwatch is paused.
|
||||
*/
|
||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds } = useStopwatch({
|
||||
/**
|
||||
* A stopwatch component that tracks elapsed time.
|
||||
*
|
||||
* @component
|
||||
* @param {StopwatchProps} props - The props for the Stopwatch component.
|
||||
* @returns {React.ReactElement} A stopwatch element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Stopwatch offsetTimestamp={new Date()} isPaused={false} />
|
||||
* ```
|
||||
*/
|
||||
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds, pause, start } = useStopwatch({
|
||||
autoStart: true,
|
||||
offsetTimestamp: offsetTimestamp,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
isPaused ? pause() : start();
|
||||
}, [isPaused]);
|
||||
|
||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||
};
|
||||
|
||||
export { Stopwatch, setStopWatchOffset };
|
||||
export { setStopWatchOffset, Stopwatch };
|
||||
|
||||
@@ -3,6 +3,14 @@ import * as CSS from 'csstype';
|
||||
//
|
||||
// Nav
|
||||
//
|
||||
/**
|
||||
* Describes a step in the stepper navigation.
|
||||
* @interface IStepDescription
|
||||
* @property {() => JSX.Element} stepContent - The content of the step.
|
||||
* @property {string} [stepStateColor] - The color representing the step's state.
|
||||
* @property {number} [stepStatusCircleSize] - The size of the status circle.
|
||||
* @property {() => void} [onClickHandler] - Handler for click events on the step.
|
||||
*/
|
||||
export interface IStepDescription {
|
||||
stepContent: () => JSX.Element;
|
||||
stepStateColor?: string;
|
||||
@@ -10,10 +18,27 @@ export interface IStepDescription {
|
||||
onClickHandler?: () => void | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the StepperNav component.
|
||||
* @interface IStepperNavProps
|
||||
* @property {IStepDescription[]} steps - The steps to display in the navigation.
|
||||
*/
|
||||
export interface IStepperNavProps {
|
||||
steps: IStepDescription[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A navigation component for displaying steps in a vertical layout.
|
||||
*
|
||||
* @component
|
||||
* @param {IStepperNavProps} props - The props for the StepperNav component.
|
||||
* @returns {React.ReactElement} A stepper navigation element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <StepperNav steps={[{ stepContent: () => <div>Step 1</div> }]} />
|
||||
* ```
|
||||
*/
|
||||
export const StepperNav = (props: IStepperNavProps): JSX.Element => {
|
||||
return (
|
||||
<nav>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
Header,
|
||||
NavigationWrapper,
|
||||
ScreenWrapper,
|
||||
TabWrapper,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '../layout';
|
||||
|
||||
/**
|
||||
* Props for the ExamplePage component.
|
||||
* @interface ExamplePageProps
|
||||
* @property {ReactNode} [children] - The content to display within the example page.
|
||||
*/
|
||||
interface ExamplePageProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* An example page component demonstrating the standard layout components.
|
||||
*
|
||||
* @component
|
||||
* @param {ExamplePageProps} props - The props for the ExamplePage component.
|
||||
* @returns {React.ReactElement} An example page element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ExamplePage>
|
||||
* <div>Custom content here</div>
|
||||
* </ExamplePage>
|
||||
* ```
|
||||
*/
|
||||
export function ExamplePage({ children }: ExamplePageProps) {
|
||||
return (
|
||||
<NavigationWrapper>
|
||||
<ScreenWrapper>
|
||||
<Header
|
||||
title="Example Page"
|
||||
subtitle="Demonstrating the standard layout components"
|
||||
actions={[
|
||||
<button key="action" className="btn-primary">
|
||||
Action
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<TabWrapper defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Overview</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Details</TabsTrigger>
|
||||
<TabsTrigger value="tab3">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="tab1">
|
||||
<div className="p-4">
|
||||
<h2>Overview Content</h2>
|
||||
{children}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="tab2">
|
||||
<div className="p-4">
|
||||
<h2>Details Content</h2>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="tab3">
|
||||
<div className="p-4">
|
||||
<h2>Settings Content</h2>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabWrapper>
|
||||
</ScreenWrapper>
|
||||
</NavigationWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ui';
|
||||
@@ -0,0 +1,9 @@
|
||||
export { NavigationWrapper } from './navigation';
|
||||
export { Header } from './screen-header/Header';
|
||||
export { ScreenWrapper } from './screen-wrapper/ScreenWrapper';
|
||||
export { TabWrapper } from './screen-wrapper/TabWrapper';
|
||||
// Re-export tab components for convenience
|
||||
export {
|
||||
TabsContent, TabsList,
|
||||
TabsTrigger
|
||||
} from '@/components/ui/tabs';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TopNavigation } from './TopNavigation';
|
||||
|
||||
/**
|
||||
* NavigationWrapperProps interface extends React.HTMLProps<HTMLDivElement> to include all standard HTML div attributes.
|
||||
*/
|
||||
interface NavigationWrapperProps extends React.HTMLProps<HTMLDivElement> {}
|
||||
|
||||
/**
|
||||
* Wraps the application's navigation and content within a consistent layout.
|
||||
* It provides a basic structure with a top navigation bar and a content area.
|
||||
*
|
||||
* @param {NavigationWrapperProps} props - The props passed to the NavigationWrapper component.
|
||||
* @param {React.ReactNode} props.children - The content to be rendered within the wrapper.
|
||||
* @param {string} [props.className] - Optional CSS class names to apply to the wrapper.
|
||||
* @param {React.HTMLAttributes<HTMLDivElement>} props.props - Other standard HTML attributes.
|
||||
*
|
||||
* @returns {React.ReactElement} A div element containing the top navigation and the children.
|
||||
*/
|
||||
export function NavigationWrapper({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: NavigationWrapperProps) {
|
||||
// const { isAuthenticated } = useGitHubAuth();
|
||||
|
||||
return (
|
||||
<div className={cn('min-h-screen bg-background', className)} {...props}>
|
||||
<TopNavigation />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
'use client';
|
||||
import { OnboardingDialog } from '@/components/onboarding-flow';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { useOctokit } from '@/context/OctokitContext';
|
||||
import { useWallet } from '@/context/WalletContextProvider';
|
||||
import { LaconicMark } from '@/laconic-assets/laconic-mark';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { Organization } from 'gql-client';
|
||||
import { Menu, Plus, Shapes, Wallet } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { ProjectSearchBar } from '../search/ProjectSearchBar';
|
||||
import { ColorModeToggle } from './components/ColorModeToggle';
|
||||
import { GitHubSessionButton } from './components/GitHubSessionButton';
|
||||
import { WalletSessionId } from './components/WalletSessionId';
|
||||
|
||||
/**
|
||||
* TopNavigation Component
|
||||
*
|
||||
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=299-1294&m=dev
|
||||
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=299-1294&t=LEaCSA9ND6svuP3P-4
|
||||
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=298-242&t=LEaCSA9ND6svuP3P-4
|
||||
* https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=299-1294&m=dev
|
||||
* Renders the top navigation bar, adapting its layout for desktop and mobile views.
|
||||
* It includes the project search bar, navigation links, user authentication via GitHub,
|
||||
* color mode toggle, and wallet session information. On mobile, it utilizes a sheet
|
||||
* for a responsive and accessible navigation experience.
|
||||
*
|
||||
* @returns {React.ReactElement} A PopoverPrimitive.Root element containing the top navigation bar.
|
||||
*/
|
||||
export function TopNavigation() {
|
||||
const navigate = useNavigate();
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
const { octokit } = useOctokit();
|
||||
const client = useGQLClient();
|
||||
const [defaultOrg, setDefaultOrg] = useState<Organization | null>(null);
|
||||
const { isReady } = useWallet();
|
||||
|
||||
// Check if GitHub is connected
|
||||
const isGitHubConnected = Boolean(octokit);
|
||||
|
||||
// Fetch the default organization (first one in the list)
|
||||
const fetchDefaultOrganization = useCallback(async () => {
|
||||
try {
|
||||
const { organizations } = await client.getOrganizations();
|
||||
if (organizations && organizations.length > 0) {
|
||||
setDefaultOrg(organizations[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching organizations:', error);
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isGitHubConnected && isReady) {
|
||||
fetchDefaultOrganization();
|
||||
}
|
||||
}, [isGitHubConnected, fetchDefaultOrganization, isReady]);
|
||||
|
||||
const handleOnboardingClose = () => {
|
||||
setShowOnboarding(false);
|
||||
// Refresh organization data after onboarding
|
||||
fetchDefaultOrganization();
|
||||
};
|
||||
|
||||
// Navigate to create page with organization slug
|
||||
const handleCreateNew = () => {
|
||||
if (defaultOrg) {
|
||||
navigate(`/${defaultOrg.slug}/projects/create`);
|
||||
} else {
|
||||
// If no organization is available, show onboarding
|
||||
setShowOnboarding(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunOnboarding = () => {
|
||||
// Clear existing onboarding progress data
|
||||
localStorage.removeItem('onboarding_progress');
|
||||
localStorage.removeItem('onboarding_state');
|
||||
|
||||
// Force starting from connect step
|
||||
localStorage.setItem('onboarding_force_connect', 'true');
|
||||
|
||||
setShowOnboarding(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root>
|
||||
<div className="bg-background">
|
||||
{/* Top Navigation - Desktop */}
|
||||
<nav className="md:flex items-center justify-between hidden h-16 px-6 border-b">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Logo / Home Link */}
|
||||
{/* <Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="hover:bg-transparent p-0"
|
||||
> */}
|
||||
<Link to="/" className="flex items-center justify-center w-10 h-10">
|
||||
<LaconicMark />
|
||||
</Link>
|
||||
{/* </Button> */}
|
||||
|
||||
{/* Search Bar */}
|
||||
{/* <div className="w-96">
|
||||
<ProjectSearchBar
|
||||
onChange={(project) => {
|
||||
navigate(
|
||||
`/${project.organization.slug}/projects/${project.id}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* Navigation Items */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/deploy-tools/projects">
|
||||
<Button variant="ghost" asChild className="gap-2">
|
||||
<Shapes className="w-5 h-5" />
|
||||
<span>Projects</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="text-muted-foreground gap-2"
|
||||
>
|
||||
<Link to="/wallet">
|
||||
<Button asChild variant="ghost">
|
||||
<Wallet className="w-5 h-5" />
|
||||
<span>Wallet</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Add New Button with Dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{isGitHubConnected && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={handleCreateNew}>
|
||||
Create New
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={handleRunOnboarding}>
|
||||
Run Onboarding
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* <NavigationActions /> */}
|
||||
<Button variant="ghost" asChild className="text-muted-foreground">
|
||||
<Link to="/support">Support</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="text-muted-foreground">
|
||||
<Link to="/docs">Documentation</Link>
|
||||
</Button>
|
||||
<GitHubSessionButton />
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Top Navigation - Mobile */}
|
||||
<nav className="md:hidden flex items-center justify-between h-16 px-4 border-b">
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<Button asChild variant="outline" size="icon">
|
||||
<div>
|
||||
<Menu className="w-4 h-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="w-[300px] sm:w-[400px] flex flex-col"
|
||||
>
|
||||
<nav className="flex flex-col flex-grow space-y-4">
|
||||
<div className="px-4 py-2">
|
||||
<ProjectSearchBar
|
||||
onChange={(project) => {
|
||||
navigate(
|
||||
`/${project.organization.slug}/projects/${project.id}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" asChild className="justify-start gap-2">
|
||||
<Link to="/projects">
|
||||
<Shapes className="w-5 h-5" />
|
||||
<span>Projects</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start gap-2">
|
||||
<Link to="/wallet">
|
||||
<Wallet className="w-5 h-5" />
|
||||
<span>Wallet</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start">
|
||||
<Link to="/support">Support</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start">
|
||||
<Link to="/docs">Documentation</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start"
|
||||
onClick={handleRunOnboarding}
|
||||
>
|
||||
Run Onboarding
|
||||
</Button>
|
||||
{isGitHubConnected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start"
|
||||
onClick={handleCreateNew}
|
||||
>
|
||||
Create New
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
<div className="flex items-center justify-between mt-auto">
|
||||
<GitHubSessionButton />
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Add New Button (Mobile) */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{isGitHubConnected && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={handleCreateNew}>
|
||||
Create New
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={handleRunOnboarding}>
|
||||
Run Onboarding
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Onboarding Dialog */}
|
||||
{showOnboarding && (
|
||||
<OnboardingDialog defaultOpen={true} onClose={handleOnboardingClose} />
|
||||
)}
|
||||
</PopoverPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
export function ColorModeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
function toggleTheme() {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="icon" onClick={toggleTheme}>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useGitHubAuth } from '@/hooks/useGitHubAuth';
|
||||
import { Github } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Renders a button that allows users to log in or log out with their GitHub account.
|
||||
* It displays the user's avatar and username when logged in, and provides a logout option.
|
||||
*
|
||||
* @returns {React.ReactElement} A DropdownMenu element containing the GitHub session button.
|
||||
*/
|
||||
export function GitHubSessionButton() {
|
||||
const { isAuthenticated, user, login, logout } = useGitHubAuth();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={`relative ${isAuthenticated ? 'text-green-500' : 'text-muted-foreground'}`}
|
||||
>
|
||||
<Github className="h-[1.2rem] w-[1.2rem]" />
|
||||
{isAuthenticated && (
|
||||
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-green-500" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end" className="w-56" sideOffset={4}>
|
||||
{isAuthenticated && user ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.avatar_url} alt={user.login} />
|
||||
<AvatarFallback>
|
||||
{user.login.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">
|
||||
{user.name || user.login}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{user.login}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuItem onClick={logout}>Log out</DropdownMenuItem>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={login}>
|
||||
Log in with GitHub
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* LaconicIconProps interface defines the props for the LaconicIcon component.
|
||||
*/
|
||||
interface LaconicIconProps {
|
||||
/**
|
||||
* Optional CSS class names to apply to the component.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The width of the icon.
|
||||
* @default 40
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* The height of the icon.
|
||||
* @default 40
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders the Laconic icon.
|
||||
*
|
||||
* @param {LaconicIconProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} An SVG element representing the Laconic icon.
|
||||
*/
|
||||
export const LaconicIcon: React.FC<LaconicIconProps> = ({
|
||||
className = '',
|
||||
width = 40,
|
||||
height = 40,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.7179 20.6493C14.6275 17.7397 16.4284 13.7219 16.4276 9.28566C16.4288 8.68351 16.3955 8.08808 16.3294 7.5L7.5 7.5008L7.50026 24.4654C7.49946 26.5218 8.28351 28.5788 9.85175 30.1469C11.4201 31.7151 13.4785 32.5001 15.5353 32.4991L32.5 32.5L32.4994 23.6694C31.9126 23.6048 31.3171 23.5713 30.7136 23.5711C26.2786 23.5718 22.2605 25.3725 19.351 28.2819C17.2337 30.346 13.8392 30.3464 11.7483 28.2554C9.65859 26.1656 9.65764 22.7701 11.7179 20.6493ZM30.6686 9.33579C28.2303 6.89759 24.2689 6.89665 21.8298 9.33579C19.3906 11.7748 19.3916 15.7361 21.8298 18.1743C24.2694 20.6138 28.2295 20.6134 30.6686 18.1743C33.1078 15.7353 33.1083 11.7752 30.6686 9.33579Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { Bell, Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Renders the navigation actions, including buttons for creating a new project and displaying notifications.
|
||||
*
|
||||
* @returns {React.ReactElement} A div element containing the navigation actions.
|
||||
*/
|
||||
export function NavigationActions() {
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
|
||||
/**
|
||||
* Fetches the organization slug.
|
||||
* @returns {Promise<string>} The organization slug.
|
||||
*/
|
||||
const fetchOrgSlug = useCallback(async () => {
|
||||
const { organizations } = await client.getOrganizations();
|
||||
// TODO: Get the selected organization. This is temp
|
||||
return organizations[0].slug;
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
fetchOrgSlug().then((organizationSlug) => {
|
||||
navigate(`/${organizationSlug}/projects/create`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useWallet } from '@/context/WalletContextProvider';
|
||||
import type React from 'react';
|
||||
/**
|
||||
* WalletSessionIdProps interface defines the props for the WalletSessionId component.
|
||||
*/
|
||||
interface WalletSessionIdProps {
|
||||
/**
|
||||
* The wallet ID to display.
|
||||
*/
|
||||
walletId?: string;
|
||||
/**
|
||||
* Optional CSS class names to apply to the component.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays the wallet session ID.
|
||||
*
|
||||
* @param {WalletSessionIdProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element containing the wallet session ID.
|
||||
*/
|
||||
export const WalletSessionId: React.FC<WalletSessionIdProps> = ({
|
||||
walletId,
|
||||
className = '',
|
||||
}) => {
|
||||
const { wallet, isConnected } = useWallet();
|
||||
// const wallet = { id: 'x123xxx' };
|
||||
console.log(wallet, 'from WalletSessionId.tsx');
|
||||
const displayId = wallet?.id || 'Wallet';
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md bg-secondary px-2.5 py-0.5 ${className}`}
|
||||
>
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-400' : 'bg-gray-400'}`}
|
||||
/>
|
||||
<span className="text-secondary-foreground text-xs font-semibold">
|
||||
{displayId}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export { ProjectSearchBar } from '../search/ProjectSearchBar';
|
||||
export { ColorModeToggle } from './components/ColorModeToggle';
|
||||
export { GitHubSessionButton } from './components/GitHubSessionButton';
|
||||
export { LaconicIcon } from './components/LaconicIcon';
|
||||
export { NavigationActions } from './components/NavigationActions';
|
||||
export { WalletSessionId } from './components/WalletSessionId';
|
||||
export { NavigationWrapper } from './NavigationWrapper';
|
||||
export { TopNavigation } from './TopNavigation';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* ActionButtonProps interface defines the props for the ActionButton component.
|
||||
*/
|
||||
interface ActionButtonProps {
|
||||
/**
|
||||
* The label of the button.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The icon to display in the button.
|
||||
*/
|
||||
icon: LucideIcon;
|
||||
/**
|
||||
* The variant of the button.
|
||||
* @default 'default'
|
||||
*/
|
||||
variant?: 'default' | 'outline';
|
||||
/**
|
||||
* Callback function to be called when the button is clicked.
|
||||
*/
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button component that displays an icon and a label, and triggers an action when clicked.
|
||||
*
|
||||
* @param {ActionButtonProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A Button element.
|
||||
*/
|
||||
export function ActionButton({
|
||||
label,
|
||||
icon: Icon,
|
||||
variant = 'default',
|
||||
onClick,
|
||||
}: ActionButtonProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleClick = () => {
|
||||
onClick?.();
|
||||
toast({
|
||||
title: 'Action Triggered',
|
||||
description: 'TODO: Connect action',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant={variant} onClick={handleClick} className="gap-2">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* HeaderProps interface defines the props for the Header component.
|
||||
*/
|
||||
interface HeaderProps {
|
||||
/**
|
||||
* The title of the header.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The subtitle of the header.
|
||||
*/
|
||||
subtitle?: string;
|
||||
/**
|
||||
* An array of actions to display in the header.
|
||||
*/
|
||||
actions?: ReactNode[];
|
||||
/**
|
||||
* A back button to display in the header.
|
||||
*/
|
||||
backButton?: ReactNode;
|
||||
/**
|
||||
* Optional CSS class names to apply to the header.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The layout of the header.
|
||||
* @default 'default'
|
||||
*/
|
||||
layout?: 'default' | 'compact';
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders a header with a title, subtitle, actions, and back button.
|
||||
*
|
||||
* @param {HeaderProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element containing the header content.
|
||||
*/
|
||||
export function Header({
|
||||
title,
|
||||
subtitle,
|
||||
actions,
|
||||
backButton,
|
||||
className,
|
||||
layout = 'default',
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col sm:flex-row sm:items-center sm:justify-between',
|
||||
layout === 'compact' ? 'mb-2' : 'mb-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{backButton}
|
||||
<div>
|
||||
<h1
|
||||
className={cn(
|
||||
'font-bold',
|
||||
layout === 'compact'
|
||||
? 'text-lg sm:text-xl'
|
||||
: 'text-xl sm:text-3xl',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{actions && actions.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2',
|
||||
layout === 'default' ? 'mt-4 sm:mt-0' : 'mt-2 sm:mt-0',
|
||||
)}
|
||||
>
|
||||
{actions.map((action, index) => (
|
||||
<div key={index}>{action}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* ScreenWrapperProps interface extends React.HTMLProps<HTMLDivElement> to include all standard HTML div attributes.
|
||||
*/
|
||||
interface ScreenWrapperProps extends React.HTMLProps<HTMLDivElement> {
|
||||
/**
|
||||
* Whether the screen should be padded.
|
||||
* @default true
|
||||
*/
|
||||
padded?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for screens, providing consistent padding and layout.
|
||||
*
|
||||
* @param {ScreenWrapperProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element containing the screen content.
|
||||
*/
|
||||
export function ScreenWrapper({
|
||||
children,
|
||||
className,
|
||||
// padded = true,
|
||||
...props
|
||||
}: ScreenWrapperProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 flex-col',
|
||||
'container mx-auto p-4 md:p-6 lg:p-8 ',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||