Compare commits

..
Author SHA1 Message Date
Eric Lewis af1137b3c3 chore(deployment): 0.1.8 2024-02-29 00:47:19 -07:00
Eric Lewisandnabarun a7810a34c9 feat: one click deployer (#139)
* feat: one click deployer

This adds a script for deploying whatever is the latest on main, essentially automating the changes specified in the readme.

* fix

* repurpose existing script

* clean up

* fix

* fix

* Update latest record version

---------

Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
2024-02-29 13:11:01 +05:30
Vivian Phung ff9b08e91d Merge pull request #143 from snowball-tools/template-card-redirect-bug
Template card redirect bug
2024-02-29 00:38:12 -07:00
nabarun 94a9bf88a9 Remove check for .env in build script (#144) 2024-02-29 12:51:32 +05:30
Vivian Phung dd5f93050d fix navigate 2024-02-29 00:01:56 -07:00
Vivian Phung 5946755749 fix navigate 2024-02-28 23:52:37 -07:00
Vivian Phung 4532a962d8 fix navigate 2024-02-28 23:44:10 -07:00
Sushan Yadav 6a108c1a1b Merge pull request #137 from snowball-tools/sushan/T-4914-project-overview-layout
Project overview layout
2024-02-29 09:57:12 +05:45
Sushan Yadav 0fb378386a chore: update comment 2024-02-28 20:51:58 +05:45
Sushan Yadav 563059e887 chore: comment fixes - tab issues and hardcoded strings 2024-02-28 20:46:08 +05:45
Sushan Yadav f34eb48d1e feat: add layout 2024-02-28 17:35:02 +05:45
Sushan Yadav acf5aab26f Merge branch 'main' into sushan/T-4914-project-overview-layout 2024-02-28 16:57:32 +05:45
Sushan Yadav 6b6582c287 feat: top section 2024-02-28 16:56:28 +05:45
Sushan Yadav b43ee3b7bb feat: add deplyment infos 2024-02-28 16:39:26 +05:45
Wahyu Kurniawan b1bf47d104 [T-4921: feat] Update project card component (#136)
* 🎨 style: add hover interaction to the card

* ️ feat: make the whole card clickable

* 🎨 style: adjust hovered background for wavy border

* ♻️ refactor: move wavy border class to project card theme

* 🎨 style: add transition when hover

* 📝 docs: add todo to experiment using `Link` componnt
2024-02-28 16:50:55 +07:00
Wahyu KurniawanandZachery Ng 8ee61c0c85 [T-4910: feat] Re-styling dashboard, create project layout, project template card (#135)
* 🎨 style: adjust wavy border and add layout wavy border

* ♻️ refactor: change sidebar to use `nav`

* ♻️ refactor: org slug dashboard layout

* ♻️ refactor: create project layout and restyling it

* ♻️ refactor: remove unused style

* ️ feat: restyling template card

* ️ feat: create template icon component

* ️ feat: use `h2` for layout title

* ️ feat: Add isComingSoon property to templates and handle click event in TemplateCard component

* ♻️ refactor: WavyBorder component and update CreateProjectLayout

* 🎨 style: update button medium size padding

* 🎨 style: update layout shadow and add new shadow for the template card

* ️ feat: add wavy border gradient and line svg assets

* refactor: update wavy border svg

* 🎨 style: adjust template card name and arrow also responsive of the list of template

---------

Co-authored-by: Zachery Ng <zachery.ng@gmail.com>
2024-02-28 16:22:54 +07:00
Andre Hadianto 33aa75e341 Merge pull request #133 from snowball-tools/andrehadianto/T-4906-create-project-connect-github
[T-4906] create project connect GitHub
2024-02-28 15:55:59 +08:00
Sushan Yadav a238610522 Reskin Project Overview - Activity section (#134)
* feat: reskin w/ new design system

* feat: add sepeator calc in comment

* chore: limit commit message to 4 line

* comment fixes

* chore: use Heading component
2024-02-28 13:31:41 +05:45
Zachery Ng bd4a51c830 style: update tertiary button 2024-02-28 15:45:57 +08:00
Andre H 4a87c100d6 🔧 chore: use proper type 2024-02-28 14:20:12 +07:00
Andre H 3493d735b9 🔧 chore: replace bgClass with className 2024-02-28 14:19:02 +07:00
Andre H a324d32ebf 🔧 chore: use Heading 2024-02-28 13:51:35 +07:00
Andre H 4519494be8 ♻️ refactor: cherry pick fomr main 2024-02-28 13:49:11 +07:00
Andre H 319822c8e6 🔧 chore: add shadow to button 2024-02-28 13:46:09 +07:00
Andre H c5273ff530 ♻️ refactor: use IconWithFrame 2024-02-28 13:39:50 +07:00
Andre H 2585418ed6 🔧 chore: implement IconWithFrame common component 2024-02-28 13:39:30 +07:00
Wahyu Kurniawan 62734308fc [T-4907: style] Re-styling home page & sidebar component (#132)
* ️ feat: create heading component

* ♻️ refactor: move sidebar inside shared components

* ️ feat: add polymorphic prop type for heading component

* 🎨 style: re-styling project list page

* ♻️ refactor: remove `.env`

* 🎨 style: set default font weight to normal for heading component
2024-02-28 12:50:30 +07:00
Andre H 89306ddcc7 🔧 chore: add toast 2024-02-28 11:19:09 +07:00
Andre H 3b67396c43 ️ feat: reskin connect github 2024-02-28 10:55:10 +07:00
Andre H 3535bd3a58 🔧 chore: add GitTeaIcon 2024-02-28 10:53:05 +07:00
Wahyu Kurniawan 282001c317 [T-4903: chore] Add inter display font (#127)
* 🔧 chore: add `InterDisplay` font families

* 🎨 style: add font `InterDisplay` to tailwind

* 🔧 chore: update `yarn.lock`

* ♻️ refactor: change `InterDisplay` to `inter Display` and `inter-display` to `display
2024-02-28 08:47:43 +07:00
Eric Lewis 2f62d086de Merge pull request #131 from snowball-tools/ericlewis/deploy-2
chore(deployment): 1.1.3
2024-02-27 12:55:54 -05:00
Eric Lewis 4c544e8d15 chore: readme 2024-02-27 12:30:35 -05:00
Eric Lewis a02ee13f9d chore: deploy readme 2024-02-27 12:29:27 -05:00
Eric Lewis e32a51a771 chore(deployment): 1.1.3 2024-02-27 12:25:20 -05:00
Eric Lewis 513ca69d01 Merge pull request #130 from snowball-tools/ericlewis/pretty-login
feat: pretty login
2024-02-27 12:08:40 -05:00
Eric Lewis e34b8d6a2e Merge pull request #129 from snowball-tools/ericlewis/deploy
chore: create new deployment
2024-02-27 12:07:43 -05:00
Eric Lewis f4d5b77784 fix deploy 2024-02-27 11:56:22 -05:00
Eric Lewis 399b80ce3b chore: create new deployment 2024-02-27 11:30:39 -05:00
Eric Lewis fdaec2da94 feat: pretty login 2024-02-27 11:13:14 -05:00
Vivian Phung 13fc92bf0e Merge pull request #128 from snowball-tools/ericlewis/prettify-select 2024-02-27 10:44:59 -05:00
Eric Lewis 8a8ee1d6ae fix centering 2024-02-27 10:40:54 -05:00
Vivian Phung 5680961964 Merge pull request #122 from snowball-tools/repo 2024-02-27 09:18:20 -05:00
Vivian Phung aa0ca8dd21 Merge pull request #121 from snowball-tools/search-area-plus 2024-02-27 09:18:03 -05:00
Wahyu Kurniawan 8280f4c7f7 [T-4902: feat] Update project card (#126)
* ️ feat: create wavy border component

* ️ feat: create project card component

* ️ feat: add new size in avatar

* 🎨 style: add default border radius when the shape is default on the button

* ️ feat: create some icons for project card component

* 🔧 chore: install and setup jetbrains mono font family

* ️ feat: create `getInitials` util function

* 🎨 style: add jetbrains mono font to the tailwind config

* ️ feat: add warning error icon

* ♻️ refactor: change `jetbrains-mono` to `mono`
2024-02-27 21:16:38 +07:00
nabarun ab72bbb03f Update template repo links (#124) 2024-02-27 14:16:36 +05:30
nabarun cc4e5fd627 Add deployer package for frontend app (#123)
* Add script and records for deploying frontend app

* Update readme with steps for new deployment

* Add troubleshooting section

* Move config env update

* Add step to commit updated records

* Bump record version
2024-02-27 11:52:05 +05:30
Vivian Phung 195471b888 Merge branch 'main' into repo 2024-02-27 00:07:22 -05:00
Wahyu Kurniawan c731dd308c [T-4840: feat] Dropdown/select component (#108)
* 🎨 style: adjust z index of the date picker popover

* 🎨 style: add new spacing and rename box shadow from calendar to dropdown

* 🐛 fix: console error becasue button inside button

* ♻️ refactor: rename shadow calendar to shador dropdown on calendar component

* 🚀 perf: remove vscode settings inside `packages/frontend`

* ️ feat: create check radio icon and chevron down icon component

* 🔧 chore: install `downshift`

* ️ feat: create select component

* 🎨 style: adjust the popover position based on the user screen

* ️ feat: separate select item to be a component

* ️ feat: separate select value to be a component

* ♻️ refactor: adjust style and refactor to a new component

* ️ feat: create a type for merge two interface but keep the last value

* 🐛 fix: forward ref the component to fix console error

* ️ feat: add `hideValues` prop to hide the values when on multiple

* 🐛 fix: no result not showing

* ️ feat: make the select to be controller component

* ♻️ refactor: remove console log

* ♻️ refactor: update pr review
2024-02-27 12:05:16 +07:00
Vivian Phung 8ce531274d center items in nav bar of git repos 2024-02-27 00:00:52 -05:00
Vivian Phung 6c38094093 github icon in search repo 2024-02-26 23:56:20 -05:00
Vivian Phung 95285d30dd private repo + repo 2024-02-26 23:50:42 -05:00
Vivian Phung 61c8846400 org slug 2024-02-26 23:24:19 -05:00
Eric Lewis d73c3c7daf fix: select issue 2024-02-26 22:57:05 -05:00
Vivian Phung effa0bbf14 Merge pull request #118 from snowball-tools/ericlewis/prettify-select
feat: prettify select in sidebar
2024-02-26 22:51:15 -05:00
Eric Lewis 10a875d6e8 feat: prettify select in sidebar 2024-02-26 22:46:42 -05:00
Vivian Phung 284eb1403a Merge pull request #116 from snowball-tools/ericlewis/search-bar
feat: prettier search area
2024-02-26 22:35:46 -05:00
Eric Lewis 538e89e3c9 Merge pull request #117 from snowball-tools/ericlewis/bg
feat: background depth
2024-02-26 22:35:37 -05:00
Eric Lewis b88752da69 softer line 2024-02-26 22:34:42 -05:00
Eric Lewis 48c569c371 feat: background depth 2024-02-26 22:31:03 -05:00
Eric Lewis 549dc5af10 feat: prettier search area 2024-02-26 22:24:15 -05:00
Vivian Phung 012beaad57 metadata (#112) 2024-02-26 21:10:04 -05:00
Vivian Phung 81f27442fb github page (#114)
* ss

* comment out connect account tab model for now
2024-02-26 21:08:39 -05:00
Eric Lewis bdb948f69e Merge pull request #113 from snowball-tools/ericlewis/sidebar-tabs
feat: side bar tabs + icons
2024-02-26 18:58:14 -05:00
Eric Lewis 2442e92395 feat: side bar tabs + icons 2024-02-26 18:35:45 -05:00
Vivian Phung dcf271a01b Build webapp script fix (#111)
* build-webapp-script-fix

* build-webapp-script-fix
2024-02-26 18:06:24 -05:00
Vivian Phung 48fd953c60 Merge pull request #107 from snowball-tools/designsystem
merge design system into main
2024-02-26 17:50:01 -05:00
Vivian Phung 5eff273abb Merge branch 'designsystem' of github.com:snowball-tools/snowballtools-base into designsystem 2024-02-26 17:46:28 -05:00
Vivian Phung f9af066333 merge conflict and env.example 2024-02-26 17:45:51 -05:00
Eric Lewis 04460a24ac Merge branch 'designsystem' of https://github.com/snowball-tools/snowballtools-base into designsystem 2024-02-26 17:45:38 -05:00
Eric Lewis ccc4589033 partial sidebar style 2024-02-26 17:45:04 -05:00
Vivian Phung cf2f9a88e1 Merge branch 'main' of github.com:snowball-tools/snowballtools-base into designsystem 2024-02-26 17:44:42 -05:00
Vivian Phung 50749055bf ignore backend env file and readme 2024-02-26 17:33:44 -05:00
Vivian Phung ef7c673698 ignore environments backend folder 2024-02-26 17:22:47 -05:00
Vivian Phung 7f11d8818d update eslint with react version 2024-02-26 17:08:58 -05:00
Vivian Phung ece9a2ecec update readme 2024-02-26 17:08:22 -05:00
Eric Lewis cb614c8d8f create project uses button 2024-02-26 17:02:53 -05:00
Vivian Phung dbb928cd2f cleanup gitignore example 2024-02-26 17:02:42 -05:00
Vivian Phung cfd129fb96 frontend ignore .env and change image 2024-02-26 16:57:49 -05:00
Eric Lewis efc10d3d7e fix walletconnect 2024-02-26 15:11:18 -05:00
Eric Lewis 042aff2f87 temp (?) fix build
revert this if needed
2024-02-26 11:56:51 -05:00
nabarunandneeraj b48afed24f Add project create template for image upload PWA (#109)
* Use image upload PWA template

* Set template URL from env

* Handle review changes

* Update readme and build app script

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-26 17:54:35 +05:30
Vivian Phung 0dfecd024d merge 2024-02-24 00:01:06 -05:00
telackeyandVivian Phung 6126c03eee Restore https://github.com/snowball-tools/snowballtools-base/pull/103 (#106)
* Update logo.svg

* Update logo.svg

* Delete packages/frontend/public/favicon.ico

* Delete packages/frontend/public/logo192.png

* Delete packages/frontend/public/logo512.png

* Add files via upload

* Add files via upload

* Add files via upload

* Update site.webmanifest

* Update index.html

---------

Co-authored-by: Vivian Phung <dev+github@vivianphung.com>
2024-02-23 16:09:45 -06:00
telackeyandVivian Phung 35be62b28e readme formatting (#105)
Co-authored-by: Vivian Phung <dev+github@vivianphung.com>
2024-02-23 15:57:34 -06:00
Andre Hadianto 9db87e6be3 Merge pull request #98 from snowball-tools/andrehadianto/T-4868-tags
[T-4868] Tags component
2024-02-24 03:41:35 +08:00
nabarunandneeraj 353fd0f621 Set authority for record name from backend config (#104)
* Fix format of eth address to display

* Set authority for record name from backend config

* Add functionality to open repo button

* Use commit author image and commit url in activity card

* Handle review changes

* Fix commit author as null

* Add timeout to move to next page on project creation

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-23 14:47:29 +05:30
Andre H 5a5756bcf2 🔧 chore: add cloneIcon to dep list 2024-02-23 10:47:50 +07:00
Andre Hadianto 22233d95f8 Merge branch 'andrehadianto/design-system-components' into andrehadianto/T-4868-tags 2024-02-23 11:44:29 +08:00
Wahyu Kurniawan 12fba5ee0c [T-4845: feat] Date picker component (#97)
* ️ feat: create calendar icon for date picker

* ️ feat: create date picker component

* 📝 docs: add date picker to the example page

* 🔧 chore: install `@radix-ui/react-popover`
2024-02-23 10:22:33 +07:00
Wahyu Kurniawan 9b6c777f5f [T-4865: feat] Segmented controls component (#91)
* ️ feat: create segmented controls component

* 📝 docs: add segmented controls component to example page

* ♻️ refactor: put the icon size to icon theme

* 🐛 fix: remove `value` from `useCallback` dependency
2024-02-23 10:20:08 +07:00
Andre Hadianto 3718e260f5 Merge pull request #95 from snowball-tools/andrehadianto/T-4869-toast
[T-4869] implement toast component
2024-02-22 23:48:45 +08:00
Andre H 05e1ac6b02 ♻️ refactor: change tags to tag for rencer page 2024-02-22 22:48:31 +07:00
Andre H 55848b1dd1 ♻️ refactor: rename tags to tag 2024-02-22 22:46:10 +07:00
Andre Hadianto 42bf1534fb Merge branch 'andrehadianto/design-system-components' into andrehadianto/T-4869-toast 2024-02-22 23:42:47 +08:00
nabarunandneeraj 9acb9daacc Update README after implementation of authentication (#101)
* Update README

* Add tooltip to display ethereum address

* Update README for production deployment

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-22 19:24:06 +05:30
nabarun f7c45ca045 Unset cookie domain to localhost by default (#100) 2024-02-22 17:52:49 +05:30
nabarunandneeraj ef0eac8293 Implement authentication with SIWE (#99)
* Create web3 modal provider with SIWE

* Add auth router to handle SIWE authentication

* Use axios instance to make request

* Add button for SIWE authentication

* Add changes to access session in web-app GQL requests

* Add auth check in GQL context and load/create user

* Use authenticated user from context

* Redirect to sign in page if unauthenticated and logout button

* Change sign-in route to login

* Get project domain from config file

* Set user ethAddress column as unique

* Use formatted user name

* Get session secret and origin url from config file

* Add unique constraint for eth address

* Get secure and samesite from origin url

* Get wallet connect id and backend url from env file

* Format user email in member tab panel

* Add backend config isProduction to set trust proxy

* Use only one server url config

* Add tool tip for displaying email

* Add trustProxy and domain in server.session config

* Add SERVER_GQL_PATH constant in frontend

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-22 17:26:26 +05:30
Wahyu Kurniawan 7d1810ebd9 [T-4866: feat] Switch component (#92)
* ️ feat: create switch component

* 📝 docs: add switch to the example page

* 🔧 chore: install `@radix-ui/react-switch`

* 🎨 style: add inset shadow

* 🎨 style: addjust input outline when error and focus
2024-02-22 17:42:13 +07:00
Andre H 0e7343da6f 🔧 chore: gqlclientprovider import 2024-02-22 17:11:03 +07:00
Andre H 8952393b31 🔧 chore: remove not working swipe down gesture 2024-02-22 17:08:05 +07:00
Andre H 32918b7930 🔧 chore: index import 2024-02-22 17:07:15 +07:00
Andre H 596889b5e2 🔧 chore: add render tags component 2024-02-22 17:01:58 +07:00
Andre H fb932eeb04 ️ feat: implement tags component 2024-02-22 17:01:36 +07:00
Andre H 200ea3ca7b 🐛 fix: aria-hidden naming on input 2024-02-22 16:14:27 +07:00
Andre H ba87c6f22a 🔧 chore: replace react-hot-toast with custom toast 2024-02-22 16:11:37 +07:00
Andre H 4d646e1ac0 🔧 chore: extend to buttonOrLinkProps 2024-02-22 15:03:21 +07:00
Andre H 5be29a9745 🔧 chore: add id to allow singular toast deletion 2024-02-22 14:59:12 +07:00
Andre H fffc370d40 🔧 chore: rework usememo logic 2024-02-22 14:58:57 +07:00
Andre H 1cd60d84dd 🔧 chore: use framer-motion 2024-02-22 14:22:29 +07:00
nabarunandneeraj a846531e43 Set DNS in application deployment request (#94)
Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-22 12:50:35 +05:30
Andre H c8a153ad27 🔧 chore: usememo dependencies 2024-02-22 14:20:26 +07:00
Andre H bb7a88f7e9 🔧 chore: add toast render page 2024-02-22 14:17:02 +07:00
Andre H 675112079c ️ feat: implement toast component 2024-02-22 14:16:47 +07:00
Andre H cb928c3360 🔧 chore: use buttonbaseprops, duration 2000 default 2024-02-22 14:16:21 +07:00
Andre H 5836779403 🔧 chore: export buttonBaseProps 2024-02-22 14:13:47 +07:00
Andre H 028dd35db1 🔧 chore: add icons 2024-02-22 14:13:27 +07:00
Andre H ef9f4df28a 🔧 chore: ghost button is white, not transparent 2024-02-22 14:13:10 +07:00
nabarunandneeraj 6b17dce2ae UI fixes in Snowball frontend app (#93)
* Fix alignment of deployment status chip

* Use template name from env

* Use env for git template link

* Add loading spinner for create project

* Display user name

* Format the displayed user name

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-22 11:25:17 +05:30
Andre H 267b52a352 Merge remote-tracking branch 'origin/andrehadianto/design-system-components' into andrehadianto/T-4869-toast 2024-02-22 11:49:11 +07:00
Andre H fa96b6e734 🔧 chore: add to components render page 2024-02-22 11:46:39 +07:00
Andre H 22c581dd33 🔧 chore: add icons 2024-02-22 11:46:17 +07:00
Andre H d3013719e6 🔧 chore: wip simple toast render 2024-02-22 11:45:55 +07:00
Andre H 98a4d7be07 ️ feat: implement toast contexts 2024-02-22 11:45:35 +07:00
Andre H 3928e2610f 🔧 chore: add zIndex toast token 2024-02-22 11:45:01 +07:00
Andre H 075cc05db5 ⚒️ build: add @radix-ui/react-toast 2024-02-22 11:43:31 +07:00
241 changed files with 11769 additions and 4146 deletions
+6
View File
@@ -1 +1,7 @@
node_modules/
yarn-error.log
.yarnrc.yml
.yarn/
.yarnrc
packages/backend/environments/local.toml
+158 -39
View File
@@ -1,8 +1,8 @@
# snowballtools
# snowballtools-base
## Setup
- Clone the `snowballtools` repo
- Clone the `snowballtools-base` repo
```bash
git clone git@github.com:snowball-tools/snowballtools-base.git
@@ -28,63 +28,144 @@
cd packages/backend
```
- Load fixtures in database
- Rename backend config file from [environments/local.toml.example](packages/backend/environments/local.toml.example) to `local.toml`
```bash
yarn test:db:load:fixtures
mv environments/local.toml.example environments/local.toml
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- Client ID and secret will be available after creating Github OAuth app
- https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
- In "Homepage URL", type `http://localhost:3000`
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
### Backend Production
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
- Let us assume the following domains for backend and frontend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
```bash
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
- Update the following in backend [config file](packages/backend/environments/local.toml)
```toml
[server]
...
[server.session]
# Secret should be changed to a different random string
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
# Set URL of the frontend app
appOriginUrl = "https://dashboard.snowballtools.com"
# Set to true for session cookies to work behind proxy
trustProxy = true
# Set empty domain when using secure connection
domain = ""
```
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
- In "Homepage URL", type `https://dashboard.snowballtools.com`
- In "Authorization callback URL", type `https://dashboard.snowballtools.com/organization/projects/create`
- Generate a new client secret after app is created
```bash
# For registryConfig.restEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
# 0.0.0.0:32777
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
# For registryConfig.gqlEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
# 0.0.0.0:32771
```toml
...
[gitHub]
webhookUrl = "https://api.snowballtools.com"
...
```
- Run the script to create bond, reserve the authority and set authority bond
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```toml
...
[registryConfig]
fetchDeploymentRecordDelay = 5000
# Use actual port for REST endpoint
restEndpoint = "http://api.laconic.com:1317"
# Use actual port for GQL endpoint
gqlEndpoint = "http://api.laconic.com:9473/api"
# Set private key of account to be used in Laconicd
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
# Set Bond ID to be used for publishing records
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
chainId = "laconic_9000-1"
# Set authority that is existing in the chain
authority = "laconic"
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "750000"
...
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Start the server in `packages/backend`
```bash
yarn start
```
### Backend Development
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
- In "Homepage URL", type `http://localhost:3000`
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Setup Laconicd
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
```bash
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
```
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
```bash
# For registryConfig.restEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
# 0.0.0.0:32777
# For registryConfig.gqlEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
# 0.0.0.0:32771
```
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Setup ngrok for GitHub webhooks
- https://ngrok.com/docs/getting-started/
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```
```bash
...
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
...
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
...
[gitHub]
@@ -95,7 +176,7 @@
- Start the server in `packages/backend`
```bash
yarn start
yarn start:dev
```
## Frontend
@@ -106,10 +187,10 @@
cd packages/frontend
```
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
- Rename [.env.example](packages/frontend/.env.example) to `.env`
```env
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
```bash
mv .env.example .env
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
@@ -118,23 +199,36 @@
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```env
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
# Set actual owner/name of the template repo that will be used for creating new repo
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = cerc-io/test-progressive-web-app
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = cerc-io/image-upload-pwa-example
```
### Development
### Frontend Production
- Start the React application
- Let us assume the following domains for backend and frontend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
```bash
yarn start
- Set the following values in [.env](packages/frontend/.env) file
```env
# Backend server endpoint
REACT_APP_SERVER_URL = 'https://api.snowballtools.com'
```
- The React application will be running in `http://localhost:3000/`
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Set project home page URL to `https://dashboard.snowballtools.com`
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
### Production
```env
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
@@ -148,3 +242,28 @@
python3 -m http.server -d build 3000
```
### Frontend Development
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
```env
REACT_APP_SERVER_URL = 'http://localhost:8000'
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Project home page URL is not required to be set
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
REACT_APP_WALLET_CONNECT_ID = <Project_ID>
```
- Start the React application
```bash
yarn start
```
- The React application will be running in `http://localhost:3000/`
+4 -2
View File
@@ -10,9 +10,11 @@ if [[ -d "$DEST_DIR" ]]; then
fi
cat > $PKG_DIR/.env <<EOF
REACT_APP_GQL_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_gql_url'
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
EOF
yarn || exit 1
+1
View File
@@ -1,2 +1,3 @@
db
dist
environments/local.toml
@@ -2,6 +2,11 @@
host = "127.0.0.1"
port = 8000
gqlPath = "/graphql"
[server.session]
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
appOriginUrl = "http://localhost:3000"
trustProxy = false
domain = "localhost"
[database]
dbPath = "db/snowball"
@@ -19,7 +24,11 @@
chainId = "laconic_9000-1"
privateKey = ""
bondId = ""
authority = ""
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "550000"
gas = "750000"
[misc]
projectDomain = "apps.snowballtools.com"
+3
View File
@@ -12,8 +12,10 @@
"@types/node": "^20.11.0",
"apollo-server-core": "^3.13.0",
"apollo-server-express": "^3.13.0",
"cors": "^2.8.5",
"debug": "^4.3.1",
"express": "^4.18.2",
"express-session": "^1.18.0",
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
"luxon": "^3.4.4",
@@ -42,6 +44,7 @@
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
},
"devDependencies": {
"@types/express-session": "^1.17.10",
"@types/fs-extra": "^11.0.4",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
+17
View File
@@ -1,7 +1,18 @@
export interface SessionConfig {
secret: string;
appOriginUrl: string;
trustProxy: boolean;
domain: string;
}
export interface ServerConfig {
host: string;
port: number;
gqlPath?: string;
sessionSecret: string;
appOriginUrl: string;
isProduction: boolean;
session: SessionConfig;
}
export interface DatabaseConfig {
@@ -23,6 +34,7 @@ export interface RegistryConfig {
privateKey: string;
bondId: string;
fetchDeploymentRecordDelay: number;
authority: string;
fee: {
amount: string;
denom: string;
@@ -30,9 +42,14 @@ export interface RegistryConfig {
};
}
export interface MiscConfig {
projectDomain: string;
}
export interface Config {
server: ServerConfig;
database: DatabaseConfig;
gitHub: GitHubConfig;
registryConfig: RegistryConfig;
misc: MiscConfig;
}
-8
View File
@@ -4,11 +4,3 @@ export const DEFAULT_CONFIG_FILE_PATH =
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
export const DEFAULT_GQL_PATH = '/graphql';
// Note: temporary hardcoded user, later to be derived from auth token
export const USER_ID =
process.env.SNOWBALL_BACKEND_USER_ID ||
'59f4355d-9549-4aac-9b54-eeefceeabef0';
export const PROJECT_DOMAIN =
process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz';
+20 -45
View File
@@ -11,7 +11,7 @@ import assert from 'assert';
import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary';
import { DatabaseConfig } from './config';
import { DatabaseConfig, MiscConfig } from './config';
import { User } from './entity/User';
import { Organization } from './entity/Organization';
import { Project } from './entity/Project';
@@ -19,13 +19,10 @@ import { Deployment } from './entity/Deployment';
import { ProjectMember } from './entity/ProjectMember';
import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain';
import { PROJECT_DOMAIN } from './constants';
import { getEntities, loadAndSaveData } from './utils';
import { UserOrganization } from './entity/UserOrganization';
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
const USER_DATA_PATH = '../test/fixtures/users.json';
const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json';
const log = debug('snowball:database');
@@ -34,8 +31,9 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
// TODO: Fix order of methods
export class Database {
private dataSource: DataSource;
private projectDomain: string;
constructor ({ dbPath }: DatabaseConfig) {
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
this.dataSource = new DataSource({
type: 'better-sqlite3',
database: dbPath,
@@ -43,6 +41,8 @@ export class Database {
synchronize: true,
logging: false
});
this.projectDomain = projectDomain;
}
async init (): Promise<void> {
@@ -51,36 +51,10 @@ export class Database {
const organizations = await this.getOrganizations({});
// Load an organization if none exist
if (!organizations.length) {
const orgEntities = await getEntities(
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
);
const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [
orgEntities[0]
]);
// TODO: Remove user once authenticated
const userEntities = await getEntities(
path.resolve(__dirname, USER_DATA_PATH)
);
const savedUsers = await loadAndSaveData(User, this.dataSource, [
userEntities[0]
]);
const userOrganizationRelations = {
member: savedUsers,
organization: savedOrgs
};
const userOrgEntities = await getEntities(
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
);
await loadAndSaveData(
UserOrganization,
this.dataSource,
[userOrgEntities[0]],
userOrganizationRelations
);
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
}
}
@@ -98,9 +72,9 @@ export class Database {
return user;
}
async updateUser (userId: string, data: DeepPartial<User>): Promise<boolean> {
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> {
const userRepository = this.dataSource.getRepository(User);
const updateResult = await userRepository.update({ id: userId }, data);
const updateResult = await userRepository.update({ id: user.id }, data);
assert(updateResult.affected);
return updateResult.affected > 0;
@@ -140,6 +114,13 @@ export class Database {
return userOrgs;
}
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
const newUserOrganization = await userOrganizationRepository.save(data);
return newUserOrganization;
}
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository.find(options);
@@ -455,11 +436,7 @@ export class Database {
return Boolean(updateResult.affected);
}
async addProject (
userId: string,
organizationId: string,
data: DeepPartial<Project>
): Promise<Project> {
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project);
// TODO: Check if organization exists
@@ -469,15 +446,13 @@ export class Database {
// TODO: Set icon according to framework
newProject.icon = '';
newProject.owner = Object.assign(new User(), {
id: userId
});
newProject.owner = user;
newProject.organization = Object.assign(new Organization(), {
id: organizationId
});
newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
return projectRepository.save(newProject);
}
+4 -4
View File
@@ -87,11 +87,11 @@ export class Deployment {
@Column('simple-json')
applicationRecordData!: ApplicationRecord;
@Column('varchar')
applicationDeploymentRequestId!: string;
@Column('varchar', { nullable: true })
applicationDeploymentRequestId!: string | null;
@Column('simple-json')
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
@Column('simple-json', { nullable: true })
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
@Column('varchar', { nullable: true })
applicationDeploymentRecordId!: string | null;
+5
View File
@@ -12,10 +12,15 @@ import { UserOrganization } from './UserOrganization';
@Entity()
@Unique(['email'])
@Unique(['ethAddress'])
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
// TODO: Set ethAddress as ID
@Column()
ethAddress!: string;
@Column('varchar', { length: 255, nullable: true })
name!: string | null;
@@ -12,7 +12,7 @@ import {
import { User } from './User';
import { Organization } from './Organization';
enum Role {
export enum Role {
Owner = 'Owner',
Maintainer = 'Maintainer',
Reader = 'Reader',
+2 -4
View File
@@ -19,9 +19,7 @@ const OAUTH_CLIENT_TYPE = 'oauth-app';
export const main = async (): Promise<void> => {
// TODO: get config path using cli
const { server, database, gitHub, registryConfig } = await getConfig<Config>(
DEFAULT_CONFIG_FILE_PATH
);
const { server, database, gitHub, registryConfig, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const app = new OAuthApp({
clientType: OAUTH_CLIENT_TYPE,
@@ -29,7 +27,7 @@ export const main = async (): Promise<void> => {
clientSecret: gitHub.oAuth.clientSecret
});
const db = new Database(database);
const db = new Database(database, misc);
await db.init();
const registry = new Registry(registryConfig);
+12 -14
View File
@@ -108,7 +108,7 @@ export class Registry {
log('Application record data:', applicationRecord);
// TODO: Discuss computation of CRN
const crn = this.getCrn(packageJSON.name, appName);
const crn = this.getCrn(appName);
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
await this.registry.setName(
@@ -137,16 +137,16 @@ export class Registry {
}
async createApplicationDeploymentRequest (data: {
appName: string;
packageJsonName: string;
commitHash: string;
repository: string;
environmentVariables: { [key: string]: string };
deployment: Deployment,
appName: string,
packageJsonName: string,
repository: string,
environmentVariables: { [key: string]: string }
}): Promise<{
applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> {
const crn = this.getCrn(data.packageJsonName, data.appName);
const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]);
const applicationRecord = records[0];
@@ -160,9 +160,9 @@ export class Registry {
version: '1.0.0',
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
application: `${crn}@${applicationRecord.attributes.app_version}`,
dns: `${data.deployment.project.name}-${data.deployment.id}`,
// TODO: Not set in test-progressive-web-app CI
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
@@ -174,7 +174,7 @@ export class Registry {
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
)}`,
repository: data.repository,
repository_ref: data.commitHash
repository_ref: data.deployment.commitHash
})
};
@@ -220,10 +220,8 @@ export class Registry {
);
}
getCrn (packageJsonName: string, appName: string): string {
const [arg1] = packageJsonName.split('/');
const authority = arg1.replace('@', '');
return `crn://${authority}/applications/${appName}`;
getCrn (appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist");
return `crn://${this.registryConfig.authority}/applications/${appName}`;
}
}
+13 -37
View File
@@ -14,26 +14,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
Query: {
// TODO: add custom type for context
user: (_: any, __: any, context: any) => {
return service.getUser(context.userId);
return context.user;
},
organizations: async (_: any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.userId);
organizations: async (_:any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.user);
},
project: async (_: any, { projectId }: { projectId: string }) => {
return service.getProjectById(projectId);
},
projectsInOrganization: async (
_: any,
{ organizationSlug }: { organizationSlug: string },
context: any
) => {
return service.getProjectsInOrganization(
context.userId,
organizationSlug
);
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
return service.getProjectsInOrganization(context.user, organizationSlug);
},
deployments: async (_: any, { projectId }: { projectId: string }) => {
@@ -51,12 +44,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId);
},
searchProjects: async (
_: any,
{ searchText }: { searchText: string },
context: any
) => {
return service.searchProjects(context.userId, searchText);
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
return service.searchProjects(context.user, searchText);
},
domains: async (
@@ -78,10 +67,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
context: any
) => {
try {
return await service.removeProjectMember(
context.userId,
projectMemberId
);
return await service.removeProjectMember(context.user, projectMemberId);
} catch (err) {
log(err);
return false;
@@ -188,9 +174,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
context: any
) => {
try {
return Boolean(
await service.updateDeploymentToProd(context.userId, deploymentId)
);
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
} catch (err) {
log(err);
return false;
@@ -206,11 +190,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
context: any
) => {
try {
return await service.addProject(
context.userId,
organizationSlug,
data
);
return await service.addProject(context.user, organizationSlug, data);
} catch (err) {
log(err);
throw err;
@@ -235,9 +215,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
context: any
) => {
try {
return Boolean(
await service.redeployToProd(context.userId, deploymentId)
);
return Boolean(await service.redeployToProd(context.user, deploymentId));
} catch (err) {
log(err);
return false;
@@ -307,7 +285,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
context: any
) => {
try {
return await service.authenticateGitHub(code, context.userId);
return await service.authenticateGitHub(code, context.user);
} catch (err) {
log(err);
return false;
@@ -316,9 +294,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
unauthenticateGitHub: async (_: any, __: object, context: any) => {
try {
return service.unauthenticateGitHub(context.userId, {
gitHubToken: null
});
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
} catch (err) {
log(err);
return false;
+41
View File
@@ -0,0 +1,41 @@
import { Router } from 'express';
import { SiweMessage, generateNonce } from 'siwe';
const router = Router();
router.get('/nonce', async (_, res) => {
res.send(generateNonce());
});
router.post('/validate', async (req, res) => {
const { message, signature } = req.body;
const { success, data } = await new SiweMessage(message).verify({
signature
});
if (success) {
req.session.address = data.address;
req.session.chainId = data.chainId;
}
res.send({ success });
});
router.get('/session', (req, res) => {
if (req.session.address && req.session.chainId) {
res.send({ address: req.session.address, chainId: req.session.chainId });
} else {
res.status(401).send({ error: 'Unauthorized: No active session' });
}
});
router.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.send({ success: false });
}
res.send({ success: true });
});
});
export default router;
+65 -7
View File
@@ -1,22 +1,33 @@
import debug from 'debug';
import express from 'express';
import cors from 'cors';
import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http';
import {
ApolloServerPluginDrainHttpServer,
ApolloServerPluginLandingPageLocalDefault
ApolloServerPluginLandingPageLocalDefault,
AuthenticationError
} from 'apollo-server-core';
import session from 'express-session';
import { TypeSource } from '@graphql-tools/utils';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH, USER_ID } from './constants';
import { DEFAULT_GQL_PATH } from './constants';
import githubRouter from './routes/github';
import authRouter from './routes/auth';
import { Service } from './service';
const log = debug('snowball:server');
declare module 'express-session' {
interface SessionData {
address: string;
chainId: number;
}
}
export const createAndStartServer = async (
serverConfig: ServerConfig,
typeDefs: TypeSource,
@@ -24,6 +35,7 @@ export const createAndStartServer = async (
service: Service
): Promise<ApolloServer> => {
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session;
const app = express();
@@ -39,9 +51,19 @@ export const createAndStartServer = async (
const server = new ApolloServer({
schema,
csrfPrevention: true,
context: () => {
// TODO: Use userId derived from auth token
return { userId: USER_ID };
context: async ({ req }) => {
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
const { address } = req.session;
if (!address) {
throw new AuthenticationError('Unauthorized: No active session');
}
// Find/create user from ETH address in request session
const user = await service.loadOrCreateUser(address);
return { user };
},
plugins: [
// Proper shutdown for the HTTP server
@@ -52,13 +74,49 @@ export const createAndStartServer = async (
await server.start();
app.use(cors({
origin: appOriginUrl,
credentials: true
}));
const sessionOptions: session.SessionOptions = {
secret: secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: new URL(appOriginUrl).protocol === 'https:',
// TODO: Set cookie maxAge and handle cookie expiry in frontend
// maxAge: SESSION_COOKIE_MAX_AGE,
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax'
}
};
if (domain) {
sessionOptions.cookie!.domain = domain;
}
if (trustProxy) {
// trust first proxy
app.set('trust proxy', 1);
}
app.use(
session(sessionOptions)
);
server.applyMiddleware({
app,
path: gqlPath
path: gqlPath,
cors: {
origin: [appOriginUrl],
credentials: true
}
});
app.set('service', service);
app.use(express.json());
app.set('service', service);
app.use('/auth', authRouter);
app.use('/api/github', githubRouter);
httpServer.listen(port, host, () => {
+101 -105
View File
@@ -16,6 +16,7 @@ import { User } from './entity/User';
import { Registry } from './registry';
import { GitHubConfig, RegistryConfig } from './config';
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
import { Role } from './entity/UserOrganization';
const log = debug('snowball:service');
@@ -160,6 +161,36 @@ export class Service {
});
}
async loadOrCreateUser (ethAddress: string): Promise<User> {
// Get user by ETH address
let user = await this.db.getUser({
where: {
ethAddress
}
});
if (!user) {
const [org] = await this.db.getOrganizations({});
assert(org, 'No organizations exists in database');
// Create user with new address
user = await this.db.addUser({
email: `${ethAddress}@example.com`,
name: ethAddress,
isVerified: true,
ethAddress
});
await this.db.addUserOrganization({
member: user,
organization: org,
role: Role.Owner
});
}
return user;
}
async getOctokit (userId: string): Promise<Octokit> {
const user = await this.db.getUser({ where: { id: userId } });
assert(
@@ -170,8 +201,8 @@ export class Service {
return new Octokit({ auth: user.gitHubToken });
}
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
async getOrganizationsByUserId (user: User): Promise<Organization[]> {
const dbOrganizations = await this.db.getOrganizationsByUserId(user.id);
return dbOrganizations;
}
@@ -180,14 +211,8 @@ export class Service {
return dbProject;
}
async getProjectsInOrganization (
userId: string,
organizationSlug: string
): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(
userId,
organizationSlug
);
async getProjectsInOrganization (user: User, organizationSlug: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(user.id, organizationSlug);
return dbProjects;
}
@@ -212,11 +237,8 @@ export class Service {
return dbProjectMembers;
}
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsBySearchText(
userId,
searchText
);
async searchProjects (user: User, searchText: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsBySearchText(user.id, searchText);
return dbProjects;
}
@@ -269,20 +291,17 @@ export class Service {
return newProjectMember;
}
async removeProjectMember (
userId: string,
projectMemberId: string
): Promise<boolean> {
async removeProjectMember (user: User, projectMemberId: string): Promise<boolean> {
const member = await this.db.getProjectMemberById(projectMemberId);
if (String(member.member.id) === userId) {
if (String(member.member.id) === user.id) {
throw new Error('Invalid operation: cannot remove self');
}
const memberProject = member.project;
assert(memberProject);
if (String(userId) === String(memberProject.owner.id)) {
if (String(user.id) === String(memberProject.owner.id)) {
return this.db.removeProjectMemberById(projectMemberId);
} else {
throw new Error('Invalid operation: not authorized');
@@ -327,10 +346,7 @@ export class Service {
return this.db.deleteEnvironmentVariable(environmentVariableId);
}
async updateDeploymentToProd (
userId: string,
deploymentId: string
): Promise<Deployment> {
async updateDeploymentToProd (user: User, deploymentId: string): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({
where: { id: deploymentId },
relations: {
@@ -347,16 +363,18 @@ export class Service {
{ branch: oldDeployment.project.prodBranch }
);
const octokit = await this.getOctokit(userId);
const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(userId, octokit, {
project: oldDeployment.project,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
const newDeployment = await this.createDeployment(user.id,
octokit,
{
project: oldDeployment.project,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
return newDeployment;
}
@@ -407,39 +425,15 @@ export class Service {
repoUrl: recordData.repoUrl
});
const environmentVariables =
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
environment: Environment.Production
});
const environmentVariablesObj = environmentVariables.reduce(
(acc, env) => {
acc[env.key] = env.value;
return acc;
},
{} as { [key: string]: string }
);
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.registry.createApplicationDeploymentRequest({
appName: repo,
packageJsonName: packageJSON.name,
commitHash: data.commitHash!,
repository: recordData.repoUrl,
environmentVariables: environmentVariablesObj
});
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
await this.db.updateDeployment(
{
domainId: data.domain?.id
},
{
if (data.domain) {
await this.db.updateDeployment({
domainId: data.domain.id
}, {
domain: null
}
);
});
}
const newDeployment = await this.db.addDeployment({
project: data.project,
@@ -450,25 +444,37 @@ export class Service {
status: DeploymentStatus.Building,
applicationRecordId,
applicationRecordData,
applicationDeploymentRequestId,
applicationDeploymentRequestData,
domain: data.domain,
createdBy: Object.assign(new User(), {
id: userId
})
});
log(
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`
);
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { environment: Environment.Production });
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
acc[env.key] = env.value;
return acc;
}, {} as { [key: string]: string });
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
{
deployment: newDeployment,
appName: repo,
packageJsonName: packageJSON.name,
repository: recordData.repoUrl,
environmentVariables: environmentVariablesObj
});
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
return newDeployment;
}
async addProject (
userId: string,
organizationSlug: string,
data: DeepPartial<Project>
): Promise<Project | undefined> {
async addProject (user: User, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
const organization = await this.db.getOrganization({
where: {
slug: organizationSlug
@@ -478,9 +484,9 @@ export class Service {
throw new Error('Organization does not exist');
}
const project = await this.db.addProject(userId, organization.id, data);
const project = await this.db.addProject(user, organization.id, data);
const octokit = await this.getOctokit(userId);
const octokit = await this.getOctokit(user.id);
const [owner, repo] = project.repository.split('/');
const {
@@ -495,8 +501,7 @@ export class Service {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
// Create deployment with prod branch and latest commit
await this.createDeployment(
userId,
await this.createDeployment(user.id,
octokit,
{
project,
@@ -613,10 +618,7 @@ export class Service {
return this.db.deleteDomainById(domainId);
}
async redeployToProd (
userId: string,
deploymentId: string
): Promise<Deployment> {
async redeployToProd (user: User, deploymentId: string): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({
relations: {
project: true,
@@ -632,17 +634,19 @@ export class Service {
throw new Error('Deployment not found');
}
const octokit = await this.getOctokit(userId);
const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(userId, octokit, {
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
const newDeployment = await this.createDeployment(user.id,
octokit,
{
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
return newDeployment;
}
@@ -778,25 +782,17 @@ export class Service {
return updateResult;
}
async authenticateGitHub (
code: string,
userId: string
): Promise<{ token: string }> {
const {
authentication: { token }
} = await this.oauthApp.createToken({
async authenticateGitHub (code:string, user: User): Promise<{token: string}> {
const { authentication: { token } } = await this.oauthApp.createToken({
code
});
await this.db.updateUser(userId, { gitHubToken: token });
await this.db.updateUser(user, { gitHubToken: token });
return { token };
}
async unauthenticateGitHub (
userId: string,
data: DeepPartial<User>
): Promise<boolean> {
return this.db.updateUser(userId, data);
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
return this.db.updateUser(user, data);
}
}
+9 -6
View File
@@ -1,20 +1,23 @@
[
{
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
"name": "Snowball",
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"email": "snowball@snowballtools.xyz",
"isVerified": true
"isVerified": true,
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
},
{
"id": "e505b212-8da6-48b2-9614-098225dab34b",
"name": "Alice Anderson",
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
"email": "alice@snowballtools.xyz",
"isVerified": true
"isVerified": true,
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
},
{
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
"name": "Bob Banner",
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
"email": "bob@snowballtools.xyz",
"isVerified": true
"isVerified": true,
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
}
]
+6 -14
View File
@@ -11,17 +11,13 @@ const log = debug('snowball:initialize-registry');
const DENOM = 'aphoton';
const BOND_AMOUNT = '1000000000';
// TODO: Get authority names from args
const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
async function main () {
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
// TODO: Get authority names from args
const authorityNames = ['snowballtools', registryConfig.authority];
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
const bondId = await registry.getNextBondId(registryConfig.privateKey);
log('bondId:', bondId);
@@ -31,12 +27,8 @@ async function main () {
registryConfig.fee
);
for await (const name of AUTHORITY_NAMES) {
await registry.reserveAuthority(
{ name },
registryConfig.privateKey,
registryConfig.fee
);
for await (const name of authorityNames) {
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
log('Reserved authority name:', name);
await registry.setAuthorityBond(
{ name, bondId },
@@ -5,16 +5,14 @@ import path from 'path';
import { Registry } from '@cerc-io/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH, PROJECT_DOMAIN } from '../src/constants';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records');
async function main () {
const { registryConfig, database } = await getConfig<Config>(
DEFAULT_CONFIG_FILE_PATH
);
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(
registryConfig.gqlEndpoint,
@@ -42,7 +40,7 @@ async function main () {
});
for await (const deployment of deployments) {
const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`;
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
const applicationDeploymentRecord = {
type: 'ApplicationDeploymentRecord',
+99
View File
@@ -0,0 +1,99 @@
# deployer
- Install dependencies
```bash
yarn
```
```bash
brew install jq # if you do not have jq installed already
```
Example of how to make the necessary deploy edits [here](https://github.com/snowball-tools/snowballtools-base/pull/131/files).
- Replace variables in the following files
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
- update the name & application version numbers
- `<CURRENT_DATE_TIME>`: Replace with current time which can be generated by command `date -u`
```yml
# Example
record:
...
meta:
note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC
...
```
- Update record version in [records/application-record.yml](records/application-record.yml)
```yml
record:
type: ApplicationRecord
version: <NEW_VERSION>
...
```
- Update commit hash in the following places:
- [records/application-record.yml](records/application-record.yml)
```yml
record:
...
repository_ref: <COMMIT_HASH>
...
```
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
meta:
...
repository_ref: <COMMIT_HASH>
```
- [deploy-frontend.sh](deploy-frontend.sh)
Also be sure to update the app version
```bash
...
RCD_APP_VERSION="<NEW_VERSION>"
REPO_REF="<COMMIT_HASH>"
...
```
- Run script to deploy app
```
./deploy-frontend.sh
```
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
config:
env:
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
## Troubleshoot
- Check deployment status [here](https://console.laconic.com/deployer).
- Check records [here](https://console.laconic.com/#/registry).
- If deployment fails due to low bond balance
- Check balances
```bash
# Account balance
yarn laconic cns account get
# Bond balance
yarn laconic cns bond get --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
```
- Command to refill bond
```bash
yarn laconic cns bond refill --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac --type aphoton --quantity 10000000
```
+9
View File
@@ -0,0 +1,9 @@
services:
cns:
restEndpoint: http://console.laconic.com:1317
gqlEndpoint: http://console.laconic.com:9473/api
chainId: laconic_9000-1
gas: 1200000
fees: 200000aphoton
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Repository URL
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
# Extract version from ../frontend/package.json
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
# Current date and time for note
CURRENT_DATE_TIME=$(date -u)
# Increment application-record version
APPLICATION_RECORD_FILE="./records/application-record.yml"
if [ -f "$APPLICATION_RECORD_FILE" ]; then
# Extract current version and increment it
CURRENT_VERSION=$(grep 'version:' $APPLICATION_RECORD_FILE | head -1 | awk '{print $2}')
IFS='.' read -ra ADDR <<< "$CURRENT_VERSION"
VERSION_NUMBER=${ADDR[2]}
NEW_VERSION_NUMBER=$((VERSION_NUMBER + 1))
NEW_APPLICATION_VERSION="${ADDR[0]}.${ADDR[1]}.$NEW_VERSION_NUMBER"
else
# If file does not exist, start from version 0.0.1
NEW_APPLICATION_VERSION="0.0.1"
fi
# Generate application-deployment-request.yml
cat > ./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version
cat > ./records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: snowballtools-base-frontend
app_version: $PACKAGE_VERSION
EOF
echo "Files generated successfully."
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
RECORD_FILE=records/application-record.yml
CONFIG_FILE=config.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
RECORD_FILE=records/application-deployment-request.yml
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
+9
View File
@@ -0,0 +1,9 @@
{
"name": "deployer",
"version": "1.0.0",
"main": "index.js",
"private": true,
"devDependencies": {
"@cerc-io/laconic-registry-cli": "^0.1.10"
}
}
@@ -0,0 +1,18 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ Thu Feb 29 07:46:48 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: a7810a34c90d5bfe0c6cc0697ad9aaf79b1c41d7
@@ -0,0 +1,8 @@
record:
type: ApplicationRecord
version: 0.0.27
repository_ref: a7810a34c90d5bfe0c6cc0697ad9aaf79b1c41d7
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: snowballtools-base-frontend
app_version: 0.1.8
-4
View File
@@ -1,4 +0,0 @@
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
REACT_APP_GITHUB_CLIENT_ID =
REACT_APP_GITHUB_TEMPLATE_REPO =
+7
View File
@@ -0,0 +1,7 @@
REACT_APP_SERVER_URL = 'http://localhost:8000'
REACT_APP_GITHUB_CLIENT_ID =
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
REACT_APP_WALLET_CONNECT_ID =
+6 -1
View File
@@ -16,5 +16,10 @@
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
]
],
"settings": {
"react": {
"version": "detect"
}
}
}
+1
View File
@@ -13,6 +13,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
-39
View File
@@ -1,39 +0,0 @@
{
// eslint extension options
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"css.customData": [".vscode/tailwind.json"],
// prettier extension setting
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.rulers": [80],
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.fixAll",
"source.organizeImports"
],
// Show in vscode "Problems" tab when there are errors
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
// Use absolute import for typescript files
"typescript.preferences.importModuleSpecifier": "non-relative",
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+7
View File
@@ -0,0 +1,7 @@
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="500" height="500" fill="#0F86F5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+13 -3
View File
@@ -1,16 +1,20 @@
{
"name": "frontend",
"version": "0.1.0",
"version": "0.1.8",
"private": true,
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.22.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -18,10 +22,13 @@
"@types/node": "^16.18.68",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"@web3modal/siwe": "^4.0.5",
"@web3modal/wagmi": "^4.0.5",
"assert": "^2.1.0",
"axios": "^1.6.7",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"downshift": "^8.2.3",
"downshift": "^8.3.2",
"eslint-config-react-app": "^7.0.1",
"gql-client": "^1.0.0",
"luxon": "^3.4.4",
@@ -38,10 +45,13 @@
"react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-timer-hook": "^3.0.7",
"siwe": "^2.1.4",
"tailwind-variants": "^0.2.0",
"typescript": "^4.9.5",
"usehooks-ts": "^2.10.0",
"vertical-stepper-nav": "^1.0.2",
"viem": "^2.7.11",
"wagmi": "^2.5.7",
"web-vitals": "^2.1.4"
},
"scripts": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#2d89ef</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

+22 -30
View File
@@ -2,42 +2,34 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<meta name="description" content="snowball tools dashboard" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
<meta name="msapplication-TileColor" content="#2d89ef" />
<meta name="theme-color" content="#ffffff" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Snowball</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+19
View File
@@ -0,0 +1,19 @@
{
"name": "Snowball Tools Dashboard",
"short_name": "snowball tools",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
@@ -0,0 +1,4 @@
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="currentColor"/>
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,15 @@
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1177_19315)">
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="url(#paint0_linear_1177_19315)"/>
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="#DBEBF9"/>
</g>
<defs>
<linearGradient id="paint0_linear_1177_19315" x1="9.25" y1="0.650024" x2="9.25" y2="3.9698" gradientUnits="userSpaceOnUse">
<stop stop-color="#EBF6FF"/>
<stop offset="1" stop-color="#F7FCFF" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_1177_19315">
<rect width="19" height="4" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,10 @@
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1175_19292)">
<path d="M-9.5 0L-3.10445 2.69286C-1.11922 3.52875 1.11922 3.52875 3.10446 2.69286L6.39555 1.30714C8.38078 0.47125 10.6192 0.47125 12.6045 1.30714L15.8955 2.69286C17.8808 3.52875 20.1192 3.52875 22.1045 2.69286L28.5 0" stroke="#082F56" stroke-opacity="0.1"/>
</g>
<defs>
<clipPath id="clip0_1175_19292">
<rect width="19" height="4" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 507 B

+8 -8
View File
@@ -1,7 +1,6 @@
import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import OrgSlug from './pages/OrgSlug';
import Projects from './pages/org-slug';
import Settings from './pages/org-slug/Settings';
import {
@@ -9,13 +8,14 @@ import {
projectsRoutesWithoutSearch,
} from './pages/org-slug/projects/routes';
import ProjectSearchLayout from './layouts/ProjectSearch';
import { OctokitProvider } from './context/OctokitContext';
import Index from './pages';
import Login from './pages/Login';
import { DashboardLayout } from 'pages/org-slug/layout';
const router = createBrowserRouter([
{
path: ':orgSlug',
element: <OrgSlug />,
element: <DashboardLayout />,
children: [
{
element: <ProjectSearchLayout />,
@@ -44,14 +44,14 @@ const router = createBrowserRouter([
path: '/',
element: <Index />,
},
{
path: '/login',
element: <Login />,
},
]);
function App() {
return (
<OctokitProvider>
<RouterProvider router={router} />
</OctokitProvider>
);
return <RouterProvider router={router} />;
}
export default App;
@@ -1,27 +0,0 @@
[
{
"id": "1",
"name": "Progressive Web App (PWA)",
"icon": "^"
},
{
"id": "2",
"name": "Kotlin",
"icon": "^"
},
{
"id": "3",
"name": "React Native",
"icon": "^"
},
{
"id": "4",
"name": "Swift",
"icon": "^"
},
{
"id": "5",
"name": "Web app",
"icon": "^"
}
]
+37
View File
@@ -0,0 +1,37 @@
export default [
{
id: '1',
name: 'Progressive Web App (PWA)',
icon: 'pwa',
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '2',
name: 'Image Upload PWA',
icon: 'pwa',
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '3',
name: 'Kotlin',
icon: 'kotlin',
repoFullName: '',
isComingSoon: true,
},
{
id: '4',
name: 'React Native',
icon: 'react-native',
repoFullName: '',
isComingSoon: true,
},
{
id: '5',
name: 'Swift',
icon: 'swift',
repoFullName: '',
isComingSoon: true,
},
];
@@ -126,7 +126,8 @@ const DatePicker = ({
crossOrigin={undefined}
/>
</PopoverHandler>
<PopoverContent>
{/* TODO: Figure out what placeholder is for */}
<PopoverContent placeholder={''}>
{mode === 'single' && (
<DayPicker
mode="single"
@@ -145,19 +146,23 @@ const DatePicker = ({
/>
<HorizontalLine />
<div className="flex justify-end">
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full mr-2"
variant="outlined"
onClick={() => setIsOpen(false)}
placeholder={''}
>
Cancel
</Button>
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full"
color="gray"
onClick={() => handleRangeSelect()}
placeholder={''}
>
Select
</Button>
@@ -1,7 +1,7 @@
import React from 'react';
const HorizontalLine = () => {
return <hr className="h-px bg-gray-300 border-0" />;
return <hr className="h-px bg-gray-100 border-0" />;
};
export default HorizontalLine;
+6 -16
View File
@@ -1,33 +1,23 @@
import React, { forwardRef, RefAttributes } from 'react';
import { Input, InputProps } from '@material-tailwind/react';
import { SearchIcon } from './shared/CustomIcon';
import { Input, InputProps } from './shared/Input';
const SearchBar: React.ForwardRefRenderFunction<
HTMLInputElement,
InputProps & RefAttributes<HTMLInputElement>
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
return (
<div className="relative flex w-full gap-2">
<div className="relative flex w-full">
<Input
leftIcon={<SearchIcon />}
onChange={onChange}
value={value}
type="search"
placeholder={placeholder}
containerProps={{
className: 'min-w-[288px]',
}}
className="!border-t-blue-gray-300 pl-9 placeholder:text-blue-gray-300 focus:!border-blue-gray-300"
labelProps={{
className: 'before:content-none after:content-none',
}}
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
crossOrigin={undefined}
appearance={'borderless'}
{...props}
inputRef={ref}
/>
<div className="!absolute left-3 top-[13px]">
<i>^</i>
</div>
</div>
);
};
@@ -1,86 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
import { Organization } from 'gql-client';
import { Typography, Option } from '@material-tailwind/react';
import { useGQLClient } from '../context/GQLClientContext';
import AsyncSelect from './shared/AsyncSelect';
const Sidebar = () => {
const { orgSlug } = useParams();
const navigate = useNavigate();
const client = useGQLClient();
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const fetchUserOrganizations = useCallback(async () => {
const { organizations } = await client.getOrganizations();
setOrganizations(organizations);
}, [orgSlug]);
useEffect(() => {
fetchUserOrganizations();
setSelectedOrgSlug(orgSlug);
}, [orgSlug]);
return (
<div className="flex flex-col h-full p-4">
<div className="grow">
<div>
<Link to={`/${orgSlug}`}>
<h3 className="text-black text-2xl">Snowball</h3>
</Link>
</div>
<AsyncSelect
className="bg-white py-2"
value={selectedOrgSlug}
onChange={(value) => {
setSelectedOrgSlug(value!);
navigate(`/${value}`);
}}
selected={(_, index) => (
<div className="flex gap-2">
<div>^</div>
<div>
<span>{organizations[index!]?.name}</span>
<Typography>Organization</Typography>
</div>
</div>
)}
>
{/* TODO: Show label organization and manage in option */}
{organizations.map((org) => (
<Option key={org.id} value={org.slug}>
^ {org.name}
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
</Option>
))}
</AsyncSelect>
<div>
<NavLink
to={`/${orgSlug}`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
>
<Typography>Projects</Typography>
</NavLink>
</div>
<div>
<NavLink
to={`/${orgSlug}/settings`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
>
<Typography>Settings</Typography>
</NavLink>
</div>
</div>
<div className="grow flex flex-col justify-end">
<div>Documentation</div>
<div>Support</div>
</div>
</div>
);
};
export default Sidebar;
@@ -1,65 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Project } from 'gql-client';
import {
Menu,
MenuHandler,
MenuList,
MenuItem,
Typography,
Avatar,
} from '@material-tailwind/react';
import { relativeTimeMs } from '../../utils/time';
interface ProjectCardProps {
project: Project;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
return (
<div className="bg-white border border-gray-200 rounded-lg shadow">
<div className="flex gap-2 p-2 items-center">
<Avatar variant="rounded" src={project.icon || '/gray.png'} />
<div className="grow">
<Link to={`projects/${project.id}`}>
<Typography>{project.name}</Typography>
<Typography color="gray" variant="small">
{project.deployments[0]?.domain?.name ??
'No Production Deployment'}
</Typography>
</Link>
</div>
<Menu placement="bottom-end">
<MenuHandler>
<button>...</button>
</MenuHandler>
<MenuList>
<MenuItem>^ Project settings</MenuItem>
<MenuItem className="text-red-500">^ Delete project</MenuItem>
</MenuList>
</Menu>
</div>
<div className="border-t-2 border-solid p-4 bg-gray-50">
{project.deployments.length > 0 ? (
<>
<Typography variant="small" color="gray">
^ {project.deployments[0].commitMessage}
</Typography>
<Typography variant="small" color="gray">
{relativeTimeMs(project.deployments[0].createdAt)} on ^&nbsp;
{project.deployments[0].branch}
</Typography>
</>
) : (
<Typography variant="small" color="gray">
No Production deployment
</Typography>
)}
</div>
</div>
);
};
export default ProjectCard;
@@ -0,0 +1,80 @@
import { VariantProps, tv } from 'tailwind-variants';
export const projectCardTheme = tv({
slots: {
wrapper: [
'bg-surface-card',
'shadow-card',
'rounded-2xl',
'flex',
'flex-col',
'group',
],
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
content: ['flex', 'flex-col', 'gap-1', 'flex-1'],
title: [
'text-sm',
'font-medium',
'text-elements-high-em',
'tracking-[-0.006em]',
],
description: ['text-xs', 'text-elements-low-em'],
icons: ['flex', 'items-center', 'gap-1'],
lowerContent: [
'transition-colors',
'duration-150',
'px-4',
'py-4',
'flex',
'flex-col',
'gap-2',
'rounded-b-2xl',
'group-hover:bg-surface-card-hovered',
],
latestDeployment: ['flex', 'items-center', 'gap-2'],
deploymentStatusContainer: [
'h-3',
'w-3',
'flex',
'items-center',
'justify-center',
],
deploymentStatus: ['w-1', 'h-1', 'rounded-full'],
deploymentName: ['text-xs', 'text-elements-low-em'],
deploymentText: [
'text-xs',
'text-elements-low-em',
'font-mono',
'flex',
'items-center',
'gap-2',
],
wavyBorder: [
'bg-surface-card',
'transition-colors',
'duration-150',
'group-hover:bg-surface-card-hovered',
],
},
variants: {
status: {
success: {
deploymentStatus: ['bg-emerald-500'],
},
'in-progress': {
deploymentStatus: ['bg-orange-400'],
},
failure: {
deploymentStatus: ['bg-rose-500'],
},
pending: {
deploymentStatus: ['bg-gray-500'],
},
},
},
defaultVariants: {
status: 'pending',
},
});
export type ProjectCardTheme = VariantProps<typeof projectCardTheme>;
@@ -0,0 +1,139 @@
import React, {
ComponentPropsWithoutRef,
MouseEvent,
useCallback,
} from 'react';
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
import { Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import { WavyBorder } from 'components/shared/WavyBorder';
import {
BranchIcon,
ClockIcon,
GitHubLogo,
HorizontalDotIcon,
WarningDiamondIcon,
} from 'components/shared/CustomIcon';
import { relativeTimeMs } from 'utils/time';
import { useNavigate } from 'react-router-dom';
import { Avatar } from 'components/shared/Avatar';
import { getInitials } from 'utils/geInitials';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@material-tailwind/react';
export interface ProjectCardProps
extends ComponentPropsWithoutRef<'div'>,
ProjectCardTheme {
project: Project;
}
// TODO: Update the whole component to use `Link` from `react-router-dom` and remove the `useNavigate` hook,
// currently it's not possible to use `Link` because the dot menu is not a direct child of the `Link` component
export const ProjectCard = ({
className,
project,
status = 'failure',
...props
}: ProjectCardProps) => {
const theme = projectCardTheme();
const hasDeployment = project.deployments.length > 0;
// TODO: Update this to use the actual status from the API
const hasError = status === 'failure';
const navigate = useNavigate();
const handleOptionsClick = (
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
) => {
e.stopPropagation();
};
const handleClick = useCallback(() => {
navigate(`projects/${project.id}`);
}, [project.id, navigate]);
return (
<div
{...props}
className={theme.wrapper({ className })}
onClick={handleClick}
>
{/* Upper content */}
<div className={theme.upperContent()}>
{/* Icon container */}
<Avatar
size={48}
imageSrc={project.icon}
initials={getInitials(project.name)}
/>
{/* Title and website */}
<div className={theme.content()}>
<p className={theme.title()}>{project.name}</p>
<p className={theme.description()}>
{project.deployments[0]?.domain?.name ?? 'No domain'}
</p>
</div>
{/* Icons */}
<div className={theme.icons()}>
{hasError && <WarningDiamondIcon className="text-elements-danger" />}
<Menu placement="bottom-end">
<MenuHandler>
<Button
shape="default"
size="xs"
variant="ghost"
iconOnly
onClick={handleOptionsClick}
>
<HorizontalDotIcon />
</Button>
</MenuHandler>
<MenuList placeholder={''}>
<MenuItem placeholder={''}>Project settings</MenuItem>
<MenuItem className="text-red-500" placeholder={''}>
Delete project
</MenuItem>
</MenuList>
</Menu>
</div>
</div>
{/* Wave */}
<WavyBorder variant="stroke-and-fill" className={theme.wavyBorder()} />
{/* Lower content */}
<div className={theme.lowerContent()}>
{/* Latest deployment */}
<div className={theme.latestDeployment()}>
{/* Dot icon */}
<div className={theme.deploymentStatusContainer()}>
<div className={theme.deploymentStatus({ status })} />
</div>
<p className={theme.deploymentText()}>
{hasDeployment
? project.deployments[0]?.commitMessage
: 'No production deployment'}
</p>
</div>
{/* Deployment and branch name */}
<div className={theme.deploymentText()}>
{hasDeployment ? (
<>
<GitHubLogo />
<span>{relativeTimeMs(project.deployments[0].createdAt)} on</span>
<BranchIcon />
<span>{project.deployments[0].branch}</span>
</>
) : (
<>
<ClockIcon />
<span>Created {relativeTimeMs(project.createdAt)}</span>
</>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from './ProjectCard';
@@ -71,12 +71,13 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
(!inputValue || !isOpen) && 'hidden'
}`}
placeholder={''}
>
<List {...getMenuProps()}>
{items.length ? (
<>
<div className="p-3">
<Typography variant="small" color="gray">
<Typography variant="small" color="gray" placeholder={''}>
Suggestions
</Typography>
</div>
@@ -84,19 +85,25 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
<ListItem
selected={highlightedIndex === index || selectedItem === item}
key={item.id}
placeholder={''}
{...getItemProps({ item, index })}
>
<ListItemPrefix>
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
<ListItemPrefix placeholder={''}>
<Avatar
src={item.icon || '/gray.png'}
variant="rounded"
placeholder={''}
/>
</ListItemPrefix>
<div>
<Typography variant="h6" color="blue-gray">
<Typography variant="h6" color="blue-gray" placeholder={''}>
{item.name}
</Typography>
<Typography
variant="small"
color="gray"
className="font-normal"
placeholder={''}
>
{item.organization.name}
</Typography>
@@ -106,7 +113,9 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
</>
) : (
<div className="p-3">
<Typography>^ No projects matching this name</Typography>
<Typography placeholder={''}>
^ No projects matching this name
</Typography>
</div>
)}
</List>
@@ -1,9 +1,18 @@
import { Button } from '@material-tailwind/react';
import React from 'react';
import OauthPopup from 'react-oauth-popup';
import { useGQLClient } from '../../../context/GQLClientContext';
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
import { Button } from 'components/shared/Button';
import {
GitIcon,
EllipsesIcon,
SnowballIcon,
GithubIcon,
GitTeaIcon,
} from 'components/shared/CustomIcon';
import { useToast } from 'components/shared/Toast';
import { IconWithFrame } from 'components/shared/IconWithFrame';
import { Heading } from 'components/shared/Heading';
const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
@@ -14,43 +23,79 @@ interface ConnectAccountInterface {
onAuth: (token: string) => void;
}
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
onAuth: onToken,
}: ConnectAccountInterface) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const handleCode = async (code: string) => {
// Pass code to backend and get access token
const {
authenticateGitHub: { token },
} = await client.authenticateGitHub(code);
onToken(token);
toast({
onDismiss: dismiss,
id: 'connected-to-github',
title: 'The Git account is connected.',
variant: 'success',
});
};
// TODO: Use correct height
return (
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
<div>^</div>
<div>
<p>Connect to your git account</p>
<p>
Once connected, you can import a repository from your
<br />
account or start with one of our templates.
</p>
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
<div className="flex flex-col items-center max-w-[420px]">
{/** Icons */}
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
<IconWithFrame icon={<GitIcon />} />
<EllipsesIcon className="items-center gap-1.5 flex" />
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
</div>
{/** Text */}
<div className="flex flex-col gap-1.5 mb-6">
<Heading className="text-xl font-medium">
Connect to your Git account
</Heading>
<p className="text-center text-elements-mid-em">
Once connected, you can import a repository from your account or
start with one of our templates.
</p>
</div>
{/** CTA Buttons */}
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={handleCode}
onClose={() => {}}
title="Snowball"
width={1000}
height={1000}
>
<Button
className="w-full sm:w-auto"
leftIcon={<GithubIcon />}
variant="tertiary"
>
Connect to GitHub
</Button>
</OauthPopup>
<Button
className="w-full sm:w-auto"
leftIcon={<GitTeaIcon />}
variant="tertiary"
>
Connect to GitTea
</Button>
</div>
</div>
<div className="mt-2 flex">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={handleCode}
onClose={() => {}}
title="Snowball"
width={1000}
height={1000}
>
<Button className="rounded-full mx-2">Connect to Github</Button>
</OauthPopup>
<Button className="rounded-full mx-2">Connect to Gitea</Button>
</div>
<ConnectAccountTabPanel />
{/* TODO: Add ConnectAccountTabPanel */}
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
<ConnectAccountTabPanel />
</div> */}
</div>
);
};
@@ -1,21 +1,24 @@
import React from 'react';
import { Tabs, TabsHeader, Tab } from '@material-tailwind/react';
import { Tabs } from 'components/shared/Tabs';
const ConnectAccountTabPanel = () => {
const ConnectAccountTabPanel: React.FC = () => {
return (
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
<TabsHeader className="grid grid-cols-2">
<Tab className="row-span-1" value="import">
Import a repository
</Tab>
<Tab className="row-span-2" value="template">
Start with a template
</Tab>
</TabsHeader>
{/* <TabsBody> */}
{/* TODO: Add content */}
{/* </TabsBody> */}
<Tabs
defaultValue="Connect Accounts Tab Panel"
orientation="horizontal"
className="mt-10"
>
<Tabs.List>
{[
{ title: 'Import a repository' },
{ title: 'Start with a template' },
].map(({ title }, index) => (
<Tabs.Trigger value={title} key={index}>
{title}
</Tabs.Trigger>
))}
</Tabs.List>
</Tabs>
);
};
@@ -1,12 +1,13 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { Button, Typography } from '@material-tailwind/react';
import { DeployStep, DeployStatus } from './DeployStep';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import ConfirmDialog from '../../shared/ConfirmDialog';
import ConfirmDialog from 'components/shared/ConfirmDialog';
const TIMEOUT_DURATION = 5000;
const Deploy = () => {
const [searchParams] = useSearchParams();
const projectId = searchParams.get('projectId');
@@ -21,6 +22,14 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`);
}, []);
useEffect(() => {
const timerID = setTimeout(() => {
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
}, TIMEOUT_DURATION);
return () => clearInterval(timerID);
}, []);
return (
<div>
<div className="flex justify-between mb-6">
@@ -34,7 +43,12 @@ const Deploy = () => {
</div>
</div>
<div>
<Button onClick={handleOpen} variant="outlined" size="sm">
<Button
onClick={handleOpen}
variant="outlined"
size="sm"
placeholder={''}
>
^ Cancel
</Button>
</div>
@@ -46,7 +60,7 @@ const Deploy = () => {
handleConfirm={handleCancel}
color="red"
>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
This will halt the deployment and you will have to start the process
from scratch.
</Typography>
@@ -74,14 +88,6 @@ const Deploy = () => {
status={DeployStatus.NOT_STARTED}
step="4"
/>
<Button
onClick={() => {
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
}}
>
VIEW DEMO
</Button>
</div>
);
};
@@ -62,7 +62,12 @@ const DeployStep = ({
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
{processLogs.map((log, key) => {
return (
<Typography variant="small" color="gray" key={key}>
<Typography
variant="small"
color="gray"
key={key}
placeholder={''}
>
{log}
</Typography>
);
@@ -75,6 +80,7 @@ const DeployStep = ({
toast.success('Logs copied');
}}
color="blue"
placeholder={''}
>
^ Copy log
</Button>
@@ -1,11 +1,13 @@
import React, { useCallback } from 'react';
import toast from 'react-hot-toast';
import { useNavigate, useParams } from 'react-router-dom';
import { Chip, IconButton } from '@material-tailwind/react';
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
import { relativeTimeISO } from '../../../utils/time';
import { GitRepositoryDetails } from '../../../types';
import { useGQLClient } from '../../../context/GQLClientContext';
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
interface ProjectRepoCardProps {
repository: GitRepositoryDetails;
@@ -14,6 +16,7 @@ interface ProjectRepoCardProps {
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
const client = useGQLClient();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState(false);
const { orgSlug } = useParams();
@@ -22,6 +25,7 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
return;
}
setIsLoading(true);
const { addProject } = await client.addProject(orgSlug!, {
name: `${repository.owner!.login}-${repository.name}`,
prodBranch: repository.default_branch!,
@@ -30,7 +34,13 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
template: 'webapp',
});
navigate(`import?projectId=${addProject.id}`);
if (Boolean(addProject)) {
setIsLoading(false);
navigate(`import?projectId=${addProject.id}`);
} else {
setIsLoading(false);
toast.error('Failed to create project');
}
}, [client, repository]);
return (
@@ -38,24 +48,32 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
onClick={createProject}
>
<div>^</div>
<div className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
<GithubIcon />
</div>
<div className="grow">
<div>
<span className="text-black">{repository.full_name}</span>
{repository.visibility === 'private' && (
<Chip
className="normal-case inline ml-6 font-normal"
className="normal-case inline ml-6 font-normal text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
size="sm"
value="Private"
icon={'^'}
icon={<LockIcon />}
/>
)}
</div>
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
</div>
<div className="hidden group-hover:block">
<IconButton size="sm">{'>'}</IconButton>
</div>
{isLoading ? (
<Spinner className="h-4 w-4" />
) : (
<div className="hidden group-hover:block">
<IconButton size="sm" placeholder={''}>
{'>'}
</IconButton>
</div>
)}
</div>
);
};
@@ -9,6 +9,7 @@ import SearchBar from '../../SearchBar';
import ProjectRepoCard from './ProjectRepoCard';
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
import AsyncSelect from '../../shared/AsyncSelect';
import { GithubIcon } from 'components/shared/CustomIcon';
const DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5;
@@ -108,7 +109,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
return (
<div className="p-4">
<div className="flex gap-2 mb-2">
<div className="flex gap-2 mb-2 items-center">
<div className="basis-1/3">
<AsyncSelect
value={selectedAccount}
@@ -116,12 +117,14 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
>
{accounts.map((account) => (
<Option key={account.id} value={account.login}>
^ {account.login}
<div className="flex items-center gap-2 justify-start">
<GithubIcon /> {account.login}
</div>
</Option>
))}
</AsyncSelect>
</div>
<div className="basis-2/3">
<div className="basis-2/3 flex-grow flex items-center">
<SearchBar
value={searchedRepo}
onChange={(event) => setSearchedRepo(event.target.value)}
@@ -136,11 +139,12 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
) : (
<div className="mt-4 p-6 flex items-center justify-center">
<div className="text-center">
<Typography>No repository found</Typography>
<Typography placeholder={''}>No repository found</Typography>
<Button
className="rounded-full mt-5"
size="sm"
onClick={handleResetFilters}
placeholder={''}
>
^ Reset filters
</Button>
@@ -1,49 +0,0 @@
import React from 'react';
import toast from 'react-hot-toast';
import { IconButton, Typography } from '@material-tailwind/react';
import { Link } from 'react-router-dom';
interface TemplateDetails {
id: string;
name: string;
icon: string;
}
interface TemplateCardProps {
template: TemplateDetails;
isGitAuth: boolean;
}
const CardDetails = ({ template }: { template: TemplateDetails }) => {
return (
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
<Typography className="grow">
{template.icon} {template.name}
</Typography>
<div>
<IconButton size="sm" className="rounded-full hidden group-hover:block">
{'>'}
</IconButton>
</div>
</div>
);
};
const TemplateCard: React.FC<TemplateCardProps> = ({ template, isGitAuth }) => {
return isGitAuth ? (
<Link to={`template?templateId=${template.id}`}>
<CardDetails template={template} />
</Link>
) : (
<a
onClick={() =>
toast.error('Connect Git account to start with a template')
}
>
<CardDetails template={template} />
</a>
);
};
export default TemplateCard;
@@ -0,0 +1,91 @@
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleIcon,
ClockOutlineIcon,
TemplateIcon,
TemplateIconType,
} from 'components/shared/CustomIcon';
import { Tag } from 'components/shared/Tag';
import { useNavigate, useParams } from 'react-router-dom';
import { useToast } from 'components/shared/Toast';
import { cn } from 'utils/classnames';
export interface TemplateDetail {
id: string;
name: string;
icon: string;
repoFullName?: string;
isComingSoon?: boolean;
}
export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> {
template: TemplateDetail;
isGitAuth: boolean;
}
export const TemplateCard: React.FC<TemplateCardProps> = ({
template,
isGitAuth,
}: TemplateCardProps) => {
const { toast, dismiss } = useToast();
const navigate = useNavigate();
const { orgSlug } = useParams();
const handleClick = useCallback(() => {
if (template?.isComingSoon) {
return toast({
id: 'coming-soon',
title: 'This template is coming soon',
variant: 'info',
onDismiss: dismiss,
});
}
if (isGitAuth) {
return navigate(
`/${orgSlug}/projects/create/template?templateId=${template.id}`,
);
}
return toast({
id: 'connect-git-account',
title: 'Connect Git account to start with a template',
variant: 'error',
onDismiss: dismiss,
});
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
return (
<button
className={cn(
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative',
{
'cursor-default': template?.isComingSoon,
},
)}
onClick={handleClick}
>
{/* Icon */}
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
<TemplateIcon type={template.icon as TemplateIconType} />
</div>
{/* Name */}
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
{template.name}
</p>
{template?.isComingSoon ? (
<Tag size="xs" type="neutral" leftIcon={<ClockOutlineIcon />}>
Soon
</Tag>
) : (
<Button
variant="tertiary"
size="sm"
iconOnly
className="group-hover:flex hidden absolute right-3"
>
<ArrowRightCircleIcon />
</Button>
)}
</button>
);
};
@@ -0,0 +1 @@
export * from './TemplateCard';
@@ -1,36 +0,0 @@
import React from 'react';
import { Typography, IconButton } from '@material-tailwind/react';
import { relativeTimeISO } from '../../../utils/time';
import { GitCommitWithBranch } from '../../../types';
interface ActivityCardProps {
activity: GitCommitWithBranch;
}
const ActivityCard = ({ activity }: ActivityCardProps) => {
return (
<div className="group flex hover:bg-gray-200 rounded mt-1">
<div className="w-4">^</div>
<div className="grow">
<Typography>{activity.commit.author?.name}</Typography>
<Typography variant="small" color="gray">
{relativeTimeISO(activity.commit.author!.date!)} ^{' '}
{activity.branch.name}
</Typography>
<Typography variant="small" color="gray">
{activity.commit.message}
</Typography>
</div>
<div className="mr-2 self-center hidden group-hover:block">
<IconButton size="sm" className="rounded-full bg-gray-600">
{'>'}
</IconButton>
</div>
</div>
);
};
export default ActivityCard;
@@ -17,9 +17,9 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader>Assign Domain</DialogHeader>
<DialogBody>
<Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader placeholder={''}>Assign Domain</DialogHeader>
<DialogBody placeholder={''}>
In order to assign a domain to your production deployments, configure it
in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */}
@@ -36,12 +36,13 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
theme={atomOneLight}
/>
</DialogBody>
<DialogFooter className="flex justify-start">
<DialogFooter className="flex justify-start" placeholder={''}>
<Button
className="rounded-3xl"
variant="gradient"
color="blue"
onClick={handleOpen}
placeholder={''}
>
<span>Okay</span>
</Button>
@@ -16,6 +16,7 @@ import {
Typography,
Chip,
ChipProps,
Tooltip,
} from '@material-tailwind/react';
import { relativeTimeMs } from '../../../../utils/time';
@@ -24,6 +25,7 @@ import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
import AssignDomainDialog from './AssignDomainDialog';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import { formatAddress } from '../../../../utils/format';
interface DeployDetailsCardProps {
deployment: Deployment;
@@ -87,52 +89,65 @@ const DeploymentDetailsCard = ({
};
return (
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
<div className="col-span-2">
<div className="grid grid-cols-8 gap-2 border-b border-gray-300 p-3 my-2">
<div className="col-span-3">
<div className="flex">
{deployment.url && (
<Typography className=" basis-3/4">{deployment.url}</Typography>
<Typography className="basis-3/4" placeholder={''}>
{deployment.url}
</Typography>
)}
<Chip
value={deployment.status}
color={STATUS_COLORS[deployment.status] ?? 'gray'}
variant="ghost"
icon={<i>^</i>}
/>
</div>
<Typography color="gray">
<Typography color="gray" placeholder={''}>
{deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'}
</Typography>
</div>
<div className="col-span-1">
<Typography color="gray">^ {deployment.branch}</Typography>
<Typography color="gray">
<Chip
value={deployment.status}
color={STATUS_COLORS[deployment.status] ?? 'gray'}
variant="ghost"
icon={<i>^</i>}
/>
</div>
<div className="col-span-2">
<Typography color="gray" placeholder={''}>
^ {deployment.branch}
</Typography>
<Typography color="gray" placeholder={''}>
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
{deployment.commitMessage}
</Typography>
</div>
<div className="col-span-1 flex items-center">
<Typography color="gray" className="grow">
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
<div className="col-span-2 flex items-center">
<Typography color="gray" className="grow" placeholder={''}>
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
<Tooltip content={deployment.createdBy.name}>
{formatAddress(deployment.createdBy.name ?? '')}
</Tooltip>
</Typography>
<Menu placement="bottom-start">
<MenuHandler>
<button className="self-start">...</button>
</MenuHandler>
<MenuList>
<MenuList placeholder={''}>
<a href={deployment.url} target="_blank" rel="noreferrer">
<MenuItem disabled={!Boolean(deployment.url)}>^ Visit</MenuItem>
<MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
^ Visit
</MenuItem>
</a>
<MenuItem
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
placeholder={''}
>
^ Assign domain
</MenuItem>
<MenuItem
onClick={() => setChangeToProduction(!changeToProduction)}
disabled={!(deployment.environment !== Environment.Production)}
placeholder={''}
>
^ Change to production
</MenuItem>
@@ -145,6 +160,7 @@ const DeploymentDetailsCard = ({
deployment.isCurrent
)
}
placeholder={''}
>
^ Redeploy to production
</MenuItem>
@@ -155,6 +171,7 @@ const DeploymentDetailsCard = ({
deployment.environment !== Environment.Production ||
!Boolean(currentDeployment)
}
placeholder={''}
>
^ Rollback to this version
</MenuItem>
@@ -173,17 +190,22 @@ const DeploymentDetailsCard = ({
}}
>
<div className="flex flex-col gap-2">
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Upon confirmation, this deployment will be changed to production.
</Typography>
<DeploymentDialogBodyCard deployment={deployment} />
<Typography variant="small">
<Typography variant="small" placeholder={''}>
The new deployment will be associated with these domains:
</Typography>
{prodBranchDomains.length > 0 &&
prodBranchDomains.map((value) => {
return (
<Typography variant="small" color="blue" key={value.id}>
<Typography
variant="small"
color="blue"
key={value.id}
placeholder={''}
>
^ {value.name}
</Typography>
);
@@ -202,16 +224,16 @@ const DeploymentDetailsCard = ({
}}
>
<div className="flex flex-col gap-2">
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Upon confirmation, new deployment will be created with the same
source code as current deployment.
</Typography>
<DeploymentDialogBodyCard deployment={deployment} />
<Typography variant="small">
<Typography variant="small" placeholder={''}>
These domains will point to your new deployment:
</Typography>
{deployment.domain?.name && (
<Typography variant="small" color="blue">
<Typography variant="small" color="blue" placeholder={''}>
{deployment.domain?.name}
</Typography>
)}
@@ -230,7 +252,7 @@ const DeploymentDetailsCard = ({
}}
>
<div className="flex flex-col gap-2">
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Upon confirmation, this deployment will replace your current
deployment
</Typography>
@@ -248,10 +270,10 @@ const DeploymentDetailsCard = ({
color: 'orange',
}}
/>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
These domains will point to your new deployment:
</Typography>
<Typography variant="small" color="blue">
<Typography variant="small" color="blue" placeholder={''}>
^ {currentDeployment.domain?.name}
</Typography>
</div>
@@ -5,6 +5,7 @@ import { Typography, Chip, Card } from '@material-tailwind/react';
import { color } from '@material-tailwind/react/types/components/chip';
import { relativeTimeMs } from '../../../../utils/time';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import { formatAddress } from '../../../../utils/format';
interface DeploymentDialogBodyCardProps {
deployment: Deployment;
@@ -19,7 +20,7 @@ const DeploymentDialogBodyCard = ({
deployment,
}: DeploymentDialogBodyCardProps) => {
return (
<Card className="p-2 shadow-none">
<Card className="p-2 shadow-none" placeholder={''}>
{chip && (
<Chip
className={`w-fit normal-case font-normal`}
@@ -29,17 +30,18 @@ const DeploymentDialogBodyCard = ({
/>
)}
{deployment.url && (
<Typography variant="small" className="text-black">
<Typography variant="small" className="text-black" placeholder={''}>
{deployment.url}
</Typography>
)}
<Typography variant="small">
<Typography variant="small" placeholder={''}>
^ {deployment.branch} ^{' '}
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
{deployment.commitMessage}
</Typography>
<Typography variant="small">
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
<Typography variant="small" placeholder={''}>
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
{formatAddress(deployment.createdBy.name ?? '')}
</Typography>
</Card>
);
@@ -44,18 +44,18 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
}, [value]);
return (
<div className="grid grid-cols-4 gap-2 text-sm text-gray-600">
<div className="col-span-2">
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
<div className="col-span-4">
<SearchBar
placeholder="Search branches"
value={searchedBranch}
onChange={(event) => setSearchedBranch(event.target.value)}
/>
</div>
<div className="col-span-1">
<div className="col-span-2">
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
</div>
<div className="col-span-1 relative">
<div className="col-span-2 relative">
<Select
value={selectedStatus}
onChange={(value) => setSelectedStatus(value as StatusOptions)}
@@ -77,6 +77,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
className="rounded-full"
size="sm"
placeholder={''}
>
X
</IconButton>
@@ -0,0 +1,28 @@
import React from 'react';
import { GitCommitWithBranch } from 'types';
import { Heading } from 'components/shared/Heading';
import ActivityCard from './ActivityCard';
import { Button } from 'components/shared/Button';
export const Activity = ({
activities,
}: {
activities: GitCommitWithBranch[];
}) => {
return (
<div className="col-span-2 mr-1">
<div className="flex items-center justify-between">
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
<Button variant="tertiary" size="sm">
See all
</Button>
</div>
<div className="mt-5">
{activities.map((activity, index) => {
return <ActivityCard activity={activity} key={`activity-${index}`} />;
})}
</div>
</div>
);
};
@@ -0,0 +1,86 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { GitCommitWithBranch } from '../../../../../types';
import { Avatar } from 'components/shared/Avatar';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleFilledIcon,
BranchStrokeIcon,
} from 'components/shared/CustomIcon';
import { formatDistance } from 'date-fns';
import { getInitials } from 'utils/geInitials';
interface ActivityCardProps {
activity: GitCommitWithBranch;
}
const ActivityCard = ({ activity }: ActivityCardProps) => {
const formattedDate = formatDistance(
new Date(activity.commit.author!.date!),
new Date(),
{
addSuffix: true,
},
);
return (
<>
<Link
to={activity.html_url}
target="_blank"
className="p-3 gap-2 focus-within:ring-2 focus-within:ring-controls-primary/40 focus:outline-none rounded-xl transition-colors hover:bg-base-bg-alternate flex group"
>
<div>
<Avatar
type="orange"
size={36}
initials={getInitials(activity.commit.author?.name ?? '')}
imageSrc={activity.author?.avatar_url}
/>
</div>
<div className="flex-1">
<span className="text-elements-high-em text-sm font-medium tracking-tight">
{activity.commit.author?.name}
</span>
<span className="text-elements-low-em text-xs flex items-center gap-2">
<span title={formattedDate} className="whitespace-nowrap">
{formattedDate}
</span>
{/* Dot */}
<span className="w-0.5 h-0.5 rounded-full bg-border-interactive-hovered"></span>
<span className="flex justify-center items-center gap-1.5">
<div>
<BranchStrokeIcon className="w-3 h-3" />
</div>
<span
title={activity.branch.name}
// pseudo to increase hover area
className="before:absolute relative before:h-5 before:-top-4 before:inset-x-0"
>
<span className="line-clamp-1">{activity.branch.name}</span>
</span>
</span>
</span>
<p
title={activity.commit.message}
className="text-sm line-clamp-4 tracking-tight text-elements-mid-em mt-2"
>
{activity.commit.message}
</p>
</div>
<Button
aria-label="Go to commit"
variant="unstyled"
tabIndex={-1}
className="p-0 text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 opacity-0 transition-all"
leftIcon={<ArrowRightCircleFilledIcon className="w-5 h-5" />}
/>
</Link>
{/* Separator calc => 100% - 36px (avatar) - 12px (padding-left) - 8px (gap)
*/}
<div className="pt-2 mb-2 ml-auto h-1 w-[calc(100%-36px-12px-8px)] border-b border-border-separator last:border-b-transparent"></div>
</>
);
};
export default ActivityCard;

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