Compare commits

..
Author SHA1 Message Date
IshaVenikar a9f50a02f0 Set isCurrent to false only for deployments from same deployer 2024-10-16 15:37:09 +05:30
IshaVenikar e10c8f4818 Add wait before updating deployer LRNs after auction completion 2024-10-16 15:37:09 +05:30
IshaVenikar e3931b4bf8 Check for the last DNS deployment record when deleting 2024-10-16 15:37:09 +05:30
IshaVenikar 443a3f2b6e Refactor out common code for creating deployment 2024-10-16 15:37:09 +05:30
IshaVenikar 9d8d2199e2 Update Import project flow to configure deployment 2024-10-16 15:37:09 +05:30
IshaVenikar f374fa69ff Update project base domains after deployment is deleted 2024-10-16 15:37:09 +05:30
IshaVenikar 55e238d0b9 Update method for deleting deployment 2024-10-16 15:37:09 +05:30
IshaVenikar bf75dc8acc Update project entity 2024-10-16 15:37:09 +05:30
IshaVenikar e45cc45f38 Check auction status only if deployments don't exist 2024-10-16 15:37:09 +05:30
IshaVenikar ed2badebb6 Display bids in auction details 2024-10-16 15:37:09 +05:30
IshaVenikar b63837d432 Update auction card UI 2024-10-16 15:37:09 +05:30
IshaVenikar b3ac6e1367 Display auction details in overview page 2024-10-16 15:37:09 +05:30
IshaVenikar 508b4c7367 Update deployments only if valid request Id exists 2024-10-16 15:37:09 +05:30
IshaVenikar a662ebc018 test 2024-10-16 15:37:09 +05:30
IshaVenikar 614405a2f4 Display auction details in Overview tab 2024-10-16 15:37:09 +05:30
IshaVenikar fb873d9bc1 Pass auction Id in DNS deployment 2024-10-16 15:37:09 +05:30
IshaVenikar f67dbd0ff3 Pass auction id in deployment requests 2024-10-16 15:37:09 +05:30
IshaVenikar 22fb9323d7 Store deployer lrn for each deployment 2024-10-16 15:37:09 +05:30
IshaVenikar a9e69afe08 Update UI for configure deployment step 2024-10-16 15:37:09 +05:30
IshaVenikar 5fe04dd691 Check request Id for updating deployment data 2024-10-16 15:37:09 +05:30
IshaVenikar 012dd63a45 Navigate to success page after auction creation 2024-10-16 15:37:09 +05:30
IshaVenikar 52ae15bf62 Display loader for deploy button 2024-10-16 15:37:09 +05:30
IshaVenikar 82dab8ce21 Fix deployer LRN field in project 2024-10-16 15:37:09 +05:30
IshaVenikar 853a1024b3 Check for auction if deployment request id is not present 2024-10-16 15:37:09 +05:30
IshaVenikar dfeb281586 Pass auction data when adding project 2024-10-16 15:37:09 +05:30
IshaVenikar b58d9e6c21 Create deployments after auction creation 2024-10-16 15:37:09 +05:30
IshaVenikar 42d35cae84 Check for auction status in a loop 2024-10-16 15:37:09 +05:30
IshaVenikar 8c824f065b Add method deployment requests after auction completion 2024-10-16 15:37:09 +05:30
IshaVenikar 2a3c5de132 Set gas price in Registry instantiation 2024-10-16 15:37:09 +05:30
IshaVenikar 13730655a4 Implement UI to add configure deployment step 2024-10-16 15:37:09 +05:30
IshaVenikar ee9bf2de1c Update methods in gql client 2024-10-16 15:37:09 +05:30
IshaVenikar 9931bc74d1 Update schema and resolver functions 2024-10-16 15:37:09 +05:30
IshaVenikar 0e0e5e888f Take auction params from config 2024-10-16 15:37:09 +05:30
IshaVenikar d77e41f796 Add back-end function to create deployment with auction 2024-10-16 15:37:09 +05:30
42bdd21089 Upgrade from laconic-sdk to registry-sdk (#1)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#1
2024-10-16 08:43:51 +00:00
Gilbert e5a00016c1 Merge pull request #234 from snowball-tools/ng-deployment-test
Increase retry interval for checking deployment URL in CI test
2024-09-18 23:21:33 -05:00
nabarun 13b912d318 Increase retry interval for checking deployment URL 2024-08-23 11:09:12 +05:30
dboreham 059863c4b9 Merge pull request #233 from snowball-tools/dboreham-frontend-deploy-from-gh
Deploy frontend directly from github repo
2024-07-05 09:42:05 -06:00
dboreham 58906844cc Deploy directly from github repo 2024-07-05 09:36:05 -06:00
Vivian Phung 9f0a2ad548 space 2024-07-03 12:41:39 -04:00
Vivian Phung bd10e2cb35 fix: version temp 2024-06-25 01:34:21 -04:00
Vivian Phung eb32385cf3 revert fix 2024-06-25 01:14:08 -04:00
Vivian Phung aebb20b987 update README.md debugging deployer 2024-06-25 01:08:38 -04:00
Vivian Phung 8a2b51952f fix: temp frontend (#230)
* v.0.0.9 staging

* fix: temp frontend deployment bug
2024-06-25 01:04:03 -04:00
Vivian Phung 94f46f9621 v.0.0.9 staging (#229) 2024-06-24 19:52:51 -04:00
Vivian Phung e751addcce Update local.toml.example in packages/backend (#228)
### TL;DR

Added new keys for Google and Turnkey integration in `local.toml.example`

### What changed?

- Updated server session secret to empty string
- Added placeholders for google clientId and clientSecret
- Added placeholders for Turnkey API integration (apiBaseUrl, apiPrivateKey, apiPublicKey, defaultOrganizationId)

### How to test?

1. Pull the latest changes
2. Update `local.toml.example` file with actual values.
3. Run the application locally and ensure all services are working correctly.

### Why make this change?

To integrate new services (Google and Turnkey) into the project setup.

---
2024-06-24 19:42:33 -04:00
Vivian Phung c01f8fdabf Refactor: components/shared (#227)
### TL;DR

This pull request consolidates all shared component exports into a new `index.ts` file within the `components/shared` directory. The purpose is to streamline and centralize the exports of shared components for improved maintainability.

### What changed?

- Created a new `index.ts` file in the `components/shared` directory that exports all shared components.
- Updated import statements to use the new centralized `index.ts` file instead of individual component files.

### How to test?

1. Run the frontend application.
2. Navigate through the application to ensure all shared components render correctly without console errors.

### Why make this change?

The motivation behind this change is to organize exports of shared components in a single file. This enhances code maintainability and simplifies the process of importing shared components across the project.

---
2024-06-24 19:36:08 -04:00
Vivian Phung 2b60114dab refactor: use onToast remove react-hot-toast dep (#226)
### TL;DR

Removed the dependency on `react-hot-toast` in favor of a custom implementation for toast notifications.

### What changed?

1. Removed `react-hot-toast` from `package.json` and `yarn.lock`. 
2. Updated `EnvironmentVariables` and `Config` to use the custom toast notification system.

### Why make this change?

To reduce bundle size and have more control over the toast notification system.

### How to test?

1. Navigate to the Environment Variables settings for a project. Try adding and removing environment variables to see the new toast notifications in action.
2. Navigate to the Domains settings for a project and try adding a new domain to view the custom toast notifications.

---
2024-06-24 19:25:31 -04:00
Vivian Phung 9a1c0e8338 feat(domains): DomainCard and WebhookCard styling start (#225)
### TL;DR

Refactored the `DomainCard`, `EditDomainDialog`, and `WebhookCard` components to improve code readability and enhance UI using new shared components like `Tag`, `Heading`, `Button`, and `CustomIcon`.

### What changed?

- `DomainCard` component:
  - Replaced `Chip` with `Tag` component.
  - Used `Heading`, `Button`, and `CustomIcon` components.
  - Updated refresh icon to show `LoadingIcon` when checking.
- `EditDomainDialog` component:
  - Used `useToast` hook for toast messages.
- `WebhookCard` component:
  - Used `Input`, `Button`, and `CustomIcon` components for better UI.
- Added Storybook stories for the updated components.

### How to test?

1. Go to the project settings page.
2. Verify the `DomainCard` UI updates.
3. Edit a domain and check the toasts.
4. Verify the `WebhookCard` UI and functionality.
5. Run Storybook and inspect the added stories for the components.

### Why make this change?

To improve the consistency and user experience of the project settings UI, and to make the components more maintainable by using shared components.

---
2024-06-24 19:22:20 -04:00
Vivian Phung 1b038476c7 feat: Settings screen for org (#224)
This update introduces a new header section within the `Settings` page. It incorporates a `Heading` component from the shared components library, ensuring consistent styling and structure across the app. The `Settings` page layout now features appropriate padding and flexbox for better alignment and spacing.

---
2024-06-24 18:57:38 -04:00
Vivian Phung 4a78eb13f6 fix: ProjectSearchBarDialog (search small screen) Suggestions once (#223)
Refactor the rendering logic of the suggestion list in `ProjectSearchBarDialog` component. This change simplifies the conditional rendering by restructuring the JSX to be more readable and maintainable. Now, the 'Suggestions' label is rendered once if there are items, and the items are mapped afterward.

---
2024-06-24 18:54:32 -04:00
Vivian Phung 41bcb2e7d0 fix: ProjectSearchBarDialog suppressRefError (#222)
This PR resolves issues with the Project Search Bar component where `getMenuProps` was causing reference errors. By adding `suppressRefError: true` to `getMenuProps` in both `ProjectSearchBar` and `ProjectSearchBarDialog`, the warnings are suppressed.

---
2024-06-24 18:51:31 -04:00
Vivian Phung f981f1a3f6 fix(ProjectSearchBarDialog): getMenuProps error (#221)
- Replaced `useDebounce` with `useDebounceValue` for better type inference and simplicity
- Added `getMenuProps` to `useCombobox` to support better accessibility and usability
- Minor style tweak to improve `ProjectSearch` header hover effect
- Created Storybook stories for the `ProjectSearchBar` component

---
2024-06-24 18:48:32 -04:00
Vivian Phung dee84f18cb feat: dynamic project success page and update links (#220)
### TL;DR

Integrates project data fetching for dynamic subdomain display on the Project Deployment Success page and the OverviewTabPanel.

### What changed?

- Updated `Id.tsx` to fetch project data and dynamically display project's subdomain after deployment.
- Modified `Overview.tsx` to make project's subdomain a clickable link.

### How to test?

1. Deploy a new project and check the deployment success page for correct subdomain display.
2. Open a project's overview tab and click the subdomain link to ensure it navigates correctly.

### Why make this change?

Improves user experience by displaying the actual subdomain and making it clickable, ensuring users can conveniently verify their deployment and access project domain.

---
2024-06-24 18:44:34 -04:00
Vivian Phung 44015d5451 feat(ui): loading spinner on project creation (#219)
### TL;DR
This pull request adds a loading icon to the 'Deploy' button in the project template creation form.

### What changed?
- Imported `LoadingIcon` from `components/shared/CustomIcon`.
- Modified the `Button` component to conditionally display the `LoadingIcon` when `isLoading` is `true`. The icon will animate by spinning.

### How to test?
1. Navigate to the project template creation form.
2. Fill in the required fields.
3. Click the 'Deploy' button.
4. Ensure the loading icon appears and spins when the button is disabled (when `isLoading` is `true`).

### Why make this change?
This change provides visual feedback to users, indicating that their action is being processed, thereby enhancing user experience.

---
2024-06-24 18:41:31 -04:00
Vivian Phung a684743bd6 feat(template projects): generate git repo on backend (#218)
### TL;DR

- Still cretaes app if user migrates from page

The PR introduces a new `AddProjectFromTemplate` mutation to facilitate project creation using a repository template. This change centralizes the template project creation logic within the backend, improving code maintainability by removing redundant client-side code.

### What changed?
- Added `AddProjectFromTemplate` input type in `schema.gql` and corresponding TypeScript interfaces.
- Implemented `addProjectFromTemplate` resolver with error handling and Octokit integration for repository creation.
- Updated `service.ts` to include the new `addProjectFromTemplate` method.
- Created new GraphQL `Mutation` for `addProjectFromTemplate` in the GraphQL schema.
- Adjusted the client-side GQLClient to support the new mutation.
- Modified frontend to utilize the new backend mutation for project creation from a template.

### How to test?
1. Ensure your backend server is running.
2. Use a GraphQL client like Postman to call the `addProjectFromTemplate` mutation with appropriate input.
3. Verify that the new project is created using the specified template, and appropriate error messages are returned for failures.
4. Check the frontend flow for creating a project from a template to ensure it is working correctly.

### Why make this change?
This change enhances code maintainability by centralizing template project creation logic within the backend, thereby reducing redundancy and potential inconsistencies in client-side implementations.

---
2024-06-24 18:38:01 -04:00
Vivian Phung b12c95b2ff fix(readme) 2024-06-24 15:00:45 -04:00
Vivian Phung a4d9211ffe Refactor(README.md) (#217)
This pull request refactors the main `README.md` by removing detailed backend and frontend setup instructions and adding separate `README.md` files for the backend and frontend with their specific setup and deployment instructions. This makes the main `README.md` cleaner and directs users to specific READMEs for backend and frontend setups.

---
2024-06-22 18:07:47 -04:00
Vivian Phung af31fac3ee chore(storybook): icons to correct folder (#216)
This pull request refactors several aspects of the frontend codebase focused on Storybook configurations and icon story files. It replaces `args` with `argTypes` to enhance control configurations, adds `staticDirs` for public assets in Storybook, and standardizes the titles of some icon stories.
2024-06-22 17:27:37 -04:00
Vivian Phung 54ae3f429d fix(staging): github client id on frontend staging deployment script (#214)
### TL;DR
Update references to the new GitHub repository URLs and make minor formatting fixes in deployment scripts.

### What changed?
Updated GitHub repository URLs from `snowball-tools-platform` to `snowball-tools` in various deployment and test scripts. Made minor formatting adjustments including spacing and indentation.

### How to test?
Run the deployment scripts in their respective environments to ensure they reference the correct repository URLs and all functionalities work as expected. Check for successful creation and updation of application records.

### Why make this change?
This change was made to reflect the new repository structure and ensure consistency across all deployment scripts. The minor formatting fixes improve code readability.

---
2024-06-22 17:24:40 -04:00
Vivian Phung acfe78bf07 chore: ignore .DS_Store 2024-06-22 17:05:34 -04:00
Vivian Phung ce1833cb51 bump: version (#213)
This pull request updates the backend version endpoint to return version '0.0.8' instead of '0.0.7'.

---
2024-06-21 21:16:34 -04:00
Vivian Phung f2e59c11fd Refactor: VerifyCodeInput Component and Modify Access Code Validation (#212)
### TL;DR

Implemented the new `VerifyCodeInput` component and updated the access code validation logic.

### What changed?

1. Added a new reusable `VerifyCodeInput` component for verifying codes in a user-friendly way. This component handles paste events, input changes, and keyboard navigation.
2. Updated the backend route `/accesscode` to accept an arbitrary code for now. 
3. Incorporated the `VerifyCodeInput` component into the `AccessCode` page, replacing the generic `Input` component.
4. Updated the access code validation logic to check for a trimmed length of 5 characters instead of 6.
5. Added a slight pause for UX purposes when validating the access code on the frontend.

### How to test?

1. Go to the Access Code page.
2. Try entering an access code with various inputs (keyboard, paste, etc.) to see if it works seamlessly.
3. Verify that only a 5-digit code is considered valid.
4. Check the backend logs to ensure the validation endpoint is working as expected.

### Why make this change?

This change improves the user experience by providing a custom input component for access code verification and ensures that the access code validation meets the new requirements.

---
2024-06-21 21:13:32 -04:00
Vivian Phung b261e7e436 Feat: Access Code Authentication Flow (#211)
## What changed?

This Pull Request introduces an access code validation feature to the authentication process. Changes encompass backend route for access code validation, new frontend components for handling access code input, and integration of the access code verification in the signup flow.

### Backend:
- Added POST `/accesscode` route in `auth.ts` for validating access codes.

### Frontend:
- Created `AccessCode` component for access code input and validation.
- Added `AccessSignUp` component that integrates access code verification before signup.
- Updated `SignUp` component to check for valid access code on mount.
- Modified `SnowballAuth` to use new `AccessSignUp` instead of `SignUp`.
- Added `verifyAccessCode` utility function for code verification API call.

## How to test?
1. Run the backend and frontend projects.
2. Navigate to the signup page. You should be prompted to enter an access code.
3. Enter the code `444444` and proceed. Any other code should display an error message.
4. Verify that valid access code routes to the signup component.

## Why make this change?

This change improves authentication by adding an extra layer of security through access code verification.
2024-06-21 21:10:31 -04:00
Vivian Phung 934aa1a26b Refactor: Env to utils/constants (#210)
This PR centralizes all the environment variable references into a single constants file. The change includes replacing various `import.meta.env` references with imports from the new `utils/constants` module. This improves maintainability by providing a single place to manage environment variables.
2024-06-21 21:07:41 -04:00
104 changed files with 3948 additions and 1249 deletions
+29
View File
@@ -0,0 +1,29 @@
name: Lint
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
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:
node-version: ${{ matrix.node-version }}
- run: yarn
- name: Build libs
run: yarn workspace gql-client run build
- name: Linter check
run: yarn lint
+4 -1
View File
@@ -6,4 +6,7 @@ yarn-error.log
packages/backend/environments/local.toml packages/backend/environments/local.toml
packages/backend/dev/ packages/backend/dev/
packages/frontend/dist/ packages/frontend/dist/
# ignore all .DS_Store files
**/.DS_Store
+13 -210
View File
@@ -1,220 +1,23 @@
# snowballtools-base # snowballtools-base
## Setup This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
- Clone the `snowballtools-base` repo ## Getting Started
```bash ### Install dependencies
git clone git@github.com:snowball-tools/snowballtools-base.git
cd snowballtools-base
```
- Install dependencies in root In the root of the project, run:
```bash ```zsh
yarn yarn
``` ```
- Build packages ### Build backend
```bash ```zsh
yarn build --ignore frontend yarn build --ignore frontend
``` ```
Let us assume the following domains for backend and frontend ### Environment variables, running the development server, and deployment
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
- Configuration Files Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.
- For Backend:
```bash
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
```
- Production
- Update the following in backend [config file](packages/backend/environments/local.toml)
```toml
[server]
...
[server.session]
# Secret should be changed to a different random string
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
# Set URL of the frontend app
appOriginUrl = "https://dashboard.snowballtools.com"
# Set to true for session cookies to work behind proxy
trustProxy = true
# Set empty domain when using secure connection
domain = ""
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `https://dashboard.snowballtools.com`
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
- Generate a new client secret after app is created
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://api.snowballtools.com"
```
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
```toml
[registryConfig]
fetchDeploymentRecordDelay = 5000
# Use actual port for REST endpoint
restEndpoint = "http://api.laconic.com:1317"
# Use actual port for GQL endpoint
gqlEndpoint = "http://api.laconic.com:9473/api"
# Set private key of account to be used in Laconicd
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
# Set Bond ID to be used for publishing records
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
chainId = "laconic_9000-1"
# Set authority that is existing in the chain
authority = "laconic"
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "750000"
```
- Development
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `http://localhost:3000`
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Setup Laconicd
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
```bash
laconic-so deployment --dir laconic-loaded-deployment exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
```
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Setup ngrok for GitHub webhooks
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```bash
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
```
- For Frontend:
```bash
cp packages/frontend/.env.example packages/frontend/.env
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
VITE_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```env
VITE_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
```
- Production
- Set the following values in [.env](packages/frontend/.env) file
```env
VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Set project home page URL to `https://dashboard.snowballtools.com`
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
VITE_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
python3 -m http.server -d build 3000
```
- Development
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
```env
VITE_SERVER_URL = 'http://localhost:8000'
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Project home page URL is not required to be set
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
VITE_WALLET_CONNECT_ID = <Project_ID>
```
- The React application will be running in `http://localhost:3000/`
## Development
- Start the backend server
```bash
yarn start:backend
```
- Start the frontend
```bash
yarn start
```
+76 -1
View File
@@ -1 +1,76 @@
# Backend for Snowball Tools # backend
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
## Getting Started
### Install dependencies
In the root of the project, run:
```zsh
yarn
```
### Build backend
```zsh
yarn build --ignore frontend
```
### Environment variables
#### Local
Copy the `environments/local.toml.example` file to `environments/local.toml`:
```zsh
cp environments/local.toml.example environments/local.toml
```
#### Staging environment variables
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
#### Production environment variables
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
### Run development server
```zsh
yarn start
```
## Deployment
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
```
### Staging
```zsh
echo trigger >> .gitea/workflows/triggers/staging-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Production
```zsh
echo trigger >> .gitea/workflows/triggers/production-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Deployment status
Dumb for now
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
@@ -3,7 +3,7 @@
port = 8000 port = 8000
gqlPath = "/graphql" gqlPath = "/graphql"
[server.session] [server.session]
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi" secret = ""
appOriginUrl = "http://localhost:3000" appOriginUrl = "http://localhost:3000"
trustProxy = false trustProxy = false
domain = "localhost" domain = "localhost"
@@ -17,8 +17,19 @@
clientId = "" clientId = ""
clientSecret = "" clientSecret = ""
[google]
clientId = ""
clientSecret = ""
[turnkey]
apiBaseUrl = "https://api.turnkey.com"
apiPrivateKey = ""
apiPublicKey = ""
defaultOrganizationId = ""
[registryConfig] [registryConfig]
fetchDeploymentRecordDelay = 5000 fetchDeploymentRecordDelay = 5000
checkAuctionStatusDelay = 5000
restEndpoint = "http://localhost:1317" restEndpoint = "http://localhost:1317"
gqlEndpoint = "http://localhost:9473/api" gqlEndpoint = "http://localhost:9473/api"
chainId = "laconic_9000-1" chainId = "laconic_9000-1"
@@ -26,9 +37,16 @@
bondId = "" bondId = ""
authority = "" authority = ""
[registryConfig.fee] [registryConfig.fee]
amount = "200000" gas = ""
denom = "aphoton" fees = ""
gas = "750000" gasPrice = "1alnt"
[auction]
commitFee = "1000"
commitsDuration = "60s"
revealFee = "1000"
revealsDuration = "60s"
denom = "alnt"
[misc] [misc]
projectDomain = "apps.snowballtools.com" projectDomain = "apps.snowballtools.com"
+1 -1
View File
@@ -4,10 +4,10 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@cerc-io/registry-sdk": "^0.2.11",
"@graphql-tools/schema": "^10.0.2", "@graphql-tools/schema": "^10.0.2",
"@graphql-tools/utils": "^10.0.12", "@graphql-tools/utils": "^10.0.12",
"@octokit/oauth-app": "^6.1.0", "@octokit/oauth-app": "^6.1.0",
"@snowballtools/laconic-sdk": "^0.1.17",
"@turnkey/sdk-server": "^0.1.0", "@turnkey/sdk-server": "^0.1.0",
"@types/debug": "^4.1.5", "@types/debug": "^4.1.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
+12 -2
View File
@@ -34,14 +34,23 @@ export interface RegistryConfig {
privateKey: string; privateKey: string;
bondId: string; bondId: string;
fetchDeploymentRecordDelay: number; fetchDeploymentRecordDelay: number;
checkAuctionStatusDelay: number;
authority: string; authority: string;
fee: { fee: {
amount: string;
denom: string;
gas: string; gas: string;
fees: string;
gasPrice: string;
}; };
} }
export interface AuctionConfig {
commitFee: string;
commitsDuration: string;
revealFee: string;
revealsDuration: string;
denom: string;
}
export interface MiscConfig { export interface MiscConfig {
projectDomain: string; projectDomain: string;
} }
@@ -51,6 +60,7 @@ export interface Config {
database: DatabaseConfig; database: DatabaseConfig;
gitHub: GitHubConfig; gitHub: GitHubConfig;
registryConfig: RegistryConfig; registryConfig: RegistryConfig;
auction: AuctionConfig;
misc: MiscConfig; misc: MiscConfig;
turnkey: { turnkey: {
apiBaseUrl: string; apiBaseUrl: string;
-2
View File
@@ -465,8 +465,6 @@ export class Database {
id: organizationId id: organizationId
}); });
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
+11 -3
View File
@@ -33,6 +33,8 @@ export interface ApplicationDeploymentRequest {
version: string; version: string;
name: string; name: string;
application: string; application: string;
lrn?: string;
auction?: string;
config: string; config: string;
meta: string; meta: string;
} }
@@ -112,19 +114,22 @@ export class Deployment {
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null; applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
applicationDeploymentRemovalRequestId!: string | null; applicationDeploymentRemovalRequestId!: string | null;
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null; applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
applicationDeploymentRemovalRecordId!: string | null; applicationDeploymentRemovalRecordId!: string | null;
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null; applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
@Column('varchar')
deployerLrn!: string;
@Column({ @Column({
enum: Environment enum: Environment
}) })
@@ -133,6 +138,9 @@ export class Deployment {
@Column('boolean', { default: false }) @Column('boolean', { default: false })
isCurrent!: boolean; isCurrent!: boolean;
@Column('varchar', { nullable: true })
baseDomain!: string | null;
@Column({ @Column({
enum: DeploymentStatus enum: DeploymentStatus
}) })
@@ -147,7 +155,7 @@ export class Deployment {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@DeleteDateColumn() @DeleteDateColumn()
deletedAt!: Date | null; deletedAt!: Date | null;
} }
+8 -2
View File
@@ -46,6 +46,12 @@ export class Project {
@Column('text', { default: '' }) @Column('text', { default: '' })
description!: string; description!: string;
@Column('varchar', { nullable: true })
auctionId!: string | null;
@Column({ type: 'simple-array', nullable: true })
deployerLrns!: string[] | null;
// TODO: Compute template & framework in import repository // TODO: Compute template & framework in import repository
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
template!: string | null; template!: string | null;
@@ -61,8 +67,8 @@ export class Project {
@Column('varchar') @Column('varchar')
icon!: string; icon!: string;
@Column('varchar') @Column({ type: 'simple-array', nullable: true })
subDomain!: string; baseDomains!: string[] | null;
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;
+202 -60
View File
@@ -1,9 +1,11 @@
import debug from 'debug';
import assert from 'assert'; import assert from 'assert';
import { inc as semverInc } from 'semver'; import debug from 'debug';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Octokit } from 'octokit';
import { inc as semverInc } from 'semver';
import { DeepPartial } from 'typeorm';
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk'; import { Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { import {
@@ -12,49 +14,53 @@ import {
ApplicationDeploymentRequest, ApplicationDeploymentRequest,
ApplicationDeploymentRemovalRequest ApplicationDeploymentRemovalRequest
} from './entity/Deployment'; } from './entity/Deployment';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types'; import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionData } from './types';
import { sleep } from './utils'; import { getConfig, getRepoDetails, sleep } from './utils';
import { Auction } from '@cerc-io/registry-sdk/dist/proto/cerc/auction/v1/auction';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord'; const APP_RECORD_TYPE = 'ApplicationRecord';
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest'; const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'; const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
const SLEEP_DURATION = 1000; 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 { export class Registry {
private registry: LaconicRegistry; private registry: LaconicRegistry;
private registryConfig: RegistryConfig; private registryConfig: RegistryConfig;
constructor (registryConfig: RegistryConfig) { constructor(registryConfig: RegistryConfig) {
this.registryConfig = registryConfig; this.registryConfig = registryConfig;
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
this.registry = new LaconicRegistry( this.registry = new LaconicRegistry(
registryConfig.gqlEndpoint, registryConfig.gqlEndpoint,
registryConfig.restEndpoint, registryConfig.restEndpoint,
registryConfig.chainId { chainId: registryConfig.chainId, gasPrice }
); );
} }
async createApplicationRecord ({ async createApplicationRecord({
appName, octokit,
packageJSON, repository,
commitHash, commitHash,
appType, appType,
repoUrl
}: { }: {
appName: string; octokit: Octokit
packageJSON: PackageJSON; repository: string;
commitHash: string; commitHash: string;
appType: string; appType: string;
repoUrl: string;
}): Promise<{ }): Promise<{
applicationRecordId: string; applicationRecordId: string;
applicationRecordData: ApplicationRecord; 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 // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
// Fetch previous records // Fetch previous records
const records = await this.registry.queryRecords( const records = await this.registry.queryRecords(
@@ -87,7 +93,7 @@ export class Registry {
repository_ref: commitHash, repository_ref: commitHash,
repository: [repoUrl], repository: [repoUrl],
app_type: appType, app_type: appType,
name: appName, name: repo,
...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }), ...(packageJSON.license && { license: packageJSON.license }),
@@ -100,68 +106,145 @@ export class Registry {
...(packageJSON.version && { app_version: packageJSON.version }) ...(packageJSON.version && { app_version: packageJSON.version })
}; };
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord( const result = await this.registry.setRecord(
{ {
privateKey: this.registryConfig.privateKey, privateKey: this.registryConfig.privateKey,
record: applicationRecord, record: applicationRecord,
bondId: this.registryConfig.bondId bondId: this.registryConfig.bondId
}, },
'', this.registryConfig.privateKey,
this.registryConfig.fee fee
); );
log(`Published application record ${result.id}`);
log('Application record data:', applicationRecord); log('Application record data:', applicationRecord);
// TODO: Discuss computation of CRN // TODO: Discuss computation of LRN
const crn = this.getCrn(appName); const lrn = this.getLrn(repo);
log(`Setting name: ${crn} for record ID: ${result.data.id}`); log(`Setting name: ${lrn} for record ID: ${result.id}`);
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await this.registry.setName( await this.registry.setName(
{ cid: result.data.id, crn }, {
cid: result.id,
lrn
},
this.registryConfig.privateKey, this.registryConfig.privateKey,
this.registryConfig.fee fee
);
await sleep(SLEEP_DURATION);
await this.registry.setName(
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
this.registryConfig.privateKey,
this.registryConfig.fee
); );
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await this.registry.setName( await this.registry.setName(
{ {
cid: result.data.id, cid: result.id,
crn: `${crn}@${applicationRecord.repository_ref}` lrn: `${lrn}@${applicationRecord.app_version}`
}, },
this.registryConfig.privateKey, this.registryConfig.privateKey,
this.registryConfig.fee fee
);
await sleep(SLEEP_DURATION);
await this.registry.setName(
{
cid: result.id,
lrn: `${lrn}@${applicationRecord.repository_ref}`
},
this.registryConfig.privateKey,
fee
); );
return { return {
applicationRecordId: result.data.id, applicationRecordId: result.id,
applicationRecordData: applicationRecord applicationRecordData: applicationRecord
}; };
} }
async createApplicationDeploymentRequest (data: { async createApplicationDeploymentAuction(
appName: string,
octokit: Octokit,
auctionData: AuctionData,
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 this.registry.createProviderAuction(
{
commitFee: auctionConfig.commitFee,
commitsDuration: auctionConfig.commitsDuration,
revealFee: auctionConfig.revealFee,
revealsDuration: auctionConfig.revealsDuration,
denom: auctionConfig.denom,
maxPrice: auctionData.maxPrice,
numProviders: auctionData.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.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
record: applicationDeploymentAuction,
bondId: this.registryConfig.bondId
},
this.registryConfig.privateKey,
fee
);
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, deployment: Deployment,
appName: string, appName: string,
repository: string, repository: string,
auctionId?: string,
lrn: string,
environmentVariables: { [key: string]: string }, environmentVariables: { [key: string]: string },
dns: string, dns: string,
}): Promise<{ }): Promise<{
applicationDeploymentRequestId: string; applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest; applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> { }> {
const crn = this.getCrn(data.appName); const lrn = this.getLrn(data.appName);
const records = await this.registry.resolveNames([crn]); const records = await this.registry.resolveNames([lrn]);
const applicationRecord = records[0]; const applicationRecord = records[0];
if (!applicationRecord) { if (!applicationRecord) {
throw new Error(`No record found for ${crn}`); throw new Error(`No record found for ${lrn}`);
} }
// Create record of type ApplicationDeploymentRequest and publish // Create record of type ApplicationDeploymentRequest and publish
@@ -169,12 +252,10 @@ export class Registry {
type: APP_DEPLOYMENT_REQUEST_TYPE, type: APP_DEPLOYMENT_REQUEST_TYPE,
version: '1.0.0', version: '1.0.0',
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
application: `${crn}@${applicationRecord.attributes.app_version}`, application: `${lrn}@${applicationRecord.attributes.app_version}`,
dns: data.dns, dns: data.dns,
// TODO: Not set in test-progressive-web-app CI // 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 // https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
config: JSON.stringify({ config: JSON.stringify({
env: data.environmentVariables env: data.environmentVariables
@@ -185,32 +266,65 @@ export class Registry {
)}`, )}`,
repository: data.repository, repository: data.repository,
repository_ref: data.deployment.commitHash repository_ref: data.deployment.commitHash
}) }),
deployer: data.lrn,
...(data.auctionId && { auction: data.auctionId }),
}; };
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord( const result = await this.registry.setRecord(
{ {
privateKey: this.registryConfig.privateKey, privateKey: this.registryConfig.privateKey,
record: applicationDeploymentRequest, record: applicationDeploymentRequest,
bondId: this.registryConfig.bondId bondId: this.registryConfig.bondId
}, },
'', this.registryConfig.privateKey,
this.registryConfig.fee fee
); );
log(`Application deployment request record published: ${result.data.id}`); log(`Application deployment request record published: ${result.id}`);
log('Application deployment request data:', applicationDeploymentRequest); log('Application deployment request data:', applicationDeploymentRequest);
return { return {
applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestId: result.id,
applicationDeploymentRequestData: applicationDeploymentRequest applicationDeploymentRequestData: applicationDeploymentRequest
}; };
} }
async getAuctionWinningDeployers(
auctionId: string
): Promise<string[]> {
const records = await this.registry.getAuctionsByIds([auctionId]);
const auctionResult = records[0];
let deployerLrns = [];
const { winnerAddresses } = auctionResult;
for (const auctionWinner of winnerAddresses) {
const deployerRecords = await this.registry.queryRecords(
{
paymentAddress: auctionWinner,
},
true
);
for (const record of deployerRecords) {
if (record.names && record.names.length > 0) {
deployerLrns.push(record.names[0]);
break;
}
}
}
return deployerLrns;
}
/** /**
* Fetch ApplicationDeploymentRecords for deployments * Fetch ApplicationDeploymentRecords for deployments
*/ */
async getDeploymentRecords ( async getDeploymentRecords(
deployments: Deployment[] deployments: Deployment[]
): Promise<AppDeploymentRecord[]> { ): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
@@ -222,11 +336,11 @@ export class Registry {
true true
); );
// Filter records with ApplicationRecord ID and Deployment specific URL // Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
return records.filter((record: AppDeploymentRecord) => return records.filter((record: AppDeploymentRecord) =>
deployments.some( deployments.some(
(deployment) => (deployment) =>
deployment.applicationRecordId === record.attributes.application && deployment.applicationDeploymentRequestId === record.attributes.request &&
record.attributes.url.includes(deployment.id) record.attributes.url.includes(deployment.id)
) )
); );
@@ -235,7 +349,7 @@ export class Registry {
/** /**
* Fetch ApplicationDeploymentRecords by filter * Fetch ApplicationDeploymentRecords by filter
*/ */
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> { async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
return this.registry.queryRecords( return this.registry.queryRecords(
{ {
type: APP_DEPLOYMENT_RECORD_TYPE, type: APP_DEPLOYMENT_RECORD_TYPE,
@@ -248,7 +362,7 @@ export class Registry {
/** /**
* Fetch ApplicationDeploymentRemovalRecords for deployments * Fetch ApplicationDeploymentRemovalRecords for deployments
*/ */
async getDeploymentRemovalRecords ( async getDeploymentRemovalRecords(
deployments: Deployment[] deployments: Deployment[]
): Promise<AppDeploymentRemovalRecord[]> { ): Promise<AppDeploymentRemovalRecord[]> {
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments // Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
@@ -269,8 +383,9 @@ export class Registry {
); );
} }
async createApplicationDeploymentRemovalRequest (data: { async createApplicationDeploymentRemovalRequest(data: {
deploymentId: string; deploymentId: string;
deployerLrn: string;
}): Promise<{ }): Promise<{
applicationDeploymentRemovalRequestId: string; applicationDeploymentRemovalRequestId: string;
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest; applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
@@ -278,30 +393,57 @@ export class Registry {
const applicationDeploymentRemovalRequest = { const applicationDeploymentRemovalRequest = {
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE, type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
version: '1.0.0', version: '1.0.0',
deployment: data.deploymentId deployment: data.deploymentId,
deployer: data.deployerLrn
}; };
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord( const result = await this.registry.setRecord(
{ {
privateKey: this.registryConfig.privateKey, privateKey: this.registryConfig.privateKey,
record: applicationDeploymentRemovalRequest, record: applicationDeploymentRemovalRequest,
bondId: this.registryConfig.bondId bondId: this.registryConfig.bondId
}, },
'', this.registryConfig.privateKey,
this.registryConfig.fee fee
); );
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); log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
return { return {
applicationDeploymentRemovalRequestId: result.data.id, applicationDeploymentRemovalRequestId: result.id,
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
}; };
} }
getCrn (appName: string): string { async getCompletedAuctionIds(auctionIds: (string | null | undefined)[]): Promise<string[] | null> {
const validAuctionIds = auctionIds.filter((id): id is string => id !== null && id !== undefined);
if (!validAuctionIds.length) {
return null;
}
const auctions = await this.registry.getAuctionsByIds(validAuctionIds);
const completedAuctions = auctions
.filter((auction: Auction) => auction.status === 'completed')
.map((auction: Auction) => auction.id);
return completedAuctions;
}
async getRecordsByName(name: string): Promise<any> {
return this.registry.resolveNames([name]);
}
async getAuctionData(auctionId: string): Promise<any> {
return this.registry.getAuctionsByIds([auctionId]);
}
getLrn(appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist"); assert(this.registryConfig.authority, "Authority doesn't exist");
return `crn://${this.registryConfig.authority}/applications/${appName}`; return `lrn://${this.registryConfig.authority}/applications/${appName}`;
} }
} }
+36 -2
View File
@@ -6,6 +6,7 @@ import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { AddProjectFromTemplateInput, AuctionData } from './types';
const log = debug('snowball:resolver'); const log = debug('snowball:resolver');
@@ -68,6 +69,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
) => { ) => {
return service.getDomainsByProjectId(projectId, filter); return service.getDomainsByProjectId(projectId, filter);
}, },
getAuctionData: async (
_: any,
{ auctionId }: { auctionId: string },
) => {
return service.getAuctionData(auctionId);
},
}, },
// TODO: Return error in GQL response // TODO: Return error in GQL response
@@ -197,16 +205,42 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addProjectFromTemplate: async (
_: any,
{
organizationSlug,
data,
lrn,
auctionData
}: { organizationSlug: string; data: AddProjectFromTemplateInput; lrn: string; auctionData: AuctionData },
context: any,
) => {
try {
return await service.addProjectFromTemplate(
context.user,
organizationSlug,
data,
lrn,
auctionData
);
} catch (err) {
log(err);
throw err;
}
},
addProject: async ( addProject: async (
_: any, _: any,
{ {
organizationSlug, organizationSlug,
data, data,
}: { organizationSlug: string; data: DeepPartial<Project> }, lrn,
auctionData
}: { organizationSlug: string; data: DeepPartial<Project>; lrn: string; auctionData: AuctionData },
context: any, context: any,
) => { ) => {
try { try {
return await service.addProject(context.user, organizationSlug, data); return await service.addProject(context.user, organizationSlug, data, lrn, auctionData);
} catch (err) { } catch (err) {
log(err); log(err);
throw err; throw err;
+13
View File
@@ -5,6 +5,19 @@ import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router(); 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 // Turnkey
// //
+1 -1
View File
@@ -3,7 +3,7 @@ import { Router } from 'express';
const router = Router(); const router = Router();
router.get('/version', async (req, res) => { router.get('/version', async (req, res) => {
return res.send({ version: '0.0.7' }); return res.send({ version: '0.0.9' });
}); });
export default router; export default router;
+75 -2
View File
@@ -22,6 +22,13 @@ enum DeploymentStatus {
Deleting Deleting
} }
enum AuctionStatus {
completed
reveal
commit
expired
}
enum DomainStatus { enum DomainStatus {
Live Live
Pending Pending
@@ -65,6 +72,8 @@ type Project {
repository: String! repository: String!
prodBranch: String! prodBranch: String!
description: String description: String
deployerLrns: [String]
auctionId: String
template: String template: String
framework: String framework: String
webhooks: [String!] webhooks: [String!]
@@ -74,7 +83,7 @@ type Project {
updatedAt: String! updatedAt: String!
organization: Organization! organization: Organization!
icon: String icon: String
subDomain: String baseDomains: [String!]
} }
type ProjectMember { type ProjectMember {
@@ -94,7 +103,9 @@ type Deployment {
commitMessage: String! commitMessage: String!
url: String url: String
environment: Environment! environment: Environment!
deployerLrn: String
isCurrent: Boolean! isCurrent: Boolean!
baseDomain: String
status: DeploymentStatus! status: DeploymentStatus!
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
@@ -130,6 +141,14 @@ input AddEnvironmentVariableInput {
value: String! value: String!
} }
input AddProjectFromTemplateInput {
templateOwner: String!
templateRepo: String!
owner: String!
name: String!
isPrivate: Boolean!
}
input AddProjectInput { input AddProjectInput {
name: String! name: String!
repository: String! repository: String!
@@ -174,6 +193,48 @@ input FilterDomainsInput {
status: DomainStatus 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!]!
}
input AuctionData {
maxPrice: String,
numProviders: Int,
}
type Query { type Query {
user: User! user: User!
organizations: [Organization!] organizations: [Organization!]
@@ -184,6 +245,7 @@ type Query {
environmentVariables(projectId: String!): [EnvironmentVariable!] environmentVariables(projectId: String!): [EnvironmentVariable!]
projectMembers(projectId: String!): [ProjectMember!] projectMembers(projectId: String!): [ProjectMember!]
searchProjects(searchText: String!): [Project!] searchProjects(searchText: String!): [Project!]
getAuctionData(auctionId: String!): Auction!
domains(projectId: String!, filter: FilterDomainsInput): [Domain] domains(projectId: String!, filter: FilterDomainsInput): [Domain]
} }
@@ -204,7 +266,18 @@ type Mutation {
): Boolean! ): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(organizationSlug: String!, data: AddProjectInput): Project! addProjectFromTemplate(
organizationSlug: String!
data: AddProjectFromTemplateInput
lrn: String
auctionData: AuctionData
): Project!
addProject(
organizationSlug: String!
data: AddProjectInput!
lrn: String
auctionData: AuctionData
): Project!
updateProject(projectId: String!, data: UpdateProjectInput): Boolean! updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
redeployToProd(deploymentId: String!): Boolean! redeployToProd(deploymentId: String!): Boolean!
deleteProject(projectId: String!): Boolean! deleteProject(projectId: String!): Boolean!
+375 -124
View File
@@ -1,12 +1,12 @@
import assert from 'assert'; import assert from 'assert';
import debug from 'debug'; import debug from 'debug';
import { DeepPartial, FindOptionsWhere } from 'typeorm'; import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm';
import { Octokit, RequestError } from 'octokit'; import { Octokit, RequestError } from 'octokit';
import { OAuthApp } from '@octokit/oauth-app'; import { OAuthApp } from '@octokit/oauth-app';
import { Database } from './database'; import { Database } from './database';
import { Deployment, DeploymentStatus, Environment } from './entity/Deployment'; import { ApplicationRecord, Deployment, DeploymentStatus, Environment } from './entity/Deployment';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Organization } from './entity/Organization'; import { Organization } from './entity/Organization';
@@ -16,12 +16,14 @@ import { User } from './entity/User';
import { Registry } from './registry'; import { Registry } from './registry';
import { GitHubConfig, RegistryConfig } from './config'; import { GitHubConfig, RegistryConfig } from './config';
import { import {
AddProjectFromTemplateInput,
AppDeploymentRecord, AppDeploymentRecord,
AppDeploymentRemovalRecord, AppDeploymentRemovalRecord,
AuctionData,
GitPushEventPayload, GitPushEventPayload,
PackageJSON,
} from './types'; } from './types';
import { Role } from './entity/UserOrganization'; import { Role } from './entity/UserOrganization';
import { getRepoDetails } from './utils';
const log = debug('snowball:service'); const log = debug('snowball:service');
@@ -38,7 +40,7 @@ interface Config {
export class Service { export class Service {
private db: Database; private db: Database;
private oauthApp: OAuthApp; private oauthApp: OAuthApp;
private registry: Registry; private laconicRegistry: Registry;
private config: Config; private config: Config;
private deployRecordCheckTimeout?: NodeJS.Timeout; private deployRecordCheckTimeout?: NodeJS.Timeout;
@@ -46,7 +48,7 @@ export class Service {
constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) { constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) {
this.db = db; this.db = db;
this.oauthApp = app; this.oauthApp = app;
this.registry = registry; this.laconicRegistry = registry;
this.config = config; this.config = config;
this.init(); this.init();
} }
@@ -59,6 +61,8 @@ export class Service {
this.checkDeployRecordsAndUpdate(); this.checkDeployRecordsAndUpdate();
// Start check for ApplicationDeploymentRemovalRecords asynchronously // Start check for ApplicationDeploymentRemovalRecords asynchronously
this.checkDeploymentRemovalRecordsAndUpdate(); this.checkDeploymentRemovalRecordsAndUpdate();
// Start check for Deployment Auctions asynchronously
this.checkAuctionStatus();
} }
/** /**
@@ -107,7 +111,7 @@ export class Service {
} }
// Fetch ApplicationDeploymentRecord for deployments // Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments); const records = await this.laconicRegistry.getDeploymentRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRecords`); log(`Found ${records.length} ApplicationDeploymentRecords`);
// Update deployments for which ApplicationDeploymentRecords were returned // Update deployments for which ApplicationDeploymentRecords were returned
@@ -140,7 +144,7 @@ export class Service {
// Fetch ApplicationDeploymentRemovalRecords for deployments // Fetch ApplicationDeploymentRemovalRecords for deployments
const records = const records =
await this.registry.getDeploymentRemovalRecords(deployments); await this.laconicRegistry.getDeploymentRemovalRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`); log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
// Update deployments for which ApplicationDeploymentRemovalRecords were returned // Update deployments for which ApplicationDeploymentRemovalRecords were returned
@@ -161,36 +165,40 @@ export class Service {
records: AppDeploymentRecord[], records: AppDeploymentRecord[],
): Promise<void> { ): Promise<void> {
// Get deployments for ApplicationDeploymentRecords // Get deployments for ApplicationDeploymentRecords
// Deployments that are completed but not updated(are in building state and ApplicationDeploymentRecord is present)
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: records.map((record) => ({ where: records.map((record) => ({
applicationRecordId: record.attributes.application, applicationRecordId: record.attributes.application,
// Only for the specific deployer
deployerLrn: record.attributes.deployer
})), })),
order: { order: {
createdAt: 'DESC', createdAt: 'DESC',
}, },
}); });
// Get project IDs of deployments that are in production environment // Get deployment IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce( const productionDeploymentIds: string[] = [];
(acc, deployment): Set<string> => { deployments.forEach(deployment => {
if (deployment.environment === Environment.Production) { if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId); if (!productionDeploymentIds.includes(deployment.id)) {
productionDeploymentIds.push(deployment.id);
} }
}
return acc; });
},
new Set<string>(),
);
// Set old deployments isCurrent to false // Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds( // TODO: Only set isCurrent to false for the deployment for that specific deployer
Array.from(productionDeploymentProjectIds), for (const deploymentId of productionDeploymentIds) {
{ isCurrent: false }, await this.db.updateDeployment(
); { id: deploymentId },
{ isCurrent: false }
);
}
const recordToDeploymentsMap = deployments.reduce( const recordToDeploymentsMap = deployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => { (acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationRecordId] = deployment; acc[deployment.applicationDeploymentRequestId!] = deployment;
return acc; return acc;
}, },
{}, {},
@@ -198,16 +206,32 @@ export class Service {
// Update deployment data for ApplicationDeploymentRecords // Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => { const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.application]; const deployment = recordToDeploymentsMap[record.attributes.request];
const project = await this.getProjectById(deployment.projectId)
assert(project)
const parts = record.attributes.url.replace('https://', '').split('.');
const baseDomain = parts.slice(1).join('.');
await this.db.updateDeploymentById(deployment.id, { await this.db.updateDeploymentById(deployment.id, {
applicationDeploymentRecordId: record.id, applicationDeploymentRecordId: record.id,
applicationDeploymentRecordData: record.attributes, applicationDeploymentRecordData: record.attributes,
url: record.attributes.url, url: record.attributes.url,
baseDomain,
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production, isCurrent: deployment.environment === Environment.Production,
}); });
const baseDomains = project.baseDomains || [];
if (!baseDomains.includes(baseDomain)) {
baseDomains.push(baseDomain);
}
await this.db.updateProjectById(project.id, {
baseDomains
})
log( log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`, `Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
); );
@@ -256,11 +280,69 @@ export class Service {
); );
await this.db.deleteDeploymentById(deployment.id); await this.db.deleteDeploymentById(deployment.id);
const project = await this.db.getProjectById(deployment.projectId);
const updatedBaseDomains = project!.baseDomains!.filter(baseDomain => baseDomain !== deployment.baseDomain);
await this.db.updateProjectById(deployment.projectId, {
baseDomains: updatedBaseDomains
});
}); });
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
} }
/**
* Checks the status for all ongoing auctions
* Calls the createDeploymentFromAuction method for deployments with completed auctions
*/
async checkAuctionStatus(): Promise<void> {
const allProjects = await this.db.getProjects({
where: {
auctionId: Not(IsNull()),
},
relations: ['deployments'],
withDeleted: true,
});
// Should only check on the first deployment
const projects = allProjects.filter(project => {
if (project.deletedAt !== null) return false;
const deletedDeployments = project.deployments.filter(deployment => deployment.deletedAt !== null).length;
return project.deployments.length === 0 && deletedDeployments === 0;
});
const auctionIds = projects.map((project) => project.auctionId);
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(auctionIds);
if (completedAuctionIds) {
const projectsToBedeployed = projects.filter((project) =>
completedAuctionIds.includes(project.auctionId!)
);
for (const project of projectsToBedeployed) {
log(`Auction ${project!.auctionId} completed`);
const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!);
// Update project with deployer LRNs
await this.db.updateProjectById(project.id!, {
deployerLrns
});
for (const deployer of deployerLrns) {
log(`Creating deployment for deployer LRN ${deployer}`);
await this.createDeploymentFromAuction(project, deployer);
}
}
}
this.deployRecordCheckTimeout = setTimeout(() => {
this.checkAuctionStatus();
}, this.config.registryConfig.checkAuctionStatusDelay);
}
async getUser(userId: string): Promise<User | null> { async getUser(userId: string): Promise<User | null> {
return this.db.getUser({ return this.db.getUser({
where: { where: {
@@ -518,7 +600,7 @@ export class Service {
domain: prodBranchDomains[0], domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash, commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage, commitMessage: oldDeployment.commitMessage,
}); }, oldDeployment.deployerLrn);
return newDeployment; return newDeployment;
} }
@@ -527,44 +609,20 @@ export class Service {
userId: string, userId: string,
octokit: Octokit, octokit: Octokit,
data: DeepPartial<Deployment>, data: DeepPartial<Deployment>,
deployerLrn: string
): Promise<Deployment> { ): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found'); assert(data.project?.repository, 'Project repository not found');
log( log(
`Creating deployment in project ${data.project.name} from branch ${data.branch}`, `Creating deployment in project ${data.project.name} from branch ${data.branch}`,
); );
const [owner, repo] = data.project.repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({
owner,
repo,
path: 'package.json',
ref: data.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;
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record) // TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } = const { applicationRecordId, applicationRecordData } =
await this.registry.createApplicationRecord({ await this.laconicRegistry.createApplicationRecord({
appName: repo, octokit,
packageJSON, repository: data.project.repository,
appType: data.project!.template!, appType: data.project!.template!,
commitHash: data.commitHash!, commitHash: data.commitHash!,
repoUrl,
}); });
// Update previous deployment with prod branch domain // Update previous deployment with prod branch domain
@@ -580,6 +638,130 @@ export class Service {
); );
} }
const newDeployment = await this.createDeploymentFromData(userId, data, deployerLrn, applicationRecordId, applicationRecordData);
const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash);
const environmentVariablesObj = await this.getEnvVariables(data.project!.id!);
// To set project DNS
if (data.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`,
lrn: deployerLrn
});
}
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
lrn: deployerLrn,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`,
});
await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
// Save deployer lrn only if present
if (deployerLrn) {
newDeployment.project.deployerLrns = [deployerLrn];
}
return newDeployment;
}
async createDeploymentFromAuction(
project: DeepPartial<Project>,
deployerLrn: string
): Promise<Deployment> {
const octokit = await this.getOctokit(project.ownerId!);
const [owner, repo] = project.repository!.split('/');
const repoUrl = (
await octokit.rest.repos.get({
owner,
repo,
})
).data.html_url;
const {
data: [latestCommit],
} = await octokit.rest.repos.listCommits({
owner,
repo,
sha: project.prodBranch,
per_page: 1,
});
const lrn = this.laconicRegistry.getLrn(repo);
const [record] = await this.laconicRegistry.getRecordsByName(lrn);
const applicationRecordId = record.id;
const applicationRecordData = record.attributes;
// Create deployment with prod branch and latest commit
const deploymentData = {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message,
};
const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData);
const environmentVariablesObj = await this.getEnvVariables(project!.id!);
// To set project DNS
if (deploymentData.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`,
auctionId: project.auctionId!,
lrn: deployerLrn,
});
}
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
// Create requests for all the deployers
await this.laconicRegistry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
auctionId: project.auctionId!,
lrn: deployerLrn,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`,
});
await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return newDeployment;
}
async createDeploymentFromData(
userId: string,
data: DeepPartial<Deployment>,
deployerLrn: string,
applicationRecordId: string,
applicationRecordData: ApplicationRecord,
): Promise<Deployment> {
const newDeployment = await this.db.addDeployment({ const newDeployment = await this.db.addDeployment({
project: data.project, project: data.project,
branch: data.branch, branch: data.branch,
@@ -593,60 +775,69 @@ export class Service {
createdBy: Object.assign(new User(), { createdBy: Object.assign(new User(), {
id: userId, id: userId,
}), }),
deployerLrn,
}); });
log( log(`Created deployment ${newDeployment.id}`);
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`,
);
const environmentVariables =
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
environment: Environment.Production,
});
const environmentVariablesObj = environmentVariables.reduce(
(acc, env) => {
acc[env.key] = env.value;
return acc;
},
{} as { [key: string]: string },
);
// To set project DNS
if (data.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`,
});
}
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`,
});
await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return newDeployment; return newDeployment;
} }
async addProjectFromTemplate(
user: User,
organizationSlug: string,
data: AddProjectFromTemplateInput,
lrn?: string,
auctionData?: AuctionData
): Promise<Project | undefined> {
try {
const octokit = await this.getOctokit(user.id);
const gitRepo = await octokit?.rest.repos.createUsingTemplate({
template_owner: data.templateOwner,
template_repo: data.templateRepo,
owner: data.owner,
name: data.name,
include_all_branches: false,
private: data.isPrivate,
});
if (!gitRepo) {
throw new Error('Failed to create repository from template');
}
const createdTemplateRepo = await octokit.rest.repos.get({
owner: data.owner,
repo: data.name,
});
const prodBranch = createdTemplateRepo.data.default_branch ?? 'main';
const project = await this.addProject(user, organizationSlug, {
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`,
prodBranch,
repository: gitRepo.data.full_name,
// TODO: Set selected template
template: 'webapp',
}, lrn, auctionData);
if (!project || !project.id) {
throw new Error('Failed to create project from template');
}
return project;
} catch (error) {
console.error('Error creating project from template:', error);
throw error;
}
}
async addProject( async addProject(
user: User, user: User,
organizationSlug: string, organizationSlug: string,
data: DeepPartial<Project>, data: DeepPartial<Project>,
lrn?: string,
auctionData?: AuctionData
): Promise<Project | undefined> { ): Promise<Project | undefined> {
const organization = await this.db.getOrganization({ const organization = await this.db.getOrganization({
where: { where: {
@@ -658,6 +849,7 @@ export class Service {
} }
const project = await this.db.addProject(user, organization.id, data); const project = await this.db.addProject(user, organization.id, data);
log(`Project created ${project.id}`);
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
const [owner, repo] = project.repository.split('/'); const [owner, repo] = project.repository.split('/');
@@ -672,14 +864,21 @@ export class Service {
}); });
// Create deployment with prod branch and latest commit // Create deployment with prod branch and latest commit
await this.createDeployment(user.id, octokit, { const deploymentData = {
project, project,
branch: project.prodBranch, branch: project.prodBranch,
environment: Environment.Production, environment: Environment.Production,
domain: null, domain: null,
commitHash: latestCommit.sha, commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message, commitMessage: latestCommit.commit.message,
}); };
if (auctionData) {
const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionData!, deploymentData);
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId })
} else {
await this.createDeployment(user.id, octokit, deploymentData, lrn!);
}
await this.createRepoHook(octokit, project); await this.createRepoHook(octokit, project);
@@ -748,19 +947,30 @@ export class Service {
branch, branch,
}); });
// Create deployment with branch and latest commit in GitHub data const deployers = project.deployerLrns;
await this.createDeployment(project.ownerId, octokit, { if (!deployers) {
project, return;
branch, }
environment:
project.prodBranch === branch for (const deployer of deployers) {
? Environment.Production // Create deployment with branch and latest commit in GitHub data
: Environment.Preview, await this.createDeployment(project.ownerId, octokit,
domain, {
commitHash: headCommit.id, project,
commitMessage: headCommit.message, branch,
}); environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message,
},
deployer
);
}
} }
} }
async updateProject( async updateProject(
@@ -809,15 +1019,24 @@ export class Service {
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(user.id, octokit, { let newDeployment: Deployment;
project: oldDeployment.project,
// TODO: Put isCurrent field in project if (oldDeployment.project.auctionId) {
branch: oldDeployment.branch, newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployerLrn);
environment: Environment.Production, } else {
domain: oldDeployment.domain, newDeployment = await this.createDeployment(user.id, octokit,
commitHash: oldDeployment.commitHash, {
commitMessage: oldDeployment.commitMessage, project: oldDeployment.project,
}); // TODO: Put isCurrent field in project
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage,
},
oldDeployment.deployerLrn
);
}
return newDeployment; return newDeployment;
} }
@@ -869,10 +1088,11 @@ export class Service {
if (deployment && deployment.applicationDeploymentRecordId) { if (deployment && deployment.applicationDeploymentRecordId) {
// If deployment is current, remove deployment for project subdomain as well // If deployment is current, remove deployment for project subdomain as well
if (deployment.isCurrent) { if (deployment.isCurrent) {
const currentDeploymentURL = `https://${deployment.project.subDomain}`; const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.baseDomain}`;
// TODO: Store the latest DNS deployment record
const deploymentRecords = const deploymentRecords =
await this.registry.getDeploymentRecordsByFilter({ await this.laconicRegistry.getDeploymentRecordsByFilter({
application: deployment.applicationRecordId, application: deployment.applicationRecordId,
url: currentDeploymentURL, url: currentDeploymentURL,
}); });
@@ -885,14 +1105,20 @@ export class Service {
return false; return false;
} }
await this.registry.createApplicationDeploymentRemovalRequest({ // Multiple records are fetched, take the latest record
deploymentId: deploymentRecords[0].id, const latestRecord = deploymentRecords
.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())[0];
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
deploymentId: latestRecord.id,
deployerLrn: deployment.deployerLrn
}); });
} }
const result = const result =
await this.registry.createApplicationDeploymentRemovalRequest({ await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
deploymentId: deployment.applicationDeploymentRecordId, deploymentId: deployment.applicationDeploymentRecordId,
deployerLrn: deployment.deployerLrn
}); });
await this.db.updateDeploymentById(deployment.id, { await this.db.updateDeploymentById(deployment.id, {
@@ -1027,4 +1253,29 @@ export class Service {
): Promise<boolean> { ): Promise<boolean> {
return this.db.updateUser(user, data); return this.db.updateUser(user, data);
} }
async getEnvVariables(
projectId: string,
): Promise<{ [key: string]: string }> {
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId, {
environment: Environment.Production,
});
const environmentVariablesObj = environmentVariables.reduce(
(acc, env) => {
acc[env.key] = env.value;
return acc;
},
{} as { [key: string]: string },
);
return environmentVariablesObj;
}
async getAuctionData(
auctionId: string
): Promise<any> {
const auctions = await this.laconicRegistry.getAuctionData(auctionId);
return auctions[0];
}
} }
+36 -1
View File
@@ -29,6 +29,8 @@ export interface GitPushEventPayload {
export interface AppDeploymentRecordAttributes { export interface AppDeploymentRecordAttributes {
application: string; application: string;
auction: string;
deployer: string;
dns: string; dns: string;
meta: string; meta: string;
name: string; name: string;
@@ -41,7 +43,7 @@ export interface AppDeploymentRecordAttributes {
export interface AppDeploymentRemovalRecordAttributes { export interface AppDeploymentRemovalRecordAttributes {
deployment: string; deployment: string;
request: string; request: string;
type: "ApplicationDeploymentRemovalRecord"; type: 'ApplicationDeploymentRemovalRecord';
version: string; version: string;
} }
@@ -61,3 +63,36 @@ export interface AppDeploymentRecord extends RegistryRecord {
export interface AppDeploymentRemovalRecord extends RegistryRecord { export interface AppDeploymentRemovalRecord extends RegistryRecord {
attributes: AppDeploymentRemovalRecordAttributes; attributes: AppDeploymentRemovalRecordAttributes;
} }
export interface AddProjectFromTemplateInput {
templateOwner: string;
templateRepo: string;
owner: string;
name: string;
isPrivate: boolean;
}
export interface Auction {
id: string;
kind: string;
status: string;
ownerAddress: string;
createTime: Date;
commitsEndTime: Date;
revealsEndTime: Date;
commitFee: string;
revealFee: string;
minimumBid?: string;
winnerAddresses: string[];
winnerBids?: string[];
winnerPrice?: string;
maxPrice?: string;
numProviders: number;
fundsReleased: boolean;
bids: string[];
}
export interface AuctionData {
maxPrice: string,
numProviders: number,
}
+45 -1
View File
@@ -1,10 +1,14 @@
import assert from 'assert';
import debug from 'debug';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { Octokit } from 'octokit';
import path from 'path'; import path from 'path';
import toml from 'toml'; import toml from 'toml';
import debug from 'debug';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
import { Config } from './config'; import { Config } from './config';
import { DEFAULT_CONFIG_FILE_PATH } from './constants'; import { DEFAULT_CONFIG_FILE_PATH } from './constants';
import { PackageJSON } from './types';
const log = debug('snowball:utils'); const log = debug('snowball:utils');
@@ -76,3 +80,43 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
export const sleep = async (ms: number): Promise<void> => export const sleep = async (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms)); 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
};
}
+1 -3
View File
@@ -2,8 +2,6 @@ import * as fs from 'fs/promises';
import debug from 'debug'; import debug from 'debug';
import { getConfig } from '../src/utils'; import { getConfig } from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
const log = debug('snowball:delete-database'); const log = debug('snowball:delete-database');
@@ -13,7 +11,7 @@ const deleteFile = async (filePath: string) => {
}; };
const main = async () => { const main = async () => {
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const config = await getConfig();
deleteFile(config.database.dbPath); deleteFile(config.database.dbPath);
}; };
+1 -3
View File
@@ -16,8 +16,6 @@ import {
getEntities, getEntities,
loadAndSaveData loadAndSaveData
} from '../src/utils'; } from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
const log = debug('snowball:initialize-database'); const log = debug('snowball:initialize-database');
@@ -156,7 +154,7 @@ const generateTestData = async (dataSource: DataSource) => {
}; };
const main = async () => { const main = async () => {
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const config = await getConfig();
const isDbPresent = await checkFileExists(config.database.dbPath); const isDbPresent = await checkFileExists(config.database.dbPath);
if (!isDbPresent) { if (!isDbPresent) {
+10 -9
View File
@@ -1,39 +1,40 @@
import debug from 'debug'; 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'; import { getConfig } from '../src/utils';
const log = debug('snowball:initialize-registry'); const log = debug('snowball:initialize-registry');
const DENOM = 'aphoton'; const DENOM = 'alnt';
const BOND_AMOUNT = '1000000000'; const BOND_AMOUNT = '1000000000';
async function main () { async function main () {
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig } = await getConfig();
// TODO: Get authority names from args // TODO: Get authority names from args
const authorityNames = ['snowballtools', registryConfig.authority]; 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); const bondId = await registry.getNextBondId(registryConfig.privateKey);
log('bondId:', bondId); log('bondId:', bondId);
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
await registry.createBond( await registry.createBond(
{ denom: DENOM, amount: BOND_AMOUNT }, { denom: DENOM, amount: BOND_AMOUNT },
registryConfig.privateKey, registryConfig.privateKey,
registryConfig.fee fee
); );
for await (const name of authorityNames) { 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); log('Reserved authority name:', name);
await registry.setAuthorityBond( await registry.setAuthorityBond(
{ name, bondId }, { name, bondId },
registryConfig.privateKey, registryConfig.privateKey,
registryConfig.fee fee
); );
log(`Bond ${bondId} set for authority ${name}`); log(`Bond ${bondId} set for authority ${name}`);
} }
+10 -10
View File
@@ -2,22 +2,20 @@ import debug from 'debug';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import path from 'path'; 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 { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment'; import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records'); const log = debug('snowball:publish-deploy-records');
async function main () { async function main() {
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig, database, misc } = await getConfig();
const registry = new Registry( const registry = new Registry(
registryConfig.gqlEndpoint, registryConfig.gqlEndpoint,
registryConfig.restEndpoint, registryConfig.restEndpoint,
registryConfig.chainId { chainId: registryConfig.chainId }
); );
const dataSource = new DataSource({ const dataSource = new DataSource({
@@ -61,6 +59,8 @@ async function main () {
url url
}; };
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
const result = await registry.setRecord( const result = await registry.setRecord(
{ {
privateKey: registryConfig.privateKey, privateKey: registryConfig.privateKey,
@@ -68,12 +68,12 @@ async function main () {
bondId: registryConfig.bondId bondId: registryConfig.bondId
}, },
'', '',
registryConfig.fee fee
); );
// Remove deployment for project subdomain if deployment is for production environment // Remove deployment for project subdomain if deployment is for production environment
if (deployment.environment === Environment.Production) { if (deployment.environment === Environment.Production) {
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}` applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.baseDomain}`;
await registry.setRecord( await registry.setRecord(
{ {
@@ -82,12 +82,12 @@ async function main () {
bondId: registryConfig.bondId bondId: registryConfig.bondId
}, },
'', '',
registryConfig.fee fee
); );
} }
log('Application deployment record data:', applicationDeploymentRecord); 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 { DataSource } from 'typeorm';
import path from 'path'; 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 { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment'; import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deployment-removal-records'); const log = debug('snowball:publish-deployment-removal-records');
async function main () { async function main () {
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig, database } = await getConfig();
const registry = new Registry( const registry = new Registry(
registryConfig.gqlEndpoint, registryConfig.gqlEndpoint,
registryConfig.restEndpoint, registryConfig.restEndpoint,
registryConfig.chainId { chainId: registryConfig.chainId }
); );
const dataSource = new DataSource({ const dataSource = new DataSource({
@@ -47,6 +45,8 @@ async function main () {
request: deployment.applicationDeploymentRemovalRequestId, request: deployment.applicationDeploymentRemovalRequestId,
} }
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
const result = await registry.setRecord( const result = await registry.setRecord(
{ {
privateKey: registryConfig.privateKey, privateKey: registryConfig.privateKey,
@@ -54,11 +54,11 @@ async function main () {
bondId: registryConfig.bondId bondId: registryConfig.bondId
}, },
'', '',
registryConfig.fee fee
); );
log('Application deployment removal record data:', applicationDeploymentRemovalRecord); log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
log(`Application deployment removal record published: ${result.data.id}`); log(`Application deployment removal record published: ${result.id}`);
} }
} }
+13 -6
View File
@@ -1,9 +1,11 @@
# deployer # deployer
- Install dependencies - Install dependencies
```bash ```bash
yarn yarn
``` ```
```bash ```bash
brew install jq # if you do not have jq installed already brew install jq # if you do not have jq installed already
``` ```
@@ -12,13 +14,13 @@
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com` - To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
``` ```bash
./deploy-frontend.staging.sh ./deploy-frontend.staging.sh
``` ```
- To deploy frontend app to `dashboard.apps.snowballtools.com` - To deploy frontend app to `dashboard.apps.snowballtools.com`
``` ```bash
./deploy-frontend.sh ./deploy-frontend.sh
``` ```
@@ -27,6 +29,7 @@
## Notes ## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml) - Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml ```yml
record: record:
... ...
@@ -35,6 +38,7 @@
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
... ...
``` ```
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too - On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
## Troubleshoot ## Troubleshoot
@@ -44,14 +48,17 @@
- If deployment fails due to low bond balance - If deployment fails due to low bond balance
- Check balances - Check balances
```bash ```bash
# Account balance # Account balance
yarn laconic cns account get yarn laconic registry account get
# Bond balance # Bond balance
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
``` ```
- Command to refill bond - Command to refill bond
```bash ```bash
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000 yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
``` ```
+4 -3
View File
@@ -1,9 +1,10 @@
services: services:
cns: registry:
restEndpoint: 'http://console.laconic.com:1317' restEndpoint: 'http://console.laconic.com:1317'
gqlEndpoint: 'http://console.laconic.com:9473/api' gqlEndpoint: 'http://console.laconic.com:9473/api'
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
chainId: laconic_9000-1 chainId: laconic_9000-1
gas: 1200000 gas:
fees: 200000aphoton fees:
gasPrice: 1
+4 -3
View File
@@ -1,9 +1,10 @@
services: services:
cns: registry:
restEndpoint: http://console.laconic.com:1317 restEndpoint: http://console.laconic.com:1317
gqlEndpoint: http://console.laconic.com:9473/api gqlEndpoint: http://console.laconic.com:9473/api
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311 userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
chainId: laconic_9000-1 chainId: laconic_9000-1
gas: 1200000 gas:
fees: 200000aphoton fees:
gasPrice: 1
+18 -18
View File
@@ -18,7 +18,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts # 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 # 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 "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 if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found # Set application-record version if no previous records were found
@@ -26,20 +26,20 @@ if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; t
fi fi
# Generate application-deployment-request.yml # Generate application-deployment-request.yml
cat > ./records/application-deployment-request.yml <<EOF cat >./records/application-deployment-request.yml <<EOF
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: '1.0.0' version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION name: snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION application: lrn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard dns: dashboard
config: config:
env: env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools-platform/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-platform/image-upload-pwa-example LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2 LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta: meta:
note: Added by Snowball @ $CURRENT_DATE_TIME note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -48,7 +48,7 @@ record:
EOF EOF
# Generate application-record.yml with incremented version # Generate application-record.yml with incremented version
cat > ./records/application-record.yml <<EOF cat >./records/application-record.yml <<EOF
record: record:
type: ApplicationRecord type: ApplicationRecord
version: $NEW_APPLICATION_VERSION version: $NEW_APPLICATION_VERSION
@@ -64,7 +64,7 @@ echo "Files generated successfully."
RECORD_FILE=records/application-record.yml RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord # 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=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record" echo "FATAL: Failed to publish record"
@@ -75,17 +75,17 @@ echo "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
# Set name to record # Set name to record
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend" REGISTRY_APP_LRN="lrn://snowballtools/applications/snowballtools-base-frontend"
sleep 2 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=$? rc=$?
if [ $rc -ne 0 ]; then 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 exit $rc
fi fi
sleep 2 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=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash" echo "FATAL: Failed to set hash"
@@ -93,16 +93,16 @@ if [ $rc -ne 0 ]; then
fi fi
sleep 2 sleep 2
# Set name if latest release # 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=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release" echo "FATAL: Failed to set release"
exit $rc exit $rc
fi fi
echo "$REGISTRY_APP_CRN set for ApplicationRecord" echo "$REGISTRY_APP_LRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN # Check if record found for REGISTRY_APP_LRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN") query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name" echo "FATAL: Failed to query name"
@@ -110,14 +110,14 @@ if [ $rc -ne 0 ]; then
fi fi
APP_RECORD=$(echo $query_response | jq '.[0]') APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then 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 exit 1
fi fi
RECORD_FILE=records/application-deployment-request.yml RECORD_FILE=records/application-deployment-request.yml
sleep 2 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 if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request" echo "FATAL: Failed to query deployment request"
exit $rc exit $rc
+18 -18
View File
@@ -18,7 +18,7 @@ REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb466
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts # 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 # 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}') NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry 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 if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found # Set application-record version if no previous records were found
@@ -26,20 +26,20 @@ if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; t
fi fi
# Generate application-deployment-request.yml # Generate application-deployment-request.yml
cat > ./staging-records/application-deployment-request.yml <<EOF cat >./staging-records/application-deployment-request.yml <<EOF
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: '1.0.0' version: '1.0.0'
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION application: lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard.staging.apps.snowballtools.com dns: dashboard.staging.apps.snowballtools.com
config: config:
env: env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5 LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2 LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball 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_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
@@ -54,7 +54,7 @@ record:
EOF EOF
# Generate application-record.yml with incremented version # Generate application-record.yml with incremented version
cat > ./staging-records/application-record.yml <<EOF cat >./staging-records/application-record.yml <<EOF
record: record:
type: ApplicationRecord type: ApplicationRecord
version: $NEW_APPLICATION_VERSION version: $NEW_APPLICATION_VERSION
@@ -70,7 +70,7 @@ echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord # 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=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record" echo "FATAL: Failed to publish record"
@@ -81,17 +81,17 @@ echo "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
# Set name to record # Set name to record
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend" REGISTRY_APP_LRN="lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
sleep 2 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=$? rc=$?
if [ $rc -ne 0 ]; then 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 exit $rc
fi fi
sleep 2 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=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash" echo "FATAL: Failed to set hash"
@@ -99,16 +99,16 @@ if [ $rc -ne 0 ]; then
fi fi
sleep 2 sleep 2
# Set name if latest release # 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=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release" echo "FATAL: Failed to set release"
exit $rc exit $rc
fi fi
echo "$REGISTRY_APP_CRN set for ApplicationRecord" echo "$REGISTRY_APP_LRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN # Check if record found for REGISTRY_APP_LRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN") query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name" echo "FATAL: Failed to query name"
@@ -116,14 +116,14 @@ if [ $rc -ne 0 ]; then
fi fi
APP_RECORD=$(echo $query_response | jq '.[0]') APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then 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 exit 1
fi fi
RECORD_FILE=staging-records/application-deployment-request.yml RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2 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 if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request" echo "FATAL: Failed to query deployment request"
exit $rc exit $rc
@@ -1,6 +1,6 @@
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: '1.0.0' version: "1.0.0"
name: snowballtools-base-frontend@0.1.8 name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8 application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard dns: dashboard
@@ -8,9 +8,9 @@ record:
env: env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com 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_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/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-platform/image-upload-pwa-example LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2 LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta: meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024 note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
@@ -7,18 +7,18 @@ record:
config: config:
env: env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5 LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2 LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball 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_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073 LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost 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_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591 LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta: meta:
note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC note: Added by Snowball @ Mon Jun 24 23:51:48 UTC 2024
repository: "https://github.com/snowball-tools/snowballtools-base" repository: "https://github.com/snowball-tools/snowballtools-base"
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99 repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
@@ -1,7 +1,7 @@
record: record:
type: ApplicationRecord type: ApplicationRecord
version: 0.0.2 version: 0.0.1
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99 repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
repository: ["https://github.com/snowball-tools/snowballtools-base"] repository: ["https://github.com/snowball-tools/snowballtools-base"]
app_type: webapp app_type: webapp
name: staging-snowballtools-base-frontend name: staging-snowballtools-base-frontend
+1 -1
View File
@@ -2,7 +2,7 @@
Check if the live web app deployer is in a working state Check if the live web app deployer is in a working state
- Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch) - Web app repo used: <https://github.com/snowball-tools/test-progressive-web-app> (main branch)
- Config used: [../config.yml](../config.yml) - Config used: [../config.yml](../config.yml)
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following: - The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo - Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
@@ -1,6 +1,6 @@
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: '1.0.0' version: "1.0.0"
name: deployment-test-app@0.1.24 name: deployment-test-app@0.1.24
application: crn://snowballtools/applications/deployment-test-app@0.1.24 application: crn://snowballtools/applications/deployment-test-app@0.1.24
dns: deployment-ci-test dns: deployment-ci-test
@@ -11,5 +11,5 @@ record:
CERC_WEBAPP_DEBUG: 0 CERC_WEBAPP_DEBUG: 0
meta: meta:
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
repository: "https://github.com/snowball-tools-platform/test-progressive-web-app" repository: "https://github.com/snowball-tools/test-progressive-web-app"
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26 repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
@@ -2,7 +2,7 @@ record:
type: ApplicationRecord type: ApplicationRecord
version: 0.0.1 version: 0.0.1
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26 repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"] repository: ["https://github.com/snowball-tools/test-progressive-web-app"]
app_type: webapp app_type: webapp
name: deployment-test-app name: deployment-test-app
app_version: 0.1.24 app_version: 0.1.24
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Repository URL # Repository URL
REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app" REPO_URL="https://github.com/snowball-tools/test-progressive-web-app"
# Get the latest commit hash from the repository # Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}') LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
@@ -22,7 +22,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
APP_NAME=deployment-test-app APP_NAME=deployment-test-app
# Get latest version from registry and increment application-record version # 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 if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found # Set application-record version if no previous records were found
@@ -32,7 +32,7 @@ fi
# Generate application-record.yml with incremented version # Generate application-record.yml with incremented version
RECORD_FILE=packages/deployer/test/records/application-record.yml RECORD_FILE=packages/deployer/test/records/application-record.yml
cat > $RECORD_FILE <<EOF cat >$RECORD_FILE <<EOF
record: record:
type: ApplicationRecord type: ApplicationRecord
version: $NEW_APPLICATION_VERSION version: $NEW_APPLICATION_VERSION
@@ -46,12 +46,12 @@ EOF
# Generate application-deployment-request.yml # Generate application-deployment-request.yml
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
cat > $REQUEST_RECORD_FILE <<EOF cat >$REQUEST_RECORD_FILE <<EOF
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: '1.0.0' version: '1.0.0'
name: $APP_NAME@$PACKAGE_VERSION 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 dns: deployment-ci-test
config: config:
env: env:
@@ -67,31 +67,31 @@ EOF
echo "Record files generated successfully." echo "Record files generated successfully."
# Publish ApplicationRecord # 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 "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
# Set name to record # Set name to record
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME" REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
sleep 2 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 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 sleep 2
# Set name if latest release # 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"
echo "$REGISTRY_APP_CRN set for ApplicationRecord" echo "$REGISTRY_APP_LRN set for ApplicationRecord"
# Check if record exists for REGISTRY_APP_CRN # Check if record exists for REGISTRY_APP_LRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]') APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then 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 exit 1
fi fi
sleep 2 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 "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID echo $DEPLOYMENT_REQUEST_ID
@@ -102,7 +102,7 @@ MAX_RETRIES=20
# Check that a ApplicationDeploymentRecord is published # Check that a ApplicationDeploymentRecord is published
retry_count=0 retry_count=0
while true; do 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') len_deployment_records=$(echo $deployment_records_response | jq 'length')
# Check if number of records returned is 0 # Check if number of records returned is 0
@@ -115,7 +115,7 @@ while true; do
else else
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..." echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL sleep $RETRY_INTERVAL
retry_count=$((retry_count+1)) retry_count=$((retry_count + 1))
fi fi
else else
echo "ApplicationDeploymentRecord found" echo "ApplicationDeploymentRecord found"
@@ -140,7 +140,7 @@ fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
retry_count=0 retry_count=0
max_retries=10 max_retries=10
retry_interval=5 retry_interval=10
while true; do while true; do
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url) url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
if [ "$url_response" = "200" ]; then if [ "$url_response" = "200" ]; then
@@ -154,7 +154,7 @@ while true; do
else else
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..." echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval sleep $retry_interval
retry_count=$((retry_count+1)) retry_count=$((retry_count + 1))
fi fi
fi fi
done done
@@ -162,7 +162,7 @@ done
# Generate application-deployment-removal-request.yml # Generate application-deployment-removal-request.yml
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF cat >$REMOVAL_REQUEST_RECORD_FILE <<EOF
record: record:
deployment: $DEPLOYMENT_RECORD_ID deployment: $DEPLOYMENT_RECORD_ID
type: ApplicationDeploymentRemovalRequest type: ApplicationDeploymentRemovalRequest
@@ -170,14 +170,14 @@ record:
EOF EOF
sleep 2 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 "ApplicationDeploymentRemovalRequest published"
echo $REMOVAL_REQUEST_ID echo $REMOVAL_REQUEST_ID
# Check that an ApplicationDeploymentRemovalRecord is published # Check that an ApplicationDeploymentRemovalRecord is published
retry_count=0 retry_count=0
while true; do 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') len_removal_records=$(echo $removal_records_response | jq 'length')
# Check if number of records returned is 0 # Check if number of records returned is 0
@@ -190,7 +190,7 @@ while true; do
else else
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..." echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL sleep $RETRY_INTERVAL
retry_count=$((retry_count+1)) retry_count=$((retry_count + 1))
fi fi
else else
echo "ApplicationDeploymentRemovalRecord found" echo "ApplicationDeploymentRemovalRecord found"
@@ -217,7 +217,7 @@ while true; do
else else
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..." echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval sleep $retry_interval
retry_count=$((retry_count+1)) retry_count=$((retry_count + 1))
fi fi
fi fi
done done
+1 -1
View File
@@ -14,4 +14,4 @@ VITE_BUGSNAG_API_KEY=
VITE_PASSKEY_WALLET_RPID= VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL= VITE_TURNKEY_API_BASE_URL=
VITE_TURNKEY_ORGANIZATION_ID= VITE_TURNKEY_ORGANIZATION_ID=
+2
View File
@@ -9,6 +9,7 @@ import { join, dirname } from 'path';
function getAbsolutePath(value: string): any { function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json'))); return dirname(require.resolve(join(value, 'package.json')));
} }
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [ addons: [
@@ -26,6 +27,7 @@ const config: StorybookConfig = {
docs: { docs: {
autodocs: 'tag', autodocs: 'tag',
}, },
staticDirs: ['../public'],
}; };
export default config; export default config;
+56 -23
View File
@@ -1,30 +1,63 @@
# React + TypeScript + Vite # frontend
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. This is a [vite](https://vitejs.dev/) [react](https://reactjs.org/) [nextjs](https://nextjs.org/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
Currently, two official plugins are available: ## Getting Started
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh ### Install dependencies
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration In the root of the project, run:
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: ```zsh
yarn
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
};
``` ```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` ### Build backend
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list ```zsh
yarn build --ignore frontend
```
### Environment variables
#### Local
Copy the `.env.example` file to `.env`:
```zsh
cp .env.example .env
```
#### Staging environment variables
Change in [deployer/deploy-frontend.staging.sh](/packages/deployer/deploy-frontend.staging.sh)
#### Production environment variables
Change in [deployer/deploy-frontend.sh](/packages/deployer/deploy-frontend.sh)
### Run development server
```zsh
yarn dev
```
## Deployment
From the root of the project,
### Staging
```zsh
cd packages/deployer && ./deploy-frontend.staging.sh
```
### Production
```zsh
cd packages/deployer && ./deploy-frontend.sh
```
### Deployment status
Check the status of the deployment [here](https://webapp-deployer.apps.snowballtools.com)
+3 -1
View File
@@ -16,8 +16,11 @@
"@bugsnag/browser-performance": "^2.4.1", "@bugsnag/browser-performance": "^2.4.1",
"@bugsnag/js": "^7.22.7", "@bugsnag/js": "^7.22.7",
"@bugsnag/plugin-react": "^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-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16", "@fontsource/inter": "^5.0.16",
"@mui/material": "^6.1.3",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
@@ -62,7 +65,6 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropdown": "^1.11.0", "react-dropdown": "^1.11.0",
"react-hook-form": "^7.49.0", "react-hook-form": "^7.49.0",
"react-hot-toast": "^2.4.1",
"react-oauth-popup": "^1.0.5", "react-oauth-popup": "^1.0.5",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
"react-timer-hook": "^3.0.7", "react-timer-hook": "^3.0.7",
+3 -2
View File
@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Projects from './pages/org-slug'; import Projects from './pages/org-slug';
@@ -10,8 +11,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages'; import Index from './pages';
import AuthPage from './pages/AuthPage'; import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout'; import { DashboardLayout } from './pages/org-slug/layout';
import { useEffect } from 'react';
import Web3Provider from 'context/Web3Provider'; import Web3Provider from 'context/Web3Provider';
import { baseUrl } from 'utils/constants';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -59,7 +60,7 @@ function App() {
// Hacky way of checking session // Hacky way of checking session
// TODO: Handle redirect backs // TODO: Handle redirect backs
useEffect(() => { useEffect(() => {
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, { fetch(`${baseUrl}/auth/session`, {
credentials: 'include', credentials: 'include',
}).then((res) => { }).then((res) => {
if (res.status !== 200) { if (res.status !== 200) {
+7 -2
View File
@@ -1,16 +1,21 @@
import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO,
} from 'utils/constants';
export default [ export default [
{ {
id: '1', id: '1',
name: 'Progressive Web App (PWA)', name: 'Progressive Web App (PWA)',
icon: 'pwa', icon: 'pwa',
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`, repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false, isComingSoon: false,
}, },
{ {
id: '2', id: '2',
name: 'Image Upload PWA', name: 'Image Upload PWA',
icon: 'pwa', icon: 'pwa',
repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`, repoFullName: `${VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false, isComingSoon: false,
}, },
{ {
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useCombobox } from 'downshift'; import { useCombobox } from 'downshift';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
import { useDebounce } from 'usehooks-ts'; import { useDebounceValue } from 'usehooks-ts';
import SearchBar from 'components/SearchBar'; import SearchBar from 'components/SearchBar';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
@@ -42,7 +42,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
}, },
}); });
const debouncedInputValue = useDebounce<string>(inputValue, 300); const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const fetchProjects = useCallback( const fetchProjects = useCallback(
async (inputValue: string) => { async (inputValue: string) => {
@@ -62,7 +62,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
<div className="relative w-full lg:w-fit"> <div className="relative w-full lg:w-fit">
<SearchBar {...getInputProps()} /> <SearchBar {...getInputProps()} />
<div <div
{...getMenuProps()} {...getMenuProps({}, { suppressRefError: true })}
className={cn( className={cn(
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50', 'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen }, { hidden: !inputValue || !isOpen },
@@ -5,7 +5,7 @@ import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
import { useDebounce } from 'usehooks-ts'; import { useDebounceValue } from 'usehooks-ts';
import { ProjectSearchBarItem } from './ProjectSearchBarItem'; import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty'; import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -27,25 +27,30 @@ export const ProjectSearchBarDialog = ({
const client = useGQLClient(); const client = useGQLClient();
const navigate = useNavigate(); const navigate = useNavigate();
const { getInputProps, getItemProps, inputValue, setInputValue } = const {
useCombobox({ getInputProps,
items, getItemProps,
itemToString(item) { getMenuProps,
return item ? item.name : ''; inputValue,
}, setInputValue,
selectedItem, } = useCombobox({
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { items,
if (newSelectedItem) { itemToString(item) {
setSelectedItem(newSelectedItem); return item ? item.name : '';
onClickItem?.(newSelectedItem); },
navigate( selectedItem,
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`, onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
); if (newSelectedItem) {
} setSelectedItem(newSelectedItem);
}, onClickItem?.(newSelectedItem);
}); navigate(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
);
}
},
});
const debouncedInputValue = useDebounce<string>(inputValue, 300); const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const fetchProjects = useCallback( const fetchProjects = useCallback(
async (inputValue: string) => { async (inputValue: string) => {
@@ -75,7 +80,7 @@ export const ProjectSearchBarDialog = ({
<div className="h-full flex flex-col fixed top-0 inset-0"> <div className="h-full flex flex-col fixed top-0 inset-0">
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]"> <div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<Input <Input
{...getInputProps()} {...getInputProps({}, { suppressRefError: true })}
leftIcon={<SearchIcon />} leftIcon={<SearchIcon />}
placeholder="Search" placeholder="Search"
appearance="borderless" appearance="borderless"
@@ -86,23 +91,33 @@ export const ProjectSearchBarDialog = ({
</Button> </Button>
</div> </div>
{/* Content */} {/* Content */}
<div className="flex flex-col gap-1 px-2 py-2"> <div
{items.length > 0 className="flex flex-col gap-1 px-2 py-2"
? items.map((item, index) => ( {...getMenuProps(
<> {},
<div className="px-2 py-2"> {
<p className="text-elements-mid-em text-xs font-medium"> suppressRefError: true,
Suggestions },
</p> )}
</div> >
<ProjectSearchBarItem {items.length > 0 ? (
key={item.id} <>
item={item} <div className="px-2 py-2">
{...getItemProps({ item, index })} <p className="text-elements-mid-em text-xs font-medium">
/> Suggestions
</> </p>
)) </div>
: inputValue && <ProjectSearchBarEmpty />} {items.map((item, index) => (
<ProjectSearchBarItem
key={item.id}
item={item}
{...getItemProps({ item, index })}
/>
))}
</>
) : (
inputValue && <ProjectSearchBarEmpty />
)}
</div> </div>
</div> </div>
</Dialog.Content> </Dialog.Content>
@@ -0,0 +1,236 @@
import { useCallback, useState } from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts';
import { AuctionData } from 'gql-client';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Heading } from '../../shared/Heading';
import { Button } from '../../shared/Button';
import { Select, SelectOption } from 'components/shared/Select';
import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext';
type ConfigureFormValues = {
option: string;
lrn?: string;
numProviders?: number;
maxPrice?: string;
};
const Configure = () => {
const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId');
const location = useLocation();
const { templateOwner, templateRepo, owner, name, isPrivate, orgSlug, repository } = location.state || {};
const navigate = useNavigate();
const { toast, dismiss } = useToast();
const client = useGQLClient();
const [isLoading, setIsLoading] = useState(false);
const { handleSubmit, control, watch } = useForm<ConfigureFormValues>({
defaultValues: { option: 'LRN' },
});
const selectedOption = watch('option');
const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
const onSubmit: SubmitHandler<ConfigureFormValues> = useCallback(
async (data) => {
setIsLoading(true);
try {
let lrn: string | undefined;
let auctionData: AuctionData | undefined;
if (data.option === 'LRN') {
lrn = data.lrn;
} else if (data.option === 'Auction') {
auctionData = {
numProviders: Number(data.numProviders!),
maxPrice: (data.maxPrice!).toString(),
};
}
if (templateId) {
// Template-based project creation
const projectData: any = {
templateOwner,
templateRepo,
owner,
name,
isPrivate,
};
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
orgSlug,
projectData,
lrn,
auctionData
);
data.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${addProjectFromTemplate.id}`,
{ state: { isAuction: true } }
)
: navigate(
`/${orgSlug}/projects/create/template/deploy?projectId=${addProjectFromTemplate.id}&templateId=${templateId}`
);
} else {
const { addProject } = await client.addProject(
orgSlug,
{
name: repository.fullName,
prodBranch: repository.defaultBranch,
repository: repository.fullName,
template: 'webapp',
},
lrn,
auctionData
);
data.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${addProject.id}`,
{ state: { isAuction: true } }
)
: navigate(
`/${orgSlug}/projects/create/deploy?projectId=${addProject.id}`
);
}
} catch (error) {
console.error('Error creating project:', error);
toast({
id: 'error-creating-project',
title: 'Error creating project',
variant: 'error',
onDismiss: dismiss,
});
} finally {
setIsLoading(false);
}
},
[client, isPrivate, templateId, navigate, dismiss, toast]
);
return (
<div className="space-y-7">
<div className="flex justify-between">
<div className="space-y-1.5">
<Heading as="h4" className="md:text-lg font-medium">
Configure deployment
</Heading>
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
The app can be deployed by setting the deployer LRN for a single
deployment or by creating a deployer auction for multiple
deployments
</Heading>
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-4 lg:gap-7 w-full">
<div className="flex flex-col justify-start gap-3">
<Controller
name="option"
control={control}
render={({ field: { value, onChange } }) => (
<Select
label="Configuration Options"
value={
{
value: value || 'LRN',
label: value === 'Auction' ? 'Create Auction' : 'Deployer LRN',
} as SelectOption
}
onChange={(value) => onChange((value as SelectOption).value)}
options={[
{ value: 'LRN', label: 'Deployer LRN' },
{ value: 'Auction', label: 'Create Auction' },
]}
/>
)}
/>
</div>
{selectedOption === 'LRN' && (
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
The app will be deployed by the configured deployer
</Heading>
<span className="text-sm text-elements-high-em">
Enter LRN for deployer
</span>
<Controller
name="lrn"
control={control}
render={({ field: { value, onChange } }) => (
<Input value={value} onChange={onChange} />
)}
/>
</div>
)}
{selectedOption === 'Auction' && (
<>
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
Set the number of deployers and maximum price for each deployment
</Heading>
<span className="text-sm text-elements-high-em">
Number of Deployers
</span>
<Controller
name="numProviders"
control={control}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
<div className="flex flex-col justify-start gap-3">
<span className="text-sm text-elements-high-em">
Maximum Price (alnt)
</span>
<Controller
name="maxPrice"
control={control}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
</>
)}
<div>
<Button
{...buttonSize}
type="submit"
disabled={isLoading}
rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
>
{isLoading ? 'Deploying repo' : 'Deploy repo'}
</Button>
</div>
</div>
</form>
</div>
);
};
export default Configure;
@@ -14,11 +14,10 @@ import { useToast } from '../../shared/Toast';
import { IconWithFrame } from '../../shared/IconWithFrame'; import { IconWithFrame } from '../../shared/IconWithFrame';
import { Heading } from '../../shared/Heading'; import { Heading } from '../../shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard'; import { MockConnectGitCard } from './MockConnectGitCard';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
const SCOPES = 'repo user'; const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${ const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
import.meta.env.VITE_GITHUB_CLIENT_ID
}&scope=${encodeURIComponent(SCOPES)}`;
interface ConnectAccountInterface { interface ConnectAccountInterface {
onAuth: (token: string) => void; onAuth: (token: string) => void;
@@ -38,36 +38,19 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
}); });
} }
try { navigate(`configure`,
setIsLoading(true); {
const { addProject } = await client.addProject(orgSlug, { state: {
name: `${repository.owner?.login}-${repository.name}`, repository: {
prodBranch: repository.default_branch as string, owner: repository.owner?.login,
repository: repository.full_name, name: repository.name,
// TODO: Compute template from repo defaultBranch: repository.default_branch,
template: 'webapp', fullName: repository.full_name,
}); },
if (addProject) { orgSlug,
navigate(`import?projectId=${addProject.id}`); },
} else {
toast({
id: 'failed-to-create-project',
title: 'Failed to create project',
variant: 'error',
onDismiss: dismiss,
});
} }
} catch (error) { );
console.error((error as Error).message);
toast({
id: 'failed-to-create-project',
title: 'Failed to create project',
variant: 'error',
onDismiss: dismiss,
});
} finally {
setIsLoading(false);
}
}, [client, repository, orgSlug, setIsLoading, navigate, toast]); }, [client, repository, orgSlug, setIsLoading, navigate, toast]);
return ( return (
@@ -83,7 +83,7 @@ const DeploymentDetailsCard = ({
return ( return (
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative"> <div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
<div className="flex-1 flex justify-between w-full md:max-w-[25%] lg:max-w-[28%]"> <div className="flex-1 flex justify-between w-full md:max-w-[30%] lg:max-w-[33%]">
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full"> <div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
{/* DEPLOYMENT URL */} {/* DEPLOYMENT URL */}
{deployment.url && ( {deployment.url && (
@@ -96,7 +96,12 @@ const DeploymentDetailsCard = ({
</OverflownText> </OverflownText>
</Heading> </Heading>
)} )}
<span className="text-sm text-elements-low-em tracking-tight"> {deployment.deployerLrn && (
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
Deployer LRN: {deployment.deployerLrn}
</span>
)}
<span className="text-sm text-elements-low-em tracking-tight block">
{deployment.environment === Environment.Production {deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}` ? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'} : 'Preview'}
@@ -0,0 +1,116 @@
import { useCallback, useEffect, useState } from 'react';
import { Auction, Project } from 'gql-client';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import { CheckRoundFilledIcon, LoadingIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { Button, Heading, Tag } from 'components/shared';
const WAIT_DURATION = 5000;
export const AuctionCard = ({ project }: { project: Project }) => {
const [auctionStatus, setAuctionStatus] = useState<string>('');
const [deployerLrns, setDeployerLrns] = useState<string[]>([]);
const [auctionDetails, setAuctionDetails] = useState<Auction | null>(null);
const [openDialog, setOpenDialog] = useState<boolean>(false);
const client = useGQLClient();
const getIconByAuctionStatus = (status: string) =>
status === 'completed' ? <CheckRoundFilledIcon /> : <LoadingIcon className="animate-spin" />;
const checkAuctionStatus = useCallback(async () => {
const result = await client.getAuctionData(project.auctionId);
setAuctionStatus(result.status);
setAuctionDetails(result);
setDeployerLrns(project.deployerLrns);
}, [client, project.auctionId, project.deployerLrns]);
useEffect(() => {
const fetchUpdatedProject = async () => {
if (auctionStatus === 'completed') {
// Wait for 5 secs since the project is not immediately updated with deployer LRNs
await new Promise((resolve) => setTimeout(resolve, WAIT_DURATION));
const updatedProject = await client.getProject(project.id);
setDeployerLrns(updatedProject.project!.deployerLrns || []);
}
};
if (auctionStatus !== 'completed') {
const intervalId = setInterval(checkAuctionStatus, WAIT_DURATION);
checkAuctionStatus();
return () => clearInterval(intervalId);
} else {
fetchUpdatedProject();
}
}, [auctionStatus, checkAuctionStatus, client]);
const renderAuctionStatus = useCallback(
() => (
<Tag
leftIcon={getIconByAuctionStatus(auctionStatus)}
size="xs"
type={auctionStatus === 'completed' ? 'positive' : 'emphasized'}
>
{auctionStatus.toUpperCase()}
</Tag>
),
[auctionStatus]
);
const handleOpenDialog = () => setOpenDialog(true);
const handleCloseDialog = () => setOpenDialog(false);
return (
<>
<div className="p-3 gap-2 rounded-xl border border-gray-200 transition-colors hover:bg-base-bg-alternate flex flex-col mt-8">
<div className="flex justify-between items-center">
<Heading className="text-lg leading-6 font-medium">Auction details</Heading>
<Button onClick={handleOpenDialog} variant="tertiary" size="sm">
View details
</Button>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-elements-high-em text-sm font-medium tracking-tight">Auction Status</span>
<div className="ml-2">{renderAuctionStatus()}</div>
</div>
<div className="flex justify-between items-center mt-2">
<span className="text-elements-high-em text-sm font-medium tracking-tight">Auction Id</span>
<span className="text-elements-mid-em text-sm text-right">
{project.auctionId}
</span>
</div>
{deployerLrns?.length > 0 && (
<div className="mt-3">
<span className="text-elements-high-em text-sm font-medium tracking-tight">Deployer LRNs</span>
{deployerLrns.map((lrn, index) => (
<p key={index} className="text-elements-mid-em text-sm">
{'\u2022'} {lrn}
</p>
))}
</div>
)}
</div>
<Dialog open={openDialog} onClose={handleCloseDialog} fullWidth maxWidth="md">
<DialogTitle>Auction Details</DialogTitle>
<DialogContent>
{auctionDetails && <pre>{JSON.stringify(auctionDetails, null, 2)}</pre>}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Close</Button>
</DialogActions>
</Dialog>
</>
);
};
@@ -60,8 +60,8 @@ const DeleteProjectDialog = ({
<Modal.Body> <Modal.Body>
<Input <Input
label={ label={
"Deleting your project is irreversible. Enter your project's name " + "Deleting your project is irreversible. Enter your project's name " + '"' +
project.name + project.name + '"' +
' below to confirm you want to permanently delete it:' ' below to confirm you want to permanently delete it:'
} }
id="input" id="input"
@@ -2,7 +2,6 @@ import { useState } from 'react';
import { Domain, DomainStatus, Project } from 'gql-client'; import { Domain, DomainStatus, Project } from 'gql-client';
import { import {
Chip,
Typography, Typography,
Menu, Menu,
MenuHandler, MenuHandler,
@@ -15,6 +14,15 @@ import EditDomainDialog from './EditDomainDialog';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog'; import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { Tag } from 'components/shared/Tag';
import {
CheckIcon,
CrossIcon,
GearIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
enum RefreshStatus { enum RefreshStatus {
IDLE, IDLE,
@@ -79,22 +87,29 @@ const DomainCard = ({
<> <>
<div className="flex justify-between py-3"> <div className="flex justify-between py-3">
<div className="flex justify-start gap-1"> <div className="flex justify-start gap-1">
<Typography variant="h6"> <Heading as="h6" className="flex-col">
<i>^</i> {domain.name} {domain.name}{' '}
</Typography> <Tag
<Chip type={
className="w-fit capitalize" domain.status === DomainStatus.Live ? 'positive' : 'negative'
value={domain.status} }
color={domain.status === DomainStatus.Live ? 'green' : 'orange'} leftIcon={
variant="ghost" domain.status === DomainStatus.Live ? (
icon={<i>^</i>} <CheckIcon />
/> ) : (
<CrossIcon />
)
}
>
{domain.status}
</Tag>
</Heading>
</div> </div>
<div className="flex justify-start gap-1"> <div className="flex justify-start gap-1">
<i <i
id="refresh" id="refresh"
className="cursor-pointer w-8 h-8" className="cursor-pointer"
onClick={() => { onClick={() => {
SetRefreshStatus(RefreshStatus.CHECKING); SetRefreshStatus(RefreshStatus.CHECKING);
setTimeout(() => { setTimeout(() => {
@@ -102,11 +117,17 @@ const DomainCard = ({
}, CHECK_FAIL_TIMEOUT); }, CHECK_FAIL_TIMEOUT);
}} }}
> >
{refreshStatus === RefreshStatus.CHECKING ? 'L' : 'R'} {refreshStatus === RefreshStatus.CHECKING ? (
<LoadingIcon className="animate-spin" />
) : (
'L'
)}
</i> </i>
<Menu placement="bottom-end"> <Menu placement="bottom-end">
<MenuHandler> <MenuHandler>
<button className="border-2 rounded-full w-8 h-8">...</button> <Button iconOnly>
<GearIcon />
</Button>
</MenuHandler> </MenuHandler>
<MenuList> <MenuList>
<MenuItem <MenuItem
@@ -143,13 +164,13 @@ const DomainCard = ({
{domain.status === DomainStatus.Pending && ( {domain.status === DomainStatus.Pending && (
<Card className="bg-slate-100 p-4 text-sm"> <Card className="bg-slate-100 p-4 text-sm">
{refreshStatus === RefreshStatus.IDLE ? ( {refreshStatus === RefreshStatus.IDLE ? (
<Typography variant="small"> <Heading>
^ Add these records to your domain and refresh to check ^ Add these records to your domain and refresh to check
</Typography> </Heading>
) : refreshStatus === RefreshStatus.CHECKING ? ( ) : refreshStatus === RefreshStatus.CHECKING ? (
<Typography variant="small" className="text-blue-500"> <Heading className="text-blue-500">
^ Checking records for {domain.name} ^ Checking records for {domain.name}
</Typography> </Heading>
) : ( ) : (
<div className="flex gap-2 text-red-500 mb-2"> <div className="flex gap-2 text-red-500 mb-2">
<div className="grow"> <div className="grow">
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { Controller, useForm, SubmitHandler } from 'react-hook-form'; import { Controller, useForm, SubmitHandler } from 'react-hook-form';
import toast from 'react-hot-toast';
import { Domain } from 'gql-client'; import { Domain } from 'gql-client';
import { import {
@@ -9,10 +8,11 @@ import {
Option, Option,
} from '@snowballtools/material-tailwind-react-fork'; } from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from '../../../../context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Modal } from 'components/shared/Modal'; import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
const DEFAULT_REDIRECT_OPTIONS = ['none']; const DEFAULT_REDIRECT_OPTIONS = ['none'];
@@ -40,6 +40,7 @@ const EditDomainDialog = ({
onUpdate, onUpdate,
}: EditDomainDialogProp) => { }: EditDomainDialogProp) => {
const client = useGQLClient(); const client = useGQLClient();
const { toast, dismiss } = useToast();
const getRedirectUrl = (domain: Domain) => { const getRedirectUrl = (domain: Domain) => {
const redirectDomain = domain.redirectTo; const redirectDomain = domain.redirectTo;
@@ -99,10 +100,20 @@ const EditDomainDialog = ({
if (updateDomain) { if (updateDomain) {
await onUpdate(); await onUpdate();
toast.success(`Domain ${domain.name} has been updated`); toast({
id: 'domain_id_updated',
title: `Domain ${domain.name} has been updated`,
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
reset(); reset();
toast.error(`Error updating domain ${domain.name}`); toast({
id: 'domain_id_error_update',
title: `Error updating domain ${domain.name}`,
variant: 'error',
onDismiss: dismiss,
});
} }
handleOpen(); handleOpen();
@@ -3,6 +3,8 @@ import { useState } from 'react';
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog'; import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { Input } from 'components/shared/Input';
import { CopyIcon, TrashIcon } from 'components/shared/CustomIcon';
interface WebhookCardProps { interface WebhookCardProps {
webhookUrl: string; webhookUrl: string;
@@ -14,11 +16,12 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
return ( return (
<div className="flex justify-between w-full mb-3"> <div className="flex justify-between w-full mb-3 gap-3">
{webhookUrl} <Input value={webhookUrl} disabled />
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
size="sm" iconOnly
size="md"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(webhookUrl); navigator.clipboard.writeText(webhookUrl);
toast({ toast({
@@ -29,16 +32,17 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
}); });
}} }}
> >
Copy <CopyIcon />
</Button> </Button>
<Button <Button
size="sm" iconOnly
size="md"
variant="danger" variant="danger"
onClick={() => { onClick={() => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}} }}
> >
X <TrashIcon />
</Button> </Button>
</div> </div>
<DeleteWebhookDialog <DeleteWebhookDialog
@@ -20,6 +20,7 @@ import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
import { SIDEBAR_MENU } from './constants'; import { SIDEBAR_MENU } from './constants';
import { UserSelect } from 'components/shared/UserSelect'; import { UserSelect } from 'components/shared/UserSelect';
import { baseUrl } from 'utils/constants';
interface SidebarProps { interface SidebarProps {
mobileOpen?: boolean; mobileOpen?: boolean;
@@ -83,7 +84,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
}, [orgSlug]); }, [orgSlug]);
const handleLogOut = useCallback(async () => { const handleLogOut = useCallback(async () => {
await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/logout`, { await fetch(`${baseUrl}/auth/logout`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}); });
@@ -0,0 +1,117 @@
import { useEffect, useRef } from 'react';
import { Input } from '../Input';
export interface VerifyCodeInputProps {
code: string;
setCode: (code: string) => void;
submitCode: () => void;
loading: boolean;
}
export const VerifyCodeInput = ({
code,
setCode,
submitCode,
loading,
}: VerifyCodeInputProps) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const handlePaste = (
e: React.ClipboardEvent<HTMLInputElement>,
i: number,
) => {
e.preventDefault();
const pasteData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only digits
if (pasteData.length > 0) {
let newCodeArray = code.split('');
for (let j = 0; j < pasteData.length && i + j < 6; j++) {
newCodeArray[i + j] = pasteData[j];
}
const newCode = newCodeArray.join('');
setCode(newCode);
const nextIndex = Math.min(i + pasteData.length, 5);
const nextInput = inputRefs.current[nextIndex];
if (nextInput) nextInput.focus();
if (!newCode.includes(' ')) {
submitCode();
}
}
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
i: number,
) => {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (e.key === 'Backspace') {
e.preventDefault();
const isEmpty = code[i] === ' ';
const newCode = !isEmpty
? code.slice(0, i) + ' ' + code.slice(i + 1, 6)
: code.slice(0, i - 1) + ' ' + code.slice(i, 6);
setCode(newCode.slice(0, 6));
if (i === 0 || !isEmpty) return;
const prev = inputRefs.current[i - 1];
if (prev) prev.focus();
return;
}
if (!e.key.match(/[0-9]/)) return;
e.preventDefault(); // Prevent the default event to avoid duplicate input
const newCode = code.slice(0, i) + e.key + code.slice(i + 1, 6);
setCode(newCode);
if (i === 5) {
submitCode();
return;
}
const next = inputRefs.current[i + 1];
if (next) next.focus();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
const value = e.target.value.slice(-1);
if (!value.match(/[0-9]/)) return;
const newCode = code.slice(0, i) + value + code.slice(i + 1, 6);
setCode(newCode);
if (i < 5) {
const next = inputRefs.current[i + 1];
if (next) next.focus();
}
if (!newCode.includes(' ')) {
submitCode();
}
};
useEffect(() => {
if (inputRefs.current[0]) {
inputRefs.current[0].focus();
}
}, []);
return (
<div className="flex gap-2">
{code.split('').map((char, i) => (
<Input
key={i}
value={char === ' ' ? '' : char}
ref={(el) => (inputRefs.current[i] = el)}
onChange={(e) => handleChange(e, i)}
onPaste={(e) => handlePaste(e, i)}
onKeyDown={(e) => handleKeyDown(e, i)}
disabled={!!loading}
style={{ textAlign: 'center' }} // Add this line to center text
/>
))}
</div>
);
};
@@ -0,0 +1 @@
export { VerifyCodeInput } from './VerifyCodeInput';
@@ -0,0 +1,27 @@
export { Avatar } from './Avatar';
export { Badge } from './Badge';
export { Button } from './Button';
export { Calendar } from './Calendar';
export { Checkbox } from './Checkbox';
export { DatePicker } from './DatePicker';
export { DotBorder } from './DotBorder';
export { Heading } from './Heading';
export { IconWithFrame } from './IconWithFrame';
export { InlineNotification } from './InlineNotification';
export { Input } from './Input';
export { Modal } from './Modal';
export { OverflownText } from './OverflownText';
export { Radio } from './Radio';
export { SegmentedControls } from './SegmentedControls';
export { Select } from './Select';
export { Sidebar } from './Sidebar';
export { Step } from './Steps';
export { Switch } from './Switch';
export { Table } from './Table';
export { Tabs } from './Tabs';
export { Tag } from './Tag';
export { useToast } from './Toast';
export { Tooltip } from './Tooltip';
export { UserSelect } from './UserSelect';
export { VerifyCodeInput } from './VerifyCodeInput';
export { WavyBorder } from './WavyBorder';
@@ -1,9 +1,11 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
if (!import.meta.env.VITE_WALLET_CONNECT_ID) { if (!VITE_WALLET_CONNECT_ID) {
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set'); throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
} }
+4 -4
View File
@@ -14,16 +14,16 @@ import { GQLClientProvider } from './context/GQLClientContext';
import { SERVER_GQL_PATH } from './constants'; import { SERVER_GQL_PATH } from './constants';
import { Toaster } from 'components/shared/Toast'; import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error'; import { LogErrorBoundary } from 'utils/log-error';
import { baseUrl } from 'utils/constants';
// @ts-ignore console.log(`v-0.0.9`);
console.log(`v-${__VERSION__}`);
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement, document.getElementById('root') as HTMLElement,
); );
assert(import.meta.env.VITE_SERVER_URL, 'VITE_SERVER_URL is not set in env'); assert(baseUrl, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`; const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint }); const gqlClient = new GQLClient({ gqlEndpoint });
@@ -32,7 +32,7 @@ const ProjectSearch = () => {
return ( return (
<section className="h-full flex flex-col"> <section className="h-full flex flex-col">
{/* Header */} {/* Header */}
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] z-30"> <div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] hover:z-30">
<div className="flex pr-6 pl-2 py-2 items-center"> <div className="flex pr-6 pl-2 py-2 items-center">
<div className="flex-1"> <div className="flex-1">
<ProjectSearchBar <ProjectSearchBar
@@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleFilledIcon,
LoaderIcon,
} from 'components/shared/CustomIcon';
import { WavyBorder } from 'components/shared/WavyBorder';
import { VerifyCodeInput } from 'components/shared/VerifyCodeInput';
import { verifyAccessCode } from 'utils/accessCode';
type AccessMethod = 'accesscode' | 'passkey';
type Err = { type: AccessMethod; message: string };
type AccessCodeProps = {
onCorrectAccessCode: () => void;
};
export const AccessCode: React.FC<AccessCodeProps> = ({
onCorrectAccessCode,
}) => {
const [accessCode, setAccessCode] = useState(' ');
const [error, setError] = useState<Err | null>();
const [accessMethod, setAccessMethod] = useState<AccessMethod | false>(false);
async function validateAccessCode() {
setAccessMethod('accesscode');
try {
const isValidAccessCode = await verifyAccessCode(accessCode);
// add a pause for ux
await new Promise((resolve) => setTimeout(resolve, 250));
if (isValidAccessCode) {
localStorage.setItem('accessCode', accessCode);
onCorrectAccessCode();
} else {
setError({
type: 'accesscode',
message: 'Invalid access code',
});
}
} catch (err: any) {
setError({ type: 'accesscode', message: err.message });
}
}
const loading = accessMethod;
const isValidAccessCodeLength = accessCode.trim().length === 5;
return (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
Access Code
</div>
</div>
<WavyBorder className="self-stretch" variant="stroke" />
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
<div className="self-stretch flex-col gap-8 flex">
<div className="flex-col justify-start items-start gap-2 inline-flex">
<VerifyCodeInput
loading={!!loading}
code={accessCode}
setCode={setAccessCode}
submitCode={validateAccessCode}
/>
</div>
<Button
rightIcon={
loading && loading === 'accesscode' ? (
<LoaderIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon height="16" />
)
}
onClick={validateAccessCode}
variant={'secondary'}
disabled={!accessCode || !isValidAccessCodeLength || !!loading}
>
Submit
</Button>
{error && error.type === 'accesscode' && (
<div className="flex flex-col gap-3">
<div className="justify-center items-center gap-2 inline-flex">
<div className="text-red-500 text-sm">
Error: {error.message}.{' '}
<a href="/signup" className="underline">
Try again?
</a>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1,20 @@
import React, { useState } from 'react';
import { AccessCode } from './AccessCode';
import { SignUp } from './SignUp';
type AccessSignUpProps = {
onDone: () => void;
};
export const AccessSignUp: React.FC<AccessSignUpProps> = ({ onDone }) => {
const [isValidAccessCode, setIsValidAccessCode] = useState<boolean>(
!!localStorage.getItem('accessCode'),
);
return isValidAccessCode ? (
<SignUp onDone={onDone} />
) : (
<AccessCode onCorrectAccessCode={() => setIsValidAccessCode(true)} />
);
};
@@ -21,6 +21,7 @@ import {
turnkeySignin, turnkeySignin,
turnkeySignup, turnkeySignup,
} from 'utils/turnkey-frontend'; } from 'utils/turnkey-frontend';
import { verifyAccessCode } from 'utils/accessCode';
type Provider = 'google' | 'github' | 'apple' | 'email'; type Provider = 'google' | 'github' | 'apple' | 'email';
@@ -111,6 +112,29 @@ export const SignUp = ({ onDone }: Props) => {
const loading = provider; const loading = provider;
const emailValid = /.@./.test(email); const emailValid = /.@./.test(email);
useEffect(() => {
const validateAccessCode = async () => {
const accessCode = localStorage.getItem('accessCode');
if (!accessCode) {
redirectToSignup();
return;
}
try {
await verifyAccessCode(accessCode);
} catch (err: any) {
redirectToSignup();
}
};
const redirectToSignup = () => {
localStorage.removeItem('accessCode');
window.location.href = '/signup';
};
validateAccessCode();
}, []);
return ( return (
<div> <div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex"> <div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
@@ -1,14 +1,15 @@
import React, { useEffect, useState } from 'react';
import { snowball } from 'utils/use-snowball'; import { snowball } from 'utils/use-snowball';
import { Login } from './Login'; import { Login } from './Login';
import { SignUp } from './SignUp';
import { useEffect, useState } from 'react';
import { Done } from './Done'; import { Done } from './Done';
import { AccessSignUp } from './AccessSignUp';
type Screen = 'login' | 'signup' | 'success'; type Screen = 'login' | 'signup' | 'success';
const DASHBOARD_URL = '/'; const DASHBOARD_URL = '/';
export const SnowballAuth = () => { export const SnowballAuth: React.FC = () => {
const path = window.location.pathname; const path = window.location.pathname;
const [screen, setScreen] = useState<Screen>( const [screen, setScreen] = useState<Screen>(
path === '/login' ? 'login' : 'signup', path === '/login' ? 'login' : 'signup',
@@ -30,7 +31,7 @@ export const SnowballAuth = () => {
if (screen === 'signup') { if (screen === 'signup') {
return ( return (
<SignUp <AccessSignUp
onDone={() => { onDone={() => {
setScreen('success'); setScreen('success');
}} }}
@@ -36,6 +36,7 @@ const deployment: Deployment = {
url: 'https://deploy1.example.com', url: 'https://deploy1.example.com',
environment: Environment.Production, environment: Environment.Production,
isCurrent: true, isCurrent: true,
deployerLrn: 'lrn://deepstack-test4/deployers/webapp-deployer-api.test4.wireitin.com',
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
createdBy: { createdBy: {
id: 'user1', id: 'user1',
@@ -1,5 +1,19 @@
const Settings = () => { import { Heading } from 'components/shared/Heading';
return <div className="p-5">Settings page</div>;
};
const Settings = () => {
return (
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
{/* Header */}
<div className="flex items-center">
<div className="grow">
<div className="flex gap-4 items-center">
<Heading as="h2" className="text-[24px]">
Settings
</Heading>
</div>
</div>
</div>
</section>
);
};
export default Settings; export default Settings;
@@ -1,13 +1,11 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import { PlusIcon } from 'components/shared/CustomIcon';
import { ProjectCard } from 'components/projects/ProjectCard'; import { ProjectCard } from 'components/projects/ProjectCard';
import { Heading } from 'components/shared/Heading'; import { Heading, Badge, Button } from 'components/shared';
import { Badge } from 'components/shared/Badge'; import { PlusIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
const Projects = () => { const Projects = () => {
const client = useGQLClient(); const client = useGQLClient();
@@ -28,6 +26,7 @@ const Projects = () => {
return ( return (
<section className="px-4 md:px-6 py-6 flex flex-col gap-6"> <section className="px-4 md:px-6 py-6 flex flex-col gap-6">
{/* Header */} {/* Header */}
<div className="flex items-center"> <div className="flex items-center">
<div className="grow"> <div className="grow">
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
@@ -31,6 +31,11 @@ const CreateWithTemplate = () => {
}, },
{ {
step: 2, step: 2,
route: `/${orgSlug}/projects/create/template/configure`,
label: 'Configure',
},
{
step: 3,
route: `/${orgSlug}/projects/create/template/deploy`, route: `/${orgSlug}/projects/create/template/deploy`,
label: 'Deploy', label: 'Deploy',
}, },
@@ -2,7 +2,8 @@ import NewProject from './index';
import CreateWithTemplate from './Template'; import CreateWithTemplate from './Template';
import { templateRoutes } from './template/routes'; import { templateRoutes } from './template/routes';
import Id from './success/Id'; import Id from './success/Id';
import Import from './Import'; import Configure from 'components/projects/create/Configure';
import Deploy from 'components/projects/create/Deploy';
export const createProjectRoutes = [ export const createProjectRoutes = [
{ {
@@ -19,7 +20,11 @@ export const createProjectRoutes = [
element: <Id />, element: <Id />,
}, },
{ {
path: 'import', path: 'configure',
element: <Import />, element: <Configure />,
},
{
path: 'deploy',
element: <Deploy />,
}, },
]; ];
@@ -1,108 +1,125 @@
import { Link, useParams } from 'react-router-dom'; import { Link, useLocation, useParams } from 'react-router-dom';
import Lottie from 'lottie-react'; import Lottie from 'lottie-react';
import { Badge } from 'components/shared/Badge'; import { Badge } from 'components/shared/Badge';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { import {
ArrowLeftCircleFilledIcon, ArrowLeftCircleFilledIcon,
LinkChainIcon,
QuestionMarkRoundFilledIcon, QuestionMarkRoundFilledIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
import logoAnimation from 'components/../../public/lottie/logo.json'; import logoAnimation from 'components/../../public/lottie/logo.json';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useEffect, useState } from 'react';
const Id = () => { const Id = () => {
const { id, orgSlug } = useParams(); const { id, orgSlug } = useParams();
const client = useGQLClient();
const [project, setProject] = useState<Project | null>(null);
const location = useLocation();
const { isAuction } = location.state || {};
const handleSetupDomain = () => { const handleSetupDomain = async () => {
//TODO: Implement this if (id) {
// console.log('id', id);
// console.log('getting project for id', id);
const project = await client.getProject(id);
// console.log('project found:', project);
if (project && project.project) {
// console.log('project:', project.project);
setProject(project.project);
}
} else {
window.location.href = '/';
}
}; };
useEffect(() => {
handleSetupDomain();
});
return ( return (
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12"> <>
{/* Icon */} {project ? (
<div className="flex justify-center"> <div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
<Lottie animationData={logoAnimation} loop={false} size={40} /> {/* Icon */}
</div> <div className="flex justify-center">
<Lottie animationData={logoAnimation} loop={false} size={40} />
{/* Heading */}
<div className="flex flex-col items-center gap-1.5">
<Heading as="h3" className="font-medium text-xl">
Project deployed successfully.
</Heading>
<p className="flex flex-col items-center lg:flex-row font-sans gap-0.5 lg:gap-2 text-sm text-elements-high-em">
Your project has been deployed at{' '}
<Button
className="no-underline text-elements-link"
// TODO: use dynamic value
href="https://www.iglootools.snowballtools.xyz"
as="a"
variant="link-emphasized"
external
leftIcon={<LinkChainIcon />}
>
{/* // TODO: use dynamic value */}
www.iglootools.snowballtools.xyz
</Button>
</p>
</div>
{/* Card */}
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
{/* Trigger question */}
<div className="flex gap-2 justify-center items-center py-3">
<div className="h-5 w-5">
<QuestionMarkRoundFilledIcon size={18} />
</div> </div>
<Heading as="h5" className="font-sans font-medium text-sm">
{`Wondering what's next?`}
</Heading>
</div>
{/* CTA card */} {/* Heading */}
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4"> <div className="flex flex-col items-center gap-1.5">
<div className="flex gap-2"> <Heading as="h3" className="font-medium text-xl">
<Badge variant="secondary">1</Badge> {isAuction? 'Auction created successfully.' : 'Project deployment created successfully.'}
<div className="space-y-3"> </Heading>
<div className="flex flex-col gap-1"> </div>
<Heading as="h6" className="text-sm font-sans">
Add a custom domain {/* Card */}
</Heading> <div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
<p className="text-xs text-elements-low-em font-sans"> {/* Trigger question */}
Make it easy for your visitors to remember your URL with a <div className="flex gap-2 justify-center items-center py-3">
custom domain. <div className="h-5 w-5">
</p> <QuestionMarkRoundFilledIcon size={18} />
</div> </div>
<Button onClick={handleSetupDomain} variant="tertiary" size="sm"> <Heading as="h5" className="font-sans font-medium text-sm">
Setup domain {`Wondering what's next?`}
</Button> </Heading>
</div>
{/* CTA card */}
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
<div className="flex gap-2">
<Badge variant="secondary">1</Badge>
<div className="space-y-3">
<div className="flex flex-col gap-1">
<Heading as="h6" className="text-sm font-sans">
Add a custom domain
</Heading>
<p className="text-xs text-elements-low-em font-sans">
Make it easy for your visitors to remember your URL with a
custom domain.
</p>
</div>
<Button
onClick={handleSetupDomain}
variant="tertiary"
size="sm"
>
Setup domain
</Button>
</div>
</div>
</div>
</div>
{/* CTA Buttons */}
<div className="flex flex-col lg:flex-row justify-center gap-3">
<div className="w-full lg:w-fit">
<Link to="/">
<Button
leftIcon={<ArrowLeftCircleFilledIcon />}
fullWidth
variant="tertiary"
>
Back to projects
</Button>
</Link>
</div>
<div className="w-full lg:w-fit">
<Link to={`/${orgSlug}/projects/${id}`}>
<Button fullWidth variant="primary">
View project
</Button>
</Link>
</div> </div>
</div> </div>
</div> </div>
</div> ) : (
<></>
{/* CTA Buttons */} )}
<div className="flex flex-col lg:flex-row justify-center gap-3"> </>
<div className="w-full lg:w-fit">
<Link to="/">
<Button
leftIcon={<ArrowLeftCircleFilledIcon />}
fullWidth
variant="tertiary"
>
Back to projects
</Button>
</Link>
</div>
<div className="w-full lg:w-fit">
<Link to={`/${orgSlug}/projects/${id}`}>
<Button fullWidth variant="primary">
View project
</Button>
</Link>
</div>
</div>
</div>
); );
}; };
@@ -0,0 +1,7 @@
import ConfigureComponent from '../../../../../components/projects/create/Configure';
const Configure = () => {
return <ConfigureComponent />;
};
export default Configure;
@@ -6,12 +6,14 @@ import { useMediaQuery } from 'usehooks-ts';
import { RequestError } from 'octokit'; import { RequestError } from 'octokit';
import { useOctokit } from '../../../../../context/OctokitContext'; import { useOctokit } from '../../../../../context/OctokitContext';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import { Template } from '../../../../../types/types'; import { Template } from '../../../../../types/types';
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { Select, SelectOption } from 'components/shared/Select'; import { Select, SelectOption } from 'components/shared/Select';
import { ArrowRightCircleFilledIcon } from 'components/shared/CustomIcon'; import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Checkbox } from 'components/shared/Checkbox'; import { Checkbox } from 'components/shared/Checkbox';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
@@ -28,7 +30,6 @@ type SubmitRepoValues = {
const CreateRepo = () => { const CreateRepo = () => {
const { octokit, isAuth } = useOctokit(); const { octokit, isAuth } = useOctokit();
const { template } = useOutletContext<{ template: Template }>(); const { template } = useOutletContext<{ template: Template }>();
const client = useGQLClient();
const { orgSlug } = useParams(); const { orgSlug } = useParams();
const { toast, dismiss } = useToast(); const { toast, dismiss } = useToast();
@@ -50,36 +51,21 @@ const CreateRepo = () => {
assert(template.repoFullName, 'Template URL not provided'); assert(template.repoFullName, 'Template URL not provided');
const [owner, repo] = template.repoFullName.split('/'); const [owner, repo] = template.repoFullName.split('/');
// TODO: Handle this functionality in backend setIsLoading(true);
const gitRepo = await octokit?.rest.repos.createUsingTemplate({
template_owner: owner,
template_repo: repo,
owner: data.account,
name: data.repoName,
include_all_branches: false,
private: data.isPrivate,
});
if (!gitRepo) { navigate(
return; `configure?templateId=${template.id}`,
} {
state: {
// Refetch to always get correct default branch templateOwner: owner,
const templateRepo = await octokit.rest.repos.get({ templateRepo: repo,
owner: template.repoFullName.split('/')[0], owner: data.account,
repo: template.repoFullName.split('/')[1], name: data.repoName,
}); isPrivate: false,
const prodBranch = templateRepo.data.default_branch ?? 'main'; orgSlug
},
const { addProject } = await client.addProject(orgSlug!, { }
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`, );
prodBranch,
repository: gitRepo.data.full_name,
// TODO: Set selected template
template: 'webapp',
});
navigate(`deploy?projectId=${addProject.id}&templateId=${template.id}`);
} catch (err) { } catch (err) {
setIsLoading(false); setIsLoading(false);
@@ -206,9 +192,15 @@ const CreateRepo = () => {
{...buttonSize} {...buttonSize}
type="submit" type="submit"
disabled={!Boolean(template.repoFullName) || isLoading} disabled={!Boolean(template.repoFullName) || isLoading}
rightIcon={<ArrowRightCircleFilledIcon />} rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
> >
Deploy Next
</Button> </Button>
</div> </div>
</div> </div>
@@ -1,4 +1,5 @@
import CreateRepo from './index'; import CreateRepo from './index';
import Configure from './Configure';
import Deploy from './Deploy'; import Deploy from './Deploy';
export const templateRoutes = [ export const templateRoutes = [
@@ -6,6 +7,10 @@ export const templateRoutes = [
index: true, index: true,
element: <CreateRepo />, element: <CreateRepo />,
}, },
{
path: 'configure',
element: <Configure />,
},
{ {
path: 'deploy', path: 'deploy',
element: <Deploy />, element: <Deploy />,
@@ -1,14 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Domain, DomainStatus } from 'gql-client';
import { Link, useNavigate, useOutletContext } from 'react-router-dom'; import { Link, useNavigate, useOutletContext } from 'react-router-dom';
import { RequestError } from 'octokit'; import { RequestError } from 'octokit';
import { useOctokit } from '../../../../context/OctokitContext'; import { useOctokit } from 'context/OctokitContext';
import { GitCommitWithBranch, OutletContextType } from '../../../../types'; import { GitCommitWithBranch, OutletContextType } from '../../../../types';
import { useGQLClient } from '../../../../context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Button } from 'components/shared/Button'; import { Button, Heading, Avatar, Tag } from 'components/shared';
import { Heading } from 'components/shared/Heading';
import { Avatar } from 'components/shared/Avatar';
import { getInitials } from 'utils/geInitials'; import { getInitials } from 'utils/geInitials';
import { import {
BranchStrokeIcon, BranchStrokeIcon,
@@ -18,12 +15,13 @@ import {
GithubStrokeIcon, GithubStrokeIcon,
GlobeIcon, GlobeIcon,
LinkIcon, LinkIcon,
CalendarDaysIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { Tag } from 'components/shared/Tag';
import { Activity } from 'components/projects/project/overview/Activity'; import { Activity } from 'components/projects/project/overview/Activity';
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo'; import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
import { CalendarDaysIcon } from 'components/shared/CustomIcon/CalendarDaysIcon';
import { relativeTimeMs } from 'utils/time'; import { relativeTimeMs } from 'utils/time';
import { Domain, DomainStatus } from 'gql-client';
import { AuctionCard } from 'components/projects/project/overview/Activity/AuctionCard';
const COMMITS_PER_PAGE = 4; const COMMITS_PER_PAGE = 4;
@@ -131,9 +129,15 @@ const OverviewTabPanel = () => {
<Heading className="text-lg leading-6 font-medium truncate"> <Heading className="text-lg leading-6 font-medium truncate">
{project.name} {project.name}
</Heading> </Heading>
<p className="text-sm text-elements-low-em tracking-tight truncate"> {project.baseDomains && project.baseDomains.length > 0 && project.baseDomains.map((baseDomain, index) => (
{project.subDomain} <a
</p> key={index}
href={`https://${project.name}.${baseDomain}`}
className="text-sm text-elements-low-em tracking-tight truncate"
>
{baseDomain}
</a>
))}
</div> </div>
</div> </div>
<OverviewInfo label="Domain" icon={<GlobeIcon />}> <OverviewInfo label="Domain" icon={<GlobeIcon />}>
@@ -205,6 +209,7 @@ const OverviewTabPanel = () => {
No current deployment found. No current deployment found.
</p> </p>
)} )}
{project.auctionId && <AuctionCard project={project}/>}
</div> </div>
<Activity activities={activities} isLoading={fetchingActivities} /> <Activity activities={activities} isLoading={fetchingActivities} />
</div> </div>
@@ -1,14 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Environment, EnvironmentVariable } from 'gql-client';
import { Collapse } from '@snowballtools/material-tailwind-react-fork'; import { Collapse } from '@snowballtools/material-tailwind-react-fork';
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow'; import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables'; import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
import { useGQLClient } from '../../../../../context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { EnvironmentVariablesFormValues } from '../../../../../types'; import { EnvironmentVariablesFormValues } from '../../../../../types';
import HorizontalLine from 'components/HorizontalLine'; import HorizontalLine from 'components/HorizontalLine';
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
@@ -17,10 +15,13 @@ import { Checkbox } from 'components/shared/Checkbox';
import { PlusIcon } from 'components/shared/CustomIcon'; import { PlusIcon } from 'components/shared/CustomIcon';
import { InlineNotification } from 'components/shared/InlineNotification'; import { InlineNotification } from 'components/shared/InlineNotification';
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer'; import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
import { useToast } from 'components/shared/Toast';
import { Environment, EnvironmentVariable } from 'gql-client';
export const EnvironmentVariablesTabPanel = () => { export const EnvironmentVariablesTabPanel = () => {
const { id } = useParams(); const { id } = useParams();
const client = useGQLClient(); const client = useGQLClient();
const { toast, dismiss } = useToast();
const [environmentVariables, setEnvironmentVariables] = useState< const [environmentVariables, setEnvironmentVariables] = useState<
EnvironmentVariable[] EnvironmentVariable[]
@@ -118,13 +119,25 @@ export const EnvironmentVariablesTabPanel = () => {
fetchEnvironmentVariables(id); fetchEnvironmentVariables(id);
toast.success( toast({
createFormData.variables.length > 1 id:
? `${createFormData.variables.length} variables added` createFormData.variables.length > 1
: `Variable added`, ? 'env_variable_added'
); : 'env_variables_added',
title:
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
toast.error('Environment variables not added'); toast({
id: 'env_variables_not_added',
title: 'Environment variables not added',
variant: 'error',
onDismiss: dismiss,
});
} }
}, },
[id, client], [id, client],
@@ -1,12 +1,12 @@
import toast from 'react-hot-toast';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useGQLClient } from '../../../../../../../context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Table } from 'components/shared/Table'; import { Table } from 'components/shared/Table';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { InlineNotification } from 'components/shared/InlineNotification'; import { InlineNotification } from 'components/shared/InlineNotification';
import { ArrowRightCircleIcon } from 'components/shared/CustomIcon'; import { ArrowRightCircleIcon } from 'components/shared/CustomIcon';
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer'; import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
import { useToast } from 'components/shared/Toast';
const Config = () => { const Config = () => {
const { id, orgSlug } = useParams(); const { id, orgSlug } = useParams();
@@ -14,15 +14,26 @@ const Config = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const primaryDomainName = searchParams.get('name'); const primaryDomainName = searchParams.get('name');
const { toast, dismiss } = useToast();
const handleSubmitDomain = async () => { const handleSubmitDomain = async () => {
if (primaryDomainName === null) { if (primaryDomainName === null) {
toast.error('Cannot resolve domain name'); toast({
id: 'unresolvable_domain_name',
title: 'Cannot resolve domain name',
variant: 'error',
onDismiss: dismiss,
});
return; return;
} }
if (id === undefined) { if (id === undefined) {
toast.error('Cannot find project'); toast({
id: 'domain_cannot_find_project',
title: 'Cannot find project',
variant: 'error',
onDismiss: dismiss,
});
return; return;
} }
@@ -31,10 +42,20 @@ const Config = () => {
}); });
if (isAdded) { if (isAdded) {
toast.success('Domain added successfully'); toast({
id: 'domain_added_successfully',
title: 'Domain added successfully',
variant: 'success',
onDismiss: dismiss,
});
navigate(`/${orgSlug}/projects/${id}/settings/domains`); navigate(`/${orgSlug}/projects/${id}/settings/domains`);
} else { } else {
toast.error('Error adding domain'); toast({
id: 'generic_error_adding_domain',
title: 'Error adding domaint',
variant: 'error',
onDismiss: dismiss,
});
} }
}; };
@@ -6,9 +6,13 @@ const meta: Meta<typeof ChevronDownSmallIcon> = {
title: 'Icons/ChevronDownSmallIcon', title: 'Icons/ChevronDownSmallIcon',
component: ChevronDownSmallIcon, component: ChevronDownSmallIcon,
tags: ['autodocs'], tags: ['autodocs'],
args: { argTypes: {
size: 'string | number' as unknown as any, size: {
name: 'string', control: 'text',
},
name: {
control: 'text',
},
}, },
}; };
@@ -6,9 +6,13 @@ const meta: Meta<typeof ChevronRight> = {
title: 'Icons/ChevronRight', title: 'Icons/ChevronRight',
component: ChevronRight, component: ChevronRight,
tags: ['autodocs'], tags: ['autodocs'],
args: { argTypes: {
size: 'string | number' as unknown as any, size: {
name: 'string', control: 'text',
},
name: {
control: 'text',
},
}, },
}; };
@@ -6,9 +6,13 @@ const meta: Meta<typeof ChevronUpDown> = {
title: 'Icons/ChevronUpDown', title: 'Icons/ChevronUpDown',
component: ChevronUpDown, component: ChevronUpDown,
tags: ['autodocs'], tags: ['autodocs'],
args: { argTypes: {
size: 'string | number' as unknown as any, size: {
name: 'string', control: 'text',
},
name: {
control: 'text',
},
}, },
}; };
@@ -3,12 +3,16 @@ import { Meta, StoryObj } from '@storybook/react';
import { ChevronUpSmallIcon } from 'components/shared/CustomIcon'; import { ChevronUpSmallIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof ChevronUpSmallIcon> = { const meta: Meta<typeof ChevronUpSmallIcon> = {
title: 'Components/ChevronUpSmallIcon', title: 'Icons/ChevronUpSmallIcon',
component: ChevronUpSmallIcon, component: ChevronUpSmallIcon,
tags: ['autodocs'], tags: ['autodocs'],
args: { argTypes: {
size: 'string | number' as unknown as any, size: {
name: 'string', control: 'text',
},
name: {
control: 'text',
},
}, },
}; };
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { CollaboratorsIcon } from 'components/shared/CustomIcon'; import { CollaboratorsIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof CollaboratorsIcon> = { const meta: Meta<typeof CollaboratorsIcon> = {
title: 'Components/CollaboratorsIcon', title: 'Icons/CollaboratorsIcon',
component: CollaboratorsIcon, component: CollaboratorsIcon,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
@@ -0,0 +1,20 @@
import { StoryObj, Meta } from '@storybook/react';
import { ProjectSearchBar } from 'components/projects/ProjectSearchBar';
const meta: Meta<typeof ProjectSearchBar> = {
title: 'Components/ProjectSearchBar',
component: ProjectSearchBar,
tags: ['autodocs'],
argTypes: {
onChange: {
action: 'change',
},
},
};
export default meta;
type Story = StoryObj<typeof ProjectSearchBar>;
export const Default: Story = {};
@@ -11,6 +11,7 @@ import {
DomainStatus, DomainStatus,
Domain, Domain,
Environment, Environment,
Permission,
} from 'gql-client'; } from 'gql-client';
export const user: User = { export const user: User = {
@@ -44,7 +45,7 @@ export const organization: Organization = {
export const member: ProjectMember = { export const member: ProjectMember = {
id: '1', id: '1',
member: user, member: user,
permissions: [], permissions: [Permission.Edit],
isPending: false, isPending: false,
createdAt: '2021-08-01T00:00:00.000Z', createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z', updatedAt: '2021-08-01T00:00:00.000Z',
@@ -70,7 +71,7 @@ export const environmentVariable1: EnvironmentVariable = {
export const domain0: Domain = { export const domain0: Domain = {
id: '1', id: '1',
name: 'Domain', name: 'domain.com',
createdAt: '2021-08-01T00:00:00.000Z', createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z', updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch', branch: 'Branch',
@@ -78,6 +79,16 @@ export const domain0: Domain = {
redirectTo: null, redirectTo: null,
}; };
export const domain1: Domain = {
id: '2',
name: 'www.domain.com',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch',
status: DomainStatus.Live,
redirectTo: domain0,
};
export const deployment0: Deployment = { export const deployment0: Deployment = {
id: '1', id: '1',
url: 'https://deployment.com', url: 'https://deployment.com',
@@ -91,6 +102,7 @@ export const deployment0: Deployment = {
domain: domain0, domain: domain0,
commitMessage: 'Commit Message', commitMessage: 'Commit Message',
createdBy: user, createdBy: user,
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
}; };
export const project: Project = { export const project: Project = {
@@ -108,7 +120,9 @@ export const project: Project = {
organization: organization, organization: organization,
template: 'Template', template: 'Template',
members: [member], members: [member],
auctionId: '7553538436710373822151221341b43f577e07b0525d083cc9b2de98890138a1',
deployerLrns: ['lrn://deployer.apps.snowballtools.com '],
webhooks: ['beepboop'], webhooks: ['beepboop'],
icon: 'Icon', icon: 'Icon',
subDomain: 'SubDomain', baseDomains: ['baseDomain'],
}; };
@@ -0,0 +1,48 @@
import { StoryObj, Meta } from '@storybook/react';
import DomainCard from 'components/projects/project/settings/DomainCard';
import { domain0, domain1, project } from '../../MockStoriesData';
const meta: Meta<typeof DomainCard> = {
title: 'Project/Settings/DomainCard',
component: DomainCard,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
project: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof DomainCard>;
export const Default: Story = {
args: {
domains: [domain0, domain1],
domain: domain0,
branches: ['main'],
project: project,
},
};
@@ -0,0 +1,43 @@
import { StoryObj, Meta } from '@storybook/react';
import EditDomainDialog from 'components/projects/project/settings/EditDomainDialog';
const meta: Meta<typeof EditDomainDialog> = {
title: 'Components/EditDomainDialog',
component: EditDomainDialog,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
open: {
control: {
type: 'boolean',
},
},
handleOpen: {
action: 'open',
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof EditDomainDialog>;
export const Default: Story = {};
@@ -0,0 +1,29 @@
import { Meta, StoryObj } from '@storybook/react';
import WebhookCard from 'components/projects/project/settings/WebhookCard';
const meta: Meta<typeof WebhookCard> = {
title: 'Project/Settings/WebhookCard',
component: WebhookCard,
tags: ['autodocs'],
argTypes: {
webhookUrl: {
control: {
type: 'text',
},
},
onDelete: {
action: 'delete',
},
},
};
export default meta;
type Story = StoryObj<typeof WebhookCard>;
export const Default: Story = {
args: {
webhookUrl: 'https://api.retool.com',
},
};
+28
View File
@@ -0,0 +1,28 @@
import { baseUrl } from './constants';
export async function verifyAccessCode(
accesscode: string,
): Promise<boolean | null> {
const res = await fetch(`${baseUrl}/auth/accesscode`, {
method: 'POST',
body: JSON.stringify({
accesscode,
}),
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
});
// If API returns a non-empty 200, this is a valid access code.
if (res.status == 200) {
const isValid = (await res.json()).isValid as boolean;
console.log('isValid', isValid);
return isValid;
} else if (res.status === 204) {
return null;
} else {
throw new Error(
`Unexpected response from access code endpoint: ${res.status}: ${await res.text()}`,
);
}
}
+11
View File
@@ -0,0 +1,11 @@
export const baseUrl = import.meta.env.VITE_SERVER_URL;
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
+8 -8
View File
@@ -1,21 +1,21 @@
import React from 'react';
import Bugsnag from '@bugsnag/js'; import Bugsnag from '@bugsnag/js';
import BugsnagPluginReact from '@bugsnag/plugin-react'; import BugsnagPluginReact from '@bugsnag/plugin-react';
import BugsnagPerformance from '@bugsnag/browser-performance'; import BugsnagPerformance from '@bugsnag/browser-performance';
import React from 'react';
const bugsnagApiKey = import.meta.env.VITE_BUGSNAG_API_KEY; import { VITE_BUGSNAG_API_KEY } from './constants';
if (bugsnagApiKey) { if (VITE_BUGSNAG_API_KEY) {
Bugsnag.start({ Bugsnag.start({
apiKey: bugsnagApiKey, apiKey: VITE_BUGSNAG_API_KEY,
plugins: [new BugsnagPluginReact()], plugins: [new BugsnagPluginReact()],
}); });
BugsnagPerformance.start({ apiKey: bugsnagApiKey }); BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
} }
export const errorLoggingEnabled = !!bugsnagApiKey; export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
export const LogErrorBoundary = bugsnagApiKey export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
? Bugsnag.getPlugin('react')!.createErrorBoundary(React) ? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
: ({ children }: any) => children; : ({ children }: any) => children;
@@ -28,7 +28,7 @@ export function logError(error: Error) {
} }
console.error(...errors); console.error(...errors);
if (import.meta.env.VITE_BUGSNAG_API_KEY) { if (VITE_BUGSNAG_API_KEY) {
Bugsnag.notify(error); Bugsnag.notify(error);
} }
} }
+3 -1
View File
@@ -2,6 +2,8 @@ import { SiweMessage } from 'siwe';
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers'; import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { baseUrl } from './constants';
const domain = window.location.host; const domain = window.location.host;
const origin = window.location.origin; const origin = window.location.origin;
@@ -17,7 +19,7 @@ export async function signInWithEthereum(
); );
const signature = await wallet.signMessage(message); const signature = await wallet.signMessage(message);
const res = await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/validate`, { const res = await fetch(`${baseUrl}/auth/validate`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -1,10 +1,7 @@
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http'; import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
import { WebauthnStamper } from '@turnkey/webauthn-stamper'; import { WebauthnStamper } from '@turnkey/webauthn-stamper';
const baseUrl = import.meta.env.VITE_SERVER_URL; import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms // All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// We only support ES256, which is listed here // We only support ES256, which is listed here
+4 -3
View File
@@ -5,16 +5,17 @@ import {
LitGoogleAuth, LitGoogleAuth,
LitPasskeyAuth, LitPasskeyAuth,
} from '@snowballtools/auth-lit'; } from '@snowballtools/auth-lit';
import { VITE_LIT_RELAY_API_KEY } from './constants';
export const snowball = Snowball.withAuth({ export const snowball = Snowball.withAuth({
google: LitGoogleAuth.configure({ google: LitGoogleAuth.configure({
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!, litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
}), }),
// apple: LitAppleAuth.configure({ // apple: LitAppleAuth.configure({
// litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!, // litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
// }), // }),
passkey: LitPasskeyAuth.configure({ passkey: LitPasskeyAuth.configure({
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!, litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
}), }),
}).create({ }).create({
initialChain: SnowballChain.sepolia, initialChain: SnowballChain.sepolia,
-1
View File
@@ -13,7 +13,6 @@ export default defineConfig({
context: '/src/context', context: '/src/context',
components: '/src/components', components: '/src/components',
pages: '/src/pages', pages: '/src/pages',
types: '/src/types',
}, },
}, },
define: { define: {
+59 -3
View File
@@ -18,6 +18,42 @@ declare enum DeploymentStatus {
Error = "Error", Error = "Error",
Deleting = "Deleting" Deleting = "Deleting"
} }
declare enum AuctionStatus {
AuctionStatusCommitPhase = "commit",
AuctionStatusRevealPhase = "reveal",
AuctionStatusExpired = "expired",
AuctionStatusCompleted = "completed"
}
type Bid = {
auctionId: string;
bidderAddress: string;
status: string;
commitHash: string;
commitTime?: Date;
commitFee?: string;
revealTime?: Date;
revealFee?: string;
bidAmount?: string;
};
type Auction = {
id: string;
kind: string;
status: string;
ownerAddress: string;
createTime?: Date;
commitsEndTime?: Date;
revealsEndTime?: Date;
commitFee?: string;
revealFee?: string;
minimumBid?: string;
winnerAddresses: string[];
winnerBids?: string[];
winnerPrice?: string;
maxPrice?: string;
numProviders: number;
fundsReleased: boolean;
bids: Bid[];
};
declare enum DomainStatus { declare enum DomainStatus {
Live = "Live", Live = "Live",
Pending = "Pending" Pending = "Pending"
@@ -55,8 +91,10 @@ type Deployment = {
commitHash: string; commitHash: string;
commitMessage: string; commitMessage: string;
url?: string; url?: string;
deployerLrn: string;
environment: Environment; environment: Environment;
isCurrent: boolean; isCurrent: boolean;
baseDomain?: string;
status: DeploymentStatus; status: DeploymentStatus;
createdBy: User; createdBy: User;
createdAt: string; createdAt: string;
@@ -112,6 +150,8 @@ type Project = {
description: string; description: string;
template: string; template: string;
framework: string; framework: string;
deployerLrns: string[];
auctionId: string;
webhooks: string[]; webhooks: string[];
members: ProjectMember[]; members: ProjectMember[];
environmentVariables: EnvironmentVariable[]; environmentVariables: EnvironmentVariable[];
@@ -119,7 +159,7 @@ type Project = {
updatedAt: string; updatedAt: string;
organization: Organization; organization: Organization;
icon: string; icon: string;
subDomain: string; baseDomains?: string[] | null;
}; };
type GetProjectMembersResponse = { type GetProjectMembersResponse = {
projectMembers: ProjectMember[]; projectMembers: ProjectMember[];
@@ -185,6 +225,9 @@ type RemoveEnvironmentVariableResponse = {
type UpdateDeploymentToProdResponse = { type UpdateDeploymentToProdResponse = {
updateDeploymentToProd: boolean; updateDeploymentToProd: boolean;
}; };
type AddProjectFromTemplateResponse = {
addProjectFromTemplate: Project;
};
type AddProjectResponse = { type AddProjectResponse = {
addProject: Project; addProject: Project;
}; };
@@ -200,6 +243,13 @@ type DeleteProjectResponse = {
type DeleteDomainResponse = { type DeleteDomainResponse = {
deleteDomain: boolean; deleteDomain: boolean;
}; };
type AddProjectFromTemplateInput = {
templateOwner: string;
templateRepo: string;
owner: string;
name: string;
isPrivate: boolean;
};
type AddProjectInput = { type AddProjectInput = {
name: string; name: string;
repository: string; repository: string;
@@ -245,6 +295,10 @@ type AuthenticateGitHubResponse = {
type UnauthenticateGitHubResponse = { type UnauthenticateGitHubResponse = {
unauthenticateGitHub: boolean; unauthenticateGitHub: boolean;
}; };
type AuctionData = {
maxPrice: string;
numProviders: number;
};
interface GraphQLConfig { interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@@ -267,7 +321,8 @@ declare class GQLClient {
updateEnvironmentVariable(environmentVariableId: string, data: UpdateEnvironmentVariableInput): Promise<UpdateEnvironmentVariableResponse>; updateEnvironmentVariable(environmentVariableId: string, data: UpdateEnvironmentVariableInput): Promise<UpdateEnvironmentVariableResponse>;
removeEnvironmentVariable(environmentVariableId: string): Promise<RemoveEnvironmentVariableResponse>; removeEnvironmentVariable(environmentVariableId: string): Promise<RemoveEnvironmentVariableResponse>;
updateDeploymentToProd(deploymentId: string): Promise<UpdateDeploymentToProdResponse>; updateDeploymentToProd(deploymentId: string): Promise<UpdateDeploymentToProdResponse>;
addProject(organizationSlug: string, data: AddProjectInput): Promise<AddProjectResponse>; addProjectFromTemplate(organizationSlug: string, data: AddProjectFromTemplateInput, lrn?: string, auctionData?: AuctionData): Promise<AddProjectFromTemplateResponse>;
addProject(organizationSlug: string, data: AddProjectInput, lrn?: string, auctionData?: AuctionData): Promise<AddProjectResponse>;
updateProject(projectId: string, data: UpdateProjectInput): Promise<UpdateProjectResponse>; updateProject(projectId: string, data: UpdateProjectInput): Promise<UpdateProjectResponse>;
updateDomain(domainId: string, data: UpdateDomainInput): Promise<UpdateDomainResponse>; updateDomain(domainId: string, data: UpdateDomainInput): Promise<UpdateDomainResponse>;
redeployToProd(deploymentId: string): Promise<RedeployToProdResponse>; redeployToProd(deploymentId: string): Promise<RedeployToProdResponse>;
@@ -279,6 +334,7 @@ declare class GQLClient {
getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>; getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>;
authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>; authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>;
unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>; unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>;
getAuctionData(auctionId: string): Promise<Auction>;
} }
export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User }; export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type Auction, type AuctionData, AuctionStatus, type AuthenticateGitHubResponse, type Bid, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User };
+59 -3
View File
@@ -18,6 +18,42 @@ declare enum DeploymentStatus {
Error = "Error", Error = "Error",
Deleting = "Deleting" Deleting = "Deleting"
} }
declare enum AuctionStatus {
AuctionStatusCommitPhase = "commit",
AuctionStatusRevealPhase = "reveal",
AuctionStatusExpired = "expired",
AuctionStatusCompleted = "completed"
}
type Bid = {
auctionId: string;
bidderAddress: string;
status: string;
commitHash: string;
commitTime?: Date;
commitFee?: string;
revealTime?: Date;
revealFee?: string;
bidAmount?: string;
};
type Auction = {
id: string;
kind: string;
status: string;
ownerAddress: string;
createTime?: Date;
commitsEndTime?: Date;
revealsEndTime?: Date;
commitFee?: string;
revealFee?: string;
minimumBid?: string;
winnerAddresses: string[];
winnerBids?: string[];
winnerPrice?: string;
maxPrice?: string;
numProviders: number;
fundsReleased: boolean;
bids: Bid[];
};
declare enum DomainStatus { declare enum DomainStatus {
Live = "Live", Live = "Live",
Pending = "Pending" Pending = "Pending"
@@ -55,8 +91,10 @@ type Deployment = {
commitHash: string; commitHash: string;
commitMessage: string; commitMessage: string;
url?: string; url?: string;
deployerLrn: string;
environment: Environment; environment: Environment;
isCurrent: boolean; isCurrent: boolean;
baseDomain?: string;
status: DeploymentStatus; status: DeploymentStatus;
createdBy: User; createdBy: User;
createdAt: string; createdAt: string;
@@ -112,6 +150,8 @@ type Project = {
description: string; description: string;
template: string; template: string;
framework: string; framework: string;
deployerLrns: string[];
auctionId: string;
webhooks: string[]; webhooks: string[];
members: ProjectMember[]; members: ProjectMember[];
environmentVariables: EnvironmentVariable[]; environmentVariables: EnvironmentVariable[];
@@ -119,7 +159,7 @@ type Project = {
updatedAt: string; updatedAt: string;
organization: Organization; organization: Organization;
icon: string; icon: string;
subDomain: string; baseDomains?: string[] | null;
}; };
type GetProjectMembersResponse = { type GetProjectMembersResponse = {
projectMembers: ProjectMember[]; projectMembers: ProjectMember[];
@@ -185,6 +225,9 @@ type RemoveEnvironmentVariableResponse = {
type UpdateDeploymentToProdResponse = { type UpdateDeploymentToProdResponse = {
updateDeploymentToProd: boolean; updateDeploymentToProd: boolean;
}; };
type AddProjectFromTemplateResponse = {
addProjectFromTemplate: Project;
};
type AddProjectResponse = { type AddProjectResponse = {
addProject: Project; addProject: Project;
}; };
@@ -200,6 +243,13 @@ type DeleteProjectResponse = {
type DeleteDomainResponse = { type DeleteDomainResponse = {
deleteDomain: boolean; deleteDomain: boolean;
}; };
type AddProjectFromTemplateInput = {
templateOwner: string;
templateRepo: string;
owner: string;
name: string;
isPrivate: boolean;
};
type AddProjectInput = { type AddProjectInput = {
name: string; name: string;
repository: string; repository: string;
@@ -245,6 +295,10 @@ type AuthenticateGitHubResponse = {
type UnauthenticateGitHubResponse = { type UnauthenticateGitHubResponse = {
unauthenticateGitHub: boolean; unauthenticateGitHub: boolean;
}; };
type AuctionData = {
maxPrice: string;
numProviders: number;
};
interface GraphQLConfig { interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@@ -267,7 +321,8 @@ declare class GQLClient {
updateEnvironmentVariable(environmentVariableId: string, data: UpdateEnvironmentVariableInput): Promise<UpdateEnvironmentVariableResponse>; updateEnvironmentVariable(environmentVariableId: string, data: UpdateEnvironmentVariableInput): Promise<UpdateEnvironmentVariableResponse>;
removeEnvironmentVariable(environmentVariableId: string): Promise<RemoveEnvironmentVariableResponse>; removeEnvironmentVariable(environmentVariableId: string): Promise<RemoveEnvironmentVariableResponse>;
updateDeploymentToProd(deploymentId: string): Promise<UpdateDeploymentToProdResponse>; updateDeploymentToProd(deploymentId: string): Promise<UpdateDeploymentToProdResponse>;
addProject(organizationSlug: string, data: AddProjectInput): Promise<AddProjectResponse>; addProjectFromTemplate(organizationSlug: string, data: AddProjectFromTemplateInput, lrn?: string, auctionData?: AuctionData): Promise<AddProjectFromTemplateResponse>;
addProject(organizationSlug: string, data: AddProjectInput, lrn?: string, auctionData?: AuctionData): Promise<AddProjectResponse>;
updateProject(projectId: string, data: UpdateProjectInput): Promise<UpdateProjectResponse>; updateProject(projectId: string, data: UpdateProjectInput): Promise<UpdateProjectResponse>;
updateDomain(domainId: string, data: UpdateDomainInput): Promise<UpdateDomainResponse>; updateDomain(domainId: string, data: UpdateDomainInput): Promise<UpdateDomainResponse>;
redeployToProd(deploymentId: string): Promise<RedeployToProdResponse>; redeployToProd(deploymentId: string): Promise<RedeployToProdResponse>;
@@ -279,6 +334,7 @@ declare class GQLClient {
getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>; getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>;
authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>; authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>;
unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>; unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>;
getAuctionData(auctionId: string): Promise<Auction>;
} }
export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User }; export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type Auction, type AuctionData, AuctionStatus, type AuthenticateGitHubResponse, type Bid, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User };
+184 -60
View File
@@ -40,6 +40,7 @@ var __async = (__this, __arguments, generator) => {
// src/index.ts // src/index.ts
var src_exports = {}; var src_exports = {};
__export(src_exports, { __export(src_exports, {
AuctionStatus: () => AuctionStatus,
DeploymentStatus: () => DeploymentStatus, DeploymentStatus: () => DeploymentStatus,
DomainStatus: () => DomainStatus, DomainStatus: () => DomainStatus,
Environment: () => Environment, Environment: () => Environment,
@@ -76,11 +77,13 @@ query ($projectId: String!) {
template template
updatedAt updatedAt
prodBranch prodBranch
auctionId
deployerLrns
framework framework
repository repository
webhooks webhooks
icon icon
subDomain baseDomains
organization { organization {
id id
name name
@@ -94,6 +97,7 @@ query ($projectId: String!) {
id id
branch branch
isCurrent isCurrent
baseDomain
status status
updatedAt updatedAt
commitHash commitHash
@@ -123,16 +127,19 @@ query ($organizationSlug: String!) {
createdAt createdAt
description description
framework framework
auctionId
deployerLrns
prodBranch prodBranch
webhooks webhooks
repository repository
updatedAt updatedAt
icon icon
subDomain baseDomains
deployments { deployments {
id id
branch branch
isCurrent isCurrent
baseDomain
status status
updatedAt updatedAt
commitHash commitHash
@@ -178,8 +185,10 @@ query ($projectId: String!) {
commitHash commitHash
commitMessage commitMessage
url url
deployerLrn
environment environment
isCurrent isCurrent
baseDomain
status status
createdAt createdAt
updatedAt updatedAt
@@ -230,6 +239,8 @@ query ($searchText: String!) {
createdAt createdAt
description description
framework framework
auctionId
deployerLrns
prodBranch prodBranch
webhooks webhooks
updatedAt updatedAt
@@ -263,97 +274,175 @@ query ($projectId: String!, $filter: FilterDomainsInput) {
} }
} }
`; `;
var getAuctionData = import_client.gql`
query ($auctionId: String!) {
getAuctionData(auctionId: $auctionId){
id
kind
status
ownerAddress
createTime
commitsEndTime
revealsEndTime
commitFee {
type
quantity
}
revealFee {
type
quantity
}
minimumBid {
type
quantity
}
winnerAddresses
winnerBids {
type
quantity
}
winnerPrice {
type
quantity
}
maxPrice {
type
quantity
}
numProviders
fundsReleased
bids {
bidderAddress
status
commitHash
commitTime
revealTime
commitFee {
type
quantity
}
revealFee {
type
quantity
}
bidAmount {
type
quantity
}
}
}
}
`;
// src/mutations.ts // src/mutations.ts
var import_client2 = require("@apollo/client"); var import_client2 = require("@apollo/client");
var removeProjectMember = import_client2.gql` var removeProjectMember = import_client2.gql`
mutation ($projectMemberId: String!) { mutation ($projectMemberId: String!) {
removeProjectMember(projectMemberId: $projectMemberId) removeProjectMember(projectMemberId: $projectMemberId)
} }
`; `;
var updateProjectMember = import_client2.gql` var updateProjectMember = import_client2.gql`
mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) { mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) {
updateProjectMember(projectMemberId: $projectMemberId, data: $data) updateProjectMember(projectMemberId: $projectMemberId, data: $data)
} }
`; `;
var addProjectMember = import_client2.gql` var addProjectMember = import_client2.gql`
mutation ($projectId: String!, $data: AddProjectMemberInput) { mutation ($projectId: String!, $data: AddProjectMemberInput) {
addProjectMember(projectId: $projectId, data: $data) addProjectMember(projectId: $projectId, data: $data)
} }
`; `;
var addEnvironmentVariables = import_client2.gql` var addEnvironmentVariables = import_client2.gql`
mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) { mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) {
addEnvironmentVariables(projectId: $projectId, data: $data) addEnvironmentVariables(projectId: $projectId, data: $data)
} }
`; `;
var updateEnvironmentVariable = import_client2.gql` var updateEnvironmentVariable = import_client2.gql`
mutation ($environmentVariableId: String!, $data: UpdateEnvironmentVariableInput!) { mutation (
updateEnvironmentVariable(environmentVariableId: $environmentVariableId, data: $data) $environmentVariableId: String!
} $data: UpdateEnvironmentVariableInput!
) {
updateEnvironmentVariable(
environmentVariableId: $environmentVariableId
data: $data
)
}
`; `;
var removeEnvironmentVariable = import_client2.gql` var removeEnvironmentVariable = import_client2.gql`
mutation ($environmentVariableId: String!) { mutation ($environmentVariableId: String!) {
removeEnvironmentVariable(environmentVariableId: $environmentVariableId) removeEnvironmentVariable(environmentVariableId: $environmentVariableId)
} }
`; `;
var updateDeploymentToProd = import_client2.gql` var updateDeploymentToProd = import_client2.gql`
mutation ($deploymentId: String!) { mutation ($deploymentId: String!) {
updateDeploymentToProd(deploymentId: $deploymentId) updateDeploymentToProd(deploymentId: $deploymentId)
} }
`;
var addProjectFromTemplate = import_client2.gql`
mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput, $lrn: String, $auctionData: AuctionData) {
addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionData: $auctionData) {
id
}
}
`; `;
var addProject = import_client2.gql` var addProject = import_client2.gql`
mutation ($organizationSlug: String!, $data: AddProjectInput) { mutation ($organizationSlug: String!, $data: AddProjectInput!, $lrn: String, $auctionData: AuctionData) {
addProject(organizationSlug: $organizationSlug, data: $data) { addProject(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionData: $auctionData) {
id id
}
} }
}`; `;
var updateProjectMutation = import_client2.gql` var updateProjectMutation = import_client2.gql`
mutation ($projectId: String!, $data: UpdateProjectInput) { mutation ($projectId: String!, $data: UpdateProjectInput) {
updateProject(projectId: $projectId, data: $data) updateProject(projectId: $projectId, data: $data)
}`; }
`;
var updateDomainMutation = import_client2.gql` var updateDomainMutation = import_client2.gql`
mutation ($domainId: String!, $data: UpdateDomainInput!) { mutation ($domainId: String!, $data: UpdateDomainInput!) {
updateDomain(domainId: $domainId, data: $data) updateDomain(domainId: $domainId, data: $data)
}`; }
`;
var redeployToProd = import_client2.gql` var redeployToProd = import_client2.gql`
mutation ($deploymentId: String!) { mutation ($deploymentId: String!) {
redeployToProd(deploymentId: $deploymentId) redeployToProd(deploymentId: $deploymentId)
} }
`; `;
var deleteProject = import_client2.gql` var deleteProject = import_client2.gql`
mutation ($projectId: String!) { mutation ($projectId: String!) {
deleteProject(projectId: $projectId) deleteProject(projectId: $projectId)
} }
`; `;
var deleteDomain = import_client2.gql` var deleteDomain = import_client2.gql`
mutation ($domainId: String!) { mutation ($domainId: String!) {
deleteDomain(domainId: $domainId) deleteDomain(domainId: $domainId)
}`; }
`;
var rollbackDeployment = import_client2.gql` var rollbackDeployment = import_client2.gql`
mutation ($projectId: String! ,$deploymentId: String!) { mutation ($projectId: String!, $deploymentId: String!) {
rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId) rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId)
} }
`; `;
var deleteDeployment = import_client2.gql` var deleteDeployment = import_client2.gql`
mutation ($deploymentId: String!) { mutation ($deploymentId: String!) {
deleteDeployment(deploymentId: $deploymentId) deleteDeployment(deploymentId: $deploymentId)
} }
`; `;
var addDomain = import_client2.gql` var addDomain = import_client2.gql`
mutation ($projectId: String!, $data: AddDomainInput!) { mutation ($projectId: String!, $data: AddDomainInput!) {
addDomain(projectId: $projectId, data: $data) addDomain(projectId: $projectId, data: $data)
} }
`; `;
var authenticateGitHub = import_client2.gql` var authenticateGitHub = import_client2.gql`
mutation ($code: String!) { mutation ($code: String!) {
authenticateGitHub(code: $code) { authenticateGitHub(code: $code) {
token token
}
} }
}`; `;
var unauthenticateGitHub = import_client2.gql` var unauthenticateGitHub = import_client2.gql`
mutation { mutation {
unauthenticateGitHub unauthenticateGitHub
}`; }
`;
// src/client.ts // src/client.ts
var defaultOptions = { var defaultOptions = {
@@ -538,13 +627,29 @@ var GQLClient = class {
return data; return data;
}); });
} }
addProject(organizationSlug, data) { addProjectFromTemplate(organizationSlug, data, lrn, auctionData) {
return __async(this, null, function* () {
const result = yield this.client.mutate({
mutation: addProjectFromTemplate,
variables: {
organizationSlug,
data,
lrn,
auctionData
}
});
return result.data;
});
}
addProject(organizationSlug, data, lrn, auctionData) {
return __async(this, null, function* () { return __async(this, null, function* () {
const result = yield this.client.mutate({ const result = yield this.client.mutate({
mutation: addProject, mutation: addProject,
variables: { variables: {
organizationSlug, organizationSlug,
data data,
lrn,
auctionData
} }
}); });
return result.data; return result.data;
@@ -673,6 +778,17 @@ var GQLClient = class {
return data; return data;
}); });
} }
getAuctionData(auctionId) {
return __async(this, null, function* () {
const { data } = yield this.client.query({
query: getAuctionData,
variables: {
auctionId
}
});
return data.getAuctionData;
});
}
}; };
// src/types.ts // src/types.ts
@@ -700,6 +816,13 @@ var DeploymentStatus = /* @__PURE__ */ ((DeploymentStatus2) => {
DeploymentStatus2["Deleting"] = "Deleting"; DeploymentStatus2["Deleting"] = "Deleting";
return DeploymentStatus2; return DeploymentStatus2;
})(DeploymentStatus || {}); })(DeploymentStatus || {});
var AuctionStatus = /* @__PURE__ */ ((AuctionStatus2) => {
AuctionStatus2["AuctionStatusCommitPhase"] = "commit";
AuctionStatus2["AuctionStatusRevealPhase"] = "reveal";
AuctionStatus2["AuctionStatusExpired"] = "expired";
AuctionStatus2["AuctionStatusCompleted"] = "completed";
return AuctionStatus2;
})(AuctionStatus || {});
var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => { var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
DomainStatus2["Live"] = "Live"; DomainStatus2["Live"] = "Live";
DomainStatus2["Pending"] = "Pending"; DomainStatus2["Pending"] = "Pending";
@@ -707,6 +830,7 @@ var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
})(DomainStatus || {}); })(DomainStatus || {});
// Annotate the CommonJS export names for ESM import in node: // Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = { 0 && (module.exports = {
AuctionStatus,
DeploymentStatus, DeploymentStatus,
DomainStatus, DomainStatus,
Environment, Environment,
File diff suppressed because one or more lines are too long
+183 -60
View File
@@ -49,11 +49,13 @@ query ($projectId: String!) {
template template
updatedAt updatedAt
prodBranch prodBranch
auctionId
deployerLrns
framework framework
repository repository
webhooks webhooks
icon icon
subDomain baseDomains
organization { organization {
id id
name name
@@ -67,6 +69,7 @@ query ($projectId: String!) {
id id
branch branch
isCurrent isCurrent
baseDomain
status status
updatedAt updatedAt
commitHash commitHash
@@ -96,16 +99,19 @@ query ($organizationSlug: String!) {
createdAt createdAt
description description
framework framework
auctionId
deployerLrns
prodBranch prodBranch
webhooks webhooks
repository repository
updatedAt updatedAt
icon icon
subDomain baseDomains
deployments { deployments {
id id
branch branch
isCurrent isCurrent
baseDomain
status status
updatedAt updatedAt
commitHash commitHash
@@ -151,8 +157,10 @@ query ($projectId: String!) {
commitHash commitHash
commitMessage commitMessage
url url
deployerLrn
environment environment
isCurrent isCurrent
baseDomain
status status
createdAt createdAt
updatedAt updatedAt
@@ -203,6 +211,8 @@ query ($searchText: String!) {
createdAt createdAt
description description
framework framework
auctionId
deployerLrns
prodBranch prodBranch
webhooks webhooks
updatedAt updatedAt
@@ -236,97 +246,175 @@ query ($projectId: String!, $filter: FilterDomainsInput) {
} }
} }
`; `;
var getAuctionData = gql`
query ($auctionId: String!) {
getAuctionData(auctionId: $auctionId){
id
kind
status
ownerAddress
createTime
commitsEndTime
revealsEndTime
commitFee {
type
quantity
}
revealFee {
type
quantity
}
minimumBid {
type
quantity
}
winnerAddresses
winnerBids {
type
quantity
}
winnerPrice {
type
quantity
}
maxPrice {
type
quantity
}
numProviders
fundsReleased
bids {
bidderAddress
status
commitHash
commitTime
revealTime
commitFee {
type
quantity
}
revealFee {
type
quantity
}
bidAmount {
type
quantity
}
}
}
}
`;
// src/mutations.ts // src/mutations.ts
import { gql as gql2 } from "@apollo/client"; import { gql as gql2 } from "@apollo/client";
var removeProjectMember = gql2` var removeProjectMember = gql2`
mutation ($projectMemberId: String!) { mutation ($projectMemberId: String!) {
removeProjectMember(projectMemberId: $projectMemberId) removeProjectMember(projectMemberId: $projectMemberId)
} }
`; `;
var updateProjectMember = gql2` var updateProjectMember = gql2`
mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) { mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) {
updateProjectMember(projectMemberId: $projectMemberId, data: $data) updateProjectMember(projectMemberId: $projectMemberId, data: $data)
} }
`; `;
var addProjectMember = gql2` var addProjectMember = gql2`
mutation ($projectId: String!, $data: AddProjectMemberInput) { mutation ($projectId: String!, $data: AddProjectMemberInput) {
addProjectMember(projectId: $projectId, data: $data) addProjectMember(projectId: $projectId, data: $data)
} }
`; `;
var addEnvironmentVariables = gql2` var addEnvironmentVariables = gql2`
mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) { mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) {
addEnvironmentVariables(projectId: $projectId, data: $data) addEnvironmentVariables(projectId: $projectId, data: $data)
} }
`; `;
var updateEnvironmentVariable = gql2` var updateEnvironmentVariable = gql2`
mutation ($environmentVariableId: String!, $data: UpdateEnvironmentVariableInput!) { mutation (
updateEnvironmentVariable(environmentVariableId: $environmentVariableId, data: $data) $environmentVariableId: String!
} $data: UpdateEnvironmentVariableInput!
) {
updateEnvironmentVariable(
environmentVariableId: $environmentVariableId
data: $data
)
}
`; `;
var removeEnvironmentVariable = gql2` var removeEnvironmentVariable = gql2`
mutation ($environmentVariableId: String!) { mutation ($environmentVariableId: String!) {
removeEnvironmentVariable(environmentVariableId: $environmentVariableId) removeEnvironmentVariable(environmentVariableId: $environmentVariableId)
} }
`; `;
var updateDeploymentToProd = gql2` var updateDeploymentToProd = gql2`
mutation ($deploymentId: String!) { mutation ($deploymentId: String!) {
updateDeploymentToProd(deploymentId: $deploymentId) updateDeploymentToProd(deploymentId: $deploymentId)
} }
`;
var addProjectFromTemplate = gql2`
mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput, $lrn: String, $auctionData: AuctionData) {
addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionData: $auctionData) {
id
}
}
`; `;
var addProject = gql2` var addProject = gql2`
mutation ($organizationSlug: String!, $data: AddProjectInput) { mutation ($organizationSlug: String!, $data: AddProjectInput!, $lrn: String, $auctionData: AuctionData) {
addProject(organizationSlug: $organizationSlug, data: $data) { addProject(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionData: $auctionData) {
id id
}
} }
}`; `;
var updateProjectMutation = gql2` var updateProjectMutation = gql2`
mutation ($projectId: String!, $data: UpdateProjectInput) { mutation ($projectId: String!, $data: UpdateProjectInput) {
updateProject(projectId: $projectId, data: $data) updateProject(projectId: $projectId, data: $data)
}`; }
`;
var updateDomainMutation = gql2` var updateDomainMutation = gql2`
mutation ($domainId: String!, $data: UpdateDomainInput!) { mutation ($domainId: String!, $data: UpdateDomainInput!) {
updateDomain(domainId: $domainId, data: $data) updateDomain(domainId: $domainId, data: $data)
}`; }
`;
var redeployToProd = gql2` var redeployToProd = gql2`
mutation ($deploymentId: String!) { mutation ($deploymentId: String!) {
redeployToProd(deploymentId: $deploymentId) redeployToProd(deploymentId: $deploymentId)
} }
`; `;
var deleteProject = gql2` var deleteProject = gql2`
mutation ($projectId: String!) { mutation ($projectId: String!) {
deleteProject(projectId: $projectId) deleteProject(projectId: $projectId)
} }
`; `;
var deleteDomain = gql2` var deleteDomain = gql2`
mutation ($domainId: String!) { mutation ($domainId: String!) {
deleteDomain(domainId: $domainId) deleteDomain(domainId: $domainId)
}`; }
`;
var rollbackDeployment = gql2` var rollbackDeployment = gql2`
mutation ($projectId: String! ,$deploymentId: String!) { mutation ($projectId: String!, $deploymentId: String!) {
rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId) rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId)
} }
`; `;
var deleteDeployment = gql2` var deleteDeployment = gql2`
mutation ($deploymentId: String!) { mutation ($deploymentId: String!) {
deleteDeployment(deploymentId: $deploymentId) deleteDeployment(deploymentId: $deploymentId)
} }
`; `;
var addDomain = gql2` var addDomain = gql2`
mutation ($projectId: String!, $data: AddDomainInput!) { mutation ($projectId: String!, $data: AddDomainInput!) {
addDomain(projectId: $projectId, data: $data) addDomain(projectId: $projectId, data: $data)
} }
`; `;
var authenticateGitHub = gql2` var authenticateGitHub = gql2`
mutation ($code: String!) { mutation ($code: String!) {
authenticateGitHub(code: $code) { authenticateGitHub(code: $code) {
token token
}
} }
}`; `;
var unauthenticateGitHub = gql2` var unauthenticateGitHub = gql2`
mutation { mutation {
unauthenticateGitHub unauthenticateGitHub
}`; }
`;
// src/client.ts // src/client.ts
var defaultOptions = { var defaultOptions = {
@@ -511,13 +599,29 @@ var GQLClient = class {
return data; return data;
}); });
} }
addProject(organizationSlug, data) { addProjectFromTemplate(organizationSlug, data, lrn, auctionData) {
return __async(this, null, function* () {
const result = yield this.client.mutate({
mutation: addProjectFromTemplate,
variables: {
organizationSlug,
data,
lrn,
auctionData
}
});
return result.data;
});
}
addProject(organizationSlug, data, lrn, auctionData) {
return __async(this, null, function* () { return __async(this, null, function* () {
const result = yield this.client.mutate({ const result = yield this.client.mutate({
mutation: addProject, mutation: addProject,
variables: { variables: {
organizationSlug, organizationSlug,
data data,
lrn,
auctionData
} }
}); });
return result.data; return result.data;
@@ -646,6 +750,17 @@ var GQLClient = class {
return data; return data;
}); });
} }
getAuctionData(auctionId) {
return __async(this, null, function* () {
const { data } = yield this.client.query({
query: getAuctionData,
variables: {
auctionId
}
});
return data.getAuctionData;
});
}
}; };
// src/types.ts // src/types.ts
@@ -673,12 +788,20 @@ var DeploymentStatus = /* @__PURE__ */ ((DeploymentStatus2) => {
DeploymentStatus2["Deleting"] = "Deleting"; DeploymentStatus2["Deleting"] = "Deleting";
return DeploymentStatus2; return DeploymentStatus2;
})(DeploymentStatus || {}); })(DeploymentStatus || {});
var AuctionStatus = /* @__PURE__ */ ((AuctionStatus2) => {
AuctionStatus2["AuctionStatusCommitPhase"] = "commit";
AuctionStatus2["AuctionStatusRevealPhase"] = "reveal";
AuctionStatus2["AuctionStatusExpired"] = "expired";
AuctionStatus2["AuctionStatusCompleted"] = "completed";
return AuctionStatus2;
})(AuctionStatus || {});
var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => { var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
DomainStatus2["Live"] = "Live"; DomainStatus2["Live"] = "Live";
DomainStatus2["Pending"] = "Pending"; DomainStatus2["Pending"] = "Pending";
return DomainStatus2; return DomainStatus2;
})(DomainStatus || {}); })(DomainStatus || {});
export { export {
AuctionStatus,
DeploymentStatus, DeploymentStatus,
DomainStatus, DomainStatus,
Environment, Environment,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,4 +16,4 @@
"dependencies": { "dependencies": {
"@apollo/client": "^3.8.9" "@apollo/client": "^3.8.9"
} }
} }
+35 -1
View File
@@ -228,15 +228,38 @@ export class GQLClient {
return data; return data;
} }
async addProjectFromTemplate(
organizationSlug: string,
data: types.AddProjectFromTemplateInput,
lrn?: string,
auctionData?: types.AuctionData,
): Promise<types.AddProjectFromTemplateResponse> {
const result = await this.client.mutate({
mutation: mutations.addProjectFromTemplate,
variables: {
organizationSlug,
data,
lrn,
auctionData
},
});
return result.data;
}
async addProject( async addProject(
organizationSlug: string, organizationSlug: string,
data: types.AddProjectInput data: types.AddProjectInput,
lrn?: string,
auctionData?: types.AuctionData,
): Promise<types.AddProjectResponse> { ): Promise<types.AddProjectResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addProject, mutation: mutations.addProject,
variables: { variables: {
organizationSlug, organizationSlug,
data, data,
lrn,
auctionData
}, },
}); });
@@ -386,4 +409,15 @@ export class GQLClient {
return data; return data;
} }
async getAuctionData(auctionId: string): Promise<types.Auction> {
const { data } = await this.client.query({
query: queries.getAuctionData,
variables: {
auctionId,
},
});
return data.getAuctionData;
}
} }

Some files were not shown because too many files have changed in this diff Show More