Compare commits

..
Author SHA1 Message Date
Wahyu Kurniawan fcaeabf07d ♻️ refactor: update pr review 2024-02-27 11:51:10 +07:00
Wahyu Kurniawan 1e056d7969 ♻️ refactor: remove console log 2024-02-27 11:50:21 +07:00
Wahyu Kurniawan 6d8723881c ️ feat: make the select to be controller component 2024-02-27 11:50:21 +07:00
Wahyu Kurniawan d387229e80 🐛 fix: no result not showing 2024-02-27 11:50:21 +07:00
Wahyu Kurniawan 54c2e8ec0b ️ feat: add hideValues prop to hide the values when on multiple 2024-02-27 11:50:21 +07:00
Wahyu Kurniawan 410def0750 🐛 fix: forward ref the component to fix console error 2024-02-27 11:50:20 +07:00
Wahyu Kurniawan d181e6ba98 ️ feat: create a type for merge two interface but keep the last value 2024-02-27 11:50:20 +07:00
Wahyu Kurniawan 100e03b7a4 ♻️ refactor: adjust style and refactor to a new component 2024-02-27 11:50:20 +07:00
Wahyu Kurniawan 5d0dd93271 ️ feat: separate select value to be a component 2024-02-27 11:50:20 +07:00
Wahyu Kurniawan 3bca06b590 ️ feat: separate select item to be a component 2024-02-27 11:50:20 +07:00
Wahyu Kurniawan c950bd8d93 🎨 style: adjust the popover position based on the user screen 2024-02-27 11:50:20 +07:00
Wahyu Kurniawan b7fe301c81 ️ feat: create select component 2024-02-27 11:50:19 +07:00
Wahyu Kurniawan 3cf8769259 🔧 chore: install downshift 2024-02-27 11:50:19 +07:00
Wahyu Kurniawan 35bc69bb15 ️ feat: create check radio icon and chevron down icon component 2024-02-27 11:50:02 +07:00
Wahyu Kurniawan 5451c3be6f 🚀 perf: remove vscode settings inside packages/frontend 2024-02-27 11:49:52 +07:00
Wahyu Kurniawan 5f8afab020 ♻️ refactor: rename shadow calendar to shador dropdown on calendar component 2024-02-27 11:49:51 +07:00
Wahyu Kurniawan 0bcb7ca4d6 🐛 fix: console error becasue button inside button 2024-02-27 11:49:51 +07:00
Wahyu Kurniawan cfeb85f5c4 🎨 style: add new spacing and rename box shadow from calendar to dropdown 2024-02-27 11:49:51 +07:00
Wahyu Kurniawan 705516f5e3 🎨 style: adjust z index of the date picker popover 2024-02-27 11:49:50 +07: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
Wahyu Kurniawan d2ca4df35a [T-4864: feat] Radio component (#90)
* 🎨 style: make the cursor of the tab trigger wrapper to default

* ️ feat: create radio component

* 📝 docs: add radio component to the example page

* 🔧 chore: install `@radix-ui/react-radio-group`
2024-02-22 17:30:33 +07:00
Zachery 30bbe4d766 [T-4870] Tooltip component (#96)
* fix: button forwardRef

* feat: tooltip component
2024-02-22 18:25:04 +08: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
Wahyu Kurniawan 496404195c Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into andrehadianto/design-system-components 2024-02-22 12:45:17 +07:00
Wahyu Kurniawan 9dec48d67c ♻️ refactor: add disabled button example page 2024-02-22 12:24:25 +07:00
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
nabarunandneeraj e816c596ca Publish ApplicationRecord and ApplicationDeploymentRequest on creating new deployments (#89)
* Create application deployment request on new deployment

* Save application deployment request datas in deployment entity

* Add project as dependency

* Use project repo name as record app name

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-22 10:04:33 +05:30
Wahyu Kurniawan 6df094bf2e [T-4861: feat] Inline notification component (#86)
* ️ feat: create info square icon component

* ️ feat: create inline notification component

* 📝 docs: add js doc comment and add inline notification component to the example page

* 🐛 fix: use the right method of `useCallback
2024-02-22 10:45:19 +07:00
Andre Hadianto 2369f4498a Merge pull request #85 from snowball-tools/andrehadianto/T-4863-input-field
feat: input field
2024-02-22 10:18:59 +08:00
Wahyu Kurniawan eb6a727425 [T-4862: feat] Link component (#88) 2024-02-22 09:17:22 +07:00
Andre H da2f7ede42 🔧 chore: add dependency to usememo 2024-02-22 09:15:17 +07:00
telackey 4d0f2ca893 SO webapp-ify the frontend (#82)
* webapp-ify

* Tweak values

* Self-contained
2024-02-21 13:53:05 -06:00
Andre H 448d0ceb7c 🔧 chore: 1 classname per object 2024-02-21 17:10:45 +07:00
Andre H 195d957251 🔧 chore: restructure tailwind classnames 2024-02-21 17:08:33 +07:00
Andre H 807a4b1197 🔧 chore: lint 2024-02-21 17:08:14 +07:00
Andre H 5f6723fad6 ♻️ refactor: create new renderInput for component page 2024-02-21 17:07:59 +07:00
Andre H b663067035 🔧 chore: use absolute import 2024-02-21 17:07:30 +07:00
nabarunandneeraj 8ca55cd888 Implement polling for project deployment updates (#87)
* Populate organization and user if db is empty

* Use hardcoded user id from fixtures

* Implement polling for get deployments query

* Handle review changes

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-21 15:34:33 +05:30
Andre Hadianto b3a99475af Merge branch 'andrehadianto/design-system-components' into andrehadianto/T-4863-input-field 2024-02-21 17:39:29 +08:00
Wahyu Kurniawan ea44efa0f2 [T-4867: feat] Horizontal and vertical tabs component (#84)
* ️ feat: create tabs component

* ♻️ refactor: avoid big conflict on the example page

* 🔧 chore: upgrade tailwindcss and install `@radix-ui/react-tabs`

* ️ feat: create globe icon component

* 🎨 style: adjust vertical tab theme

* 📝 docs: add vertical tabs to the example page
2024-02-21 16:13:16 +07:00
Andre H 7d5963d776 🔧 chore: add input to page/components 2024-02-21 15:44:21 +07:00
Andre H 33b6191539 ️ feat: implement Input Field component 2024-02-21 15:43:53 +07:00
Andre H 636f68d7a4 🔧 chore: add more icons 2024-02-21 15:43:32 +07:00
Andre H 237e9e5cb9 🔧 chore: implement classnames utils 2024-02-21 15:43:09 +07:00
Andre H e70bb34190 ⚒️ build: reorder packages 2024-02-21 15:42:35 +07:00
telackey fc240c93d8 SO deployable config for the backend. (#83)
* webapp-ify

* SO deployable config for the backend.

* Use process.env

* Detangle
2024-02-21 00:05:45 -06:00
Andre H b5eef95d15 Merge remote-tracking branch 'origin/main' 2024-02-21 11:00:14 +07:00
Zachery f0121605c4 feat: avatar component (#76) 2024-02-21 11:39:38 +08:00
Wahyu Kurniawan c8f1c58507 Merge pull request #81 from snowball-tools/ayungavis/T-4837-badge
[T-4837: feat] Badge component
2024-02-20 23:42:56 +07:00
Wahyu Kurniawan 83508cd81d Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4837-badge 2024-02-20 23:38:35 +07:00
Wahyu Kurniawan 76c59e2c58 Merge pull request #80 from snowball-tools/ayungavis/T-4838-checkbox
[T-4838: feat] Checkbox component
2024-02-20 23:37:30 +07:00
Wahyu Kurniawan a5bce0cda2 🎨 style: adjust disabled hover text color 2024-02-20 23:34:20 +07:00
Wahyu Kurniawan 3a085a79ae Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4838-checkbox 2024-02-20 23:29:18 +07:00
Wahyu Kurniawan abfe6a523c Merge pull request #79 from snowball-tools/ayungavis/T-4842-datepicker
[T-4842: feat] Calendar component
2024-02-20 23:26:38 +07:00
Wahyu Kurniawan f764bea6d1 📝 docs: add badge to the example page 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan cc9c24cca1 🎨 style: add new font sizes 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan e482f998a1 ️ feat: create badge comopnent 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan b255078431 📝 docs: add checkbox to example component page 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 26260976fb ️ feat: create check icon for checkbox component 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 5e3a6ad2b5 ️ feat: create checkbox component 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan f31ee7349f 🔧 chore: install @radix-ui/react-checkbox 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 5c5d759c10 ♻️ refactor: fix typo and change to readable name 2024-02-20 23:24:28 +07:00
Wahyu Kurniawan dc6bf3794a ️ feat: add interaction on the example page 2024-02-20 23:24:28 +07:00
Wahyu Kurniawan af0d28e654 🎨 style: add new shadows 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 4f7f9cf914 📝 docs: add calendar component to the example page 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan ad7dd1920a ️ feat: create calendar component 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan cc97ddff9d ️ feat: create chevron left, right, and grabbed horizontal component 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 0a5a1b8c55 🔧 chore: add vscode settings to enable tailwind variant intellisense 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 4f2944a974 🔧 chore: install date-fns and react-calendar 2024-02-20 23:24:26 +07:00
Wahyu Kurniawan 69198307fb Merge pull request #77 from snowball-tools/ayungavis/T-4834-button
[T-4834: feat] Button component
2024-02-20 23:23:38 +07:00
nabarunandneeraj a45fb4c617 Fix deployments visit URL and date filter (#78)
* Add method to update multiple deployments in single query

* Fix deployments URL visit and date filter

* Clean fixtures data

* Update fixtures data

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-20 10:53:42 +05:30
nabarunandneeraj c3d1b4f3eb Use branches from GitHub API in edit domain dialog (#74)
* Use branches from GitHub API

* Disable git branch input if repo not found

* Disable git branch if branches is empty

* Use async select for accounts dropdown

* Log actual HTTP error in console

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-19 16:55:07 +05:30
nabarunandneeraj ce55fe62d8 Check for ApplicationDeploymentRecord and update deployments (#73)
* Add skeleton and TODOs for polling deployment records

* Add method implementations for fetching deployment records

* Handle if deployment url is not set

* Add logs after getting ApplicationDeploymentRecord

* Add application deployment record to deployment entity

* Change type of application deployment record data

* Fetch delay to check deployment records from config

* Update isCurrent after deployment record received

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-19 13:43:29 +05:30
nabarunandneeraj b007286f4a Add script to publish dummy ApplicationDeploymentRecord (#72)
* Add script to publish application deployment record

* Refactor publish deploy records script

* Assert if name doesn't exist in package.json

* Disable import environment variables button

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-16 15:05:52 +05:30
246 changed files with 13739 additions and 4262 deletions
+6
View File
@@ -1 +1,7 @@
node_modules/
yarn-error.log
.yarnrc.yml
.yarn/
.yarnrc
packages/backend/environments/local.toml
+6
View File
@@ -0,0 +1,6 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+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 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 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/`
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
PKG_DIR="./packages/frontend"
OUTPUT_DIR="${PKG_DIR}/build"
DEST_DIR=${1:-/data}
if [[ -d "$DEST_DIR" ]]; then
echo "${DEST_DIR} already exists." 1>&2
exit 1
fi
if [[ -f "$PKG_DIR/.env" ]]; then
echo "Using existing .env file"
else
mv "$PKG_DIR/.env.example" "$PKG_DIR/.env"
echo "Created .env file. Please populate with the correct values."
exit 1
fi
cat > $PKG_DIR/.env <<EOF
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
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
yarn build || exit 1
if [[ ! -d "$OUTPUT_DIR" ]]; then
echo "Missing output directory: $OUTPUT_DIR" 1>&2
exit 1
fi
mv "$OUTPUT_DIR" "$DEST_DIR"
+4 -1
View File
@@ -25,6 +25,9 @@
"allowArgumentsExplicitlyTypedAsAny": true
}
],
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
"@typescript-eslint/no-unused-vars": [
"error",
{ "ignoreRestSiblings": true }
]
}
}
+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"
@@ -13,12 +18,17 @@
clientSecret = ""
[registryConfig]
fetchDeploymentRecordDelay = 5000
restEndpoint = "http://localhost:1317"
gqlEndpoint = "http://localhost:9473/api"
chainId = "laconic_9000-1"
privateKey = ""
bondId = ""
authority = ""
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "550000"
gas = "750000"
[misc]
projectDomain = "apps.snowballtools.com"
+7 -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",
@@ -36,11 +38,13 @@
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
"db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
"db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
"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",
+20 -2
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 {
@@ -13,7 +24,7 @@ export interface GitHubConfig {
oAuth: {
clientId: string;
clientSecret: string;
}
};
}
export interface RegistryConfig {
@@ -22,11 +33,17 @@ export interface RegistryConfig {
chainId: string;
privateKey: string;
bondId: string;
fetchDeploymentRecordDelay: number;
authority: string;
fee: {
amount: string;
denom: string;
gas: string;
}
};
}
export interface MiscConfig {
projectDomain: string;
}
export interface Config {
@@ -34,4 +51,5 @@ export interface Config {
database: DatabaseConfig;
gitHub: GitHubConfig;
registryConfig: RegistryConfig;
misc: MiscConfig;
}
+4 -6
View File
@@ -1,8 +1,6 @@
export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml';
import process from 'process';
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 = '59f4355d-9549-4aac-9b54-eeefceeabef0';
export const PROJECT_DOMAIN = 'snowball.xyz';
+196 -62
View File
@@ -1,11 +1,17 @@
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
import {
DataSource,
DeepPartial,
FindManyOptions,
FindOneOptions,
FindOptionsWhere
} from 'typeorm';
import path from 'path';
import debug from 'debug';
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';
@@ -13,7 +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 log = debug('snowball:database');
@@ -22,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,
@@ -31,11 +41,21 @@ export class Database {
synchronize: true,
logging: false
});
this.projectDomain = projectDomain;
}
async init (): Promise<void> {
await this.dataSource.initialize();
log('database initialized');
const organizations = await this.getOrganizations({});
// Load an organization if none exist
if (!organizations.length) {
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
}
}
async getUser (options: FindOneOptions<User>): Promise<User | null> {
@@ -52,15 +72,26 @@ 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;
}
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
async getOrganizations (
options: FindManyOptions<Organization>
): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organizations = await organizationRepository.find(options);
return organizations;
}
async getOrganization (
options: FindOneOptions<Organization>
): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options);
@@ -83,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);
@@ -95,7 +133,11 @@ export class Database {
const project = await projectRepository
.createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
.leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.createdBy', 'user')
.leftJoinAndSelect('deployments.domain', 'domain')
.leftJoinAndSelect('project.owner', 'owner')
@@ -108,28 +150,48 @@ export class Database {
return project;
}
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
async getProjectsInOrganization (
userId: string,
organizationSlug: string
): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository
.createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
.leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.domain', 'domain')
.leftJoin('project.projectMembers', 'projectMembers')
.leftJoin('project.organization', 'organization')
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
userId,
organizationSlug
})
.where(
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
{
userId,
organizationSlug
}
)
.getMany();
return projects;
}
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
/**
* Get deployments with specified filter
*/
async getDeployments (
options: FindManyOptions<Deployment>
): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find(options);
const deployments = await deploymentRepository.find({
return deployments;
}
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
return this.getDeployments({
relations: {
project: true,
domain: true,
@@ -144,11 +206,11 @@ export class Database {
createdAt: 'DESC'
}
});
return deployments;
}
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
async getDeployment (
options: FindOneOptions<Deployment>
): Promise<Deployment | null> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOne(options);
@@ -162,24 +224,25 @@ export class Database {
return domains;
}
async addDeployement (data: DeepPartial<Deployment>): Promise<Deployment> {
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const id = nanoid();
const url = `${data.project!.name}-${id}.${PROJECT_DOMAIN}`;
const updatedData = {
...data,
id,
url
id
};
const deployment = await deploymentRepository.save(updatedData);
return deployment;
}
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
async getProjectMembersByProjectId (
projectId: string
): Promise<ProjectMember[]> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMembers = await projectMemberRepository.find({
relations: {
@@ -196,8 +259,12 @@ export class Database {
return projectMembers;
}
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
async getEnvironmentVariablesByProjectId (
projectId: string,
filter?: FindOptionsWhere<EnvironmentVariable>
): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const environmentVariables = await environmentVariableRepository.find({
where: {
@@ -212,9 +279,12 @@ export class Database {
}
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const deleteResult = await projectMemberRepository.delete({ id: projectMemberId });
const deleteResult = await projectMemberRepository.delete({
id: projectMemberId
});
if (deleteResult.affected) {
return deleteResult.affected > 0;
@@ -223,37 +293,63 @@ export class Database {
}
}
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
async updateProjectMemberById (
projectMemberId: string,
data: DeepPartial<ProjectMember>
): Promise<boolean> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const updateResult = await projectMemberRepository.update(
{ id: projectMemberId },
data
);
return Boolean(updateResult.affected);
}
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
async addProjectMember (
data: DeepPartial<ProjectMember>
): Promise<ProjectMember> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const newProjectMember = await projectMemberRepository.save(data);
return newProjectMember;
}
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
async addEnvironmentVariables (
data: DeepPartial<EnvironmentVariable>[]
): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const savedEnvironmentVariables =
await environmentVariableRepository.save(data);
return savedEnvironmentVariables;
}
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
async updateEnvironmentVariable (
environmentVariableId: string,
data: DeepPartial<EnvironmentVariable>
): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const updateResult = await environmentVariableRepository.update(
{ id: environmentVariableId },
data
);
return Boolean(updateResult.affected);
}
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
async deleteEnvironmentVariable (
environmentVariableId: string
): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const deleteResult = await environmentVariableRepository.delete({
id: environmentVariableId
});
if (deleteResult.affected) {
return deleteResult.affected > 0;
@@ -263,7 +359,8 @@ export class Database {
}
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMemberWithProject = await projectMemberRepository.find({
relations: {
@@ -275,8 +372,7 @@ export class Database {
where: {
id: projectMemberId
}
}
);
});
if (projectMemberWithProject.length === 0) {
throw new Error('Member does not exist');
@@ -285,34 +381,62 @@ export class Database {
return projectMemberWithProject[0];
}
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
async getProjectsBySearchText (
userId: string,
searchText: string
): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository
.createQueryBuilder('project')
.leftJoinAndSelect('project.organization', 'organization')
.leftJoin('project.projectMembers', 'projectMembers')
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
userId,
searchText: `%${searchText}%`
})
.where(
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
{
userId,
searchText: `%${searchText}%`
}
)
.getMany();
return projects;
}
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
async updateDeploymentById (
deploymentId: string,
data: DeepPartial<Deployment>
): Promise<boolean> {
return this.updateDeployment({ id: deploymentId }, data);
}
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
async updateDeployment (
criteria: FindOptionsWhere<Deployment>,
data: DeepPartial<Deployment>
): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository.update(criteria, data);
return Boolean(updateResult.affected);
}
async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
async updateDeploymentsByProjectIds (
projectIds: string[],
data: DeepPartial<Deployment>
): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository
.createQueryBuilder()
.update(Deployment)
.set(data)
.where('projectId IN (:...projectIds)', { projectIds })
.execute();
return Boolean(updateResult.affected);
}
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project);
// TODO: Check if organization exists
@@ -322,22 +446,26 @@ 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);
}
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
async updateProjectById (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project);
const updateResult = await projectRepository.update({ id: projectId }, data);
const updateResult = await projectRepository.update(
{ id: projectId },
data
);
return Boolean(updateResult.affected);
}
@@ -384,14 +512,20 @@ export class Database {
return domain;
}
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
async updateDomainById (
domainId: string,
data: DeepPartial<Domain>
): Promise<boolean> {
const domainRepository = this.dataSource.getRepository(Domain);
const updateResult = await domainRepository.update({ id: domainId }, data);
return Boolean(updateResult.affected);
}
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
async getDomainsByProjectId (
projectId: string,
filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> {
const domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find({
+39 -14
View File
@@ -12,6 +12,7 @@ import {
import { Project } from './Project';
import { Domain } from './Domain';
import { User } from './User';
import { AppDeploymentRecordAttributes } from '../types';
export enum Environment {
Production = 'Production',
@@ -25,18 +26,27 @@ export enum DeploymentStatus {
Error = 'Error',
}
export interface ApplicationDeploymentRequest {
type: string;
version: string;
name: string;
application: string;
config: string;
meta: string;
}
export interface ApplicationRecord {
type: string;
version:string
name?: string
description?: string
homepage?: string
license?: string
author?: string
repository?: string[],
app_version?: string
repository_ref: string
app_type: string
version: string;
name: string;
description?: string;
homepage?: string;
license?: string;
author?: string;
repository?: string[];
app_version?: string;
repository_ref: string;
app_type: string;
}
@Entity()
@@ -45,6 +55,9 @@ export class Deployment {
@PrimaryColumn('varchar')
id!: string;
@Column()
projectId!: string;
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'projectId' })
project!: Project;
@@ -65,14 +78,26 @@ export class Deployment {
@Column('varchar')
commitMessage!: string;
@Column('varchar')
url!: string;
@Column('varchar', { nullable: true })
url!: string | null;
@Column('varchar')
registryRecordId!: string;
applicationRecordId!: string;
@Column('simple-json')
registryRecordData!: ApplicationRecord;
applicationRecordData!: ApplicationRecord;
@Column('varchar', { nullable: true })
applicationDeploymentRequestId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
@Column('varchar', { nullable: true })
applicationDeploymentRecordId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
@Column({
enum: Environment
+1 -1
View File
@@ -39,7 +39,7 @@ export class Domain {
@ManyToOne(() => Domain)
@JoinColumn({ name: 'redirectToId' })
// eslint-disable-next-line no-use-before-define
// eslint-disable-next-line no-use-before-define
redirectTo!: Domain | null;
@Column({
+7 -3
View File
@@ -27,8 +27,12 @@ export class Organization {
@UpdateDateColumn()
updatedAt!: Date;
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
cascade: ['soft-remove']
})
@OneToMany(
() => UserOrganization,
(userOrganization) => userOrganization.organization,
{
cascade: ['soft-remove']
}
)
userOrganizations!: UserOrganization[];
}
+1 -16
View File
@@ -15,15 +15,6 @@ import { Organization } from './Organization';
import { ProjectMember } from './ProjectMember';
import { Deployment } from './Deployment';
export interface ApplicationDeploymentRequest {
type: string
version: string
name: string
application: string
config: string,
meta: string
}
@Entity()
export class Project {
@PrimaryGeneratedColumn('uuid')
@@ -52,12 +43,6 @@ export class Project {
@Column('varchar', { length: 255, default: 'main' })
prodBranch!: string;
@Column('varchar', { nullable: true })
registryRecordId!: string | null;
@Column('simple-json', { nullable: true })
registryRecordData!: ApplicationDeploymentRequest | null;
@Column('text', { default: '' })
description!: string;
@@ -91,7 +76,7 @@ export class Project {
@OneToMany(() => Deployment, (deployment) => deployment.project)
deployments!: Deployment[];
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove']
})
projectMembers!: ProjectMember[];
+1 -1
View File
@@ -15,7 +15,7 @@ import { User } from './User';
export enum Permission {
View = 'View',
Edit = 'Edit'
Edit = 'Edit',
}
@Entity()
+13 -4
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;
@@ -34,13 +39,17 @@ export class User {
@CreateDateColumn()
updatedAt!: Date;
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove']
})
projectMembers!: ProjectMember[];
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
cascade: ['soft-remove']
})
@OneToMany(
() => UserOrganization,
(UserOrganization) => UserOrganization.member,
{
cascade: ['soft-remove']
}
)
userOrganizations!: UserOrganization[];
}
@@ -12,7 +12,7 @@ import {
import { User } from './User';
import { Organization } from './Organization';
enum Role {
export enum Role {
Owner = 'Owner',
Maintainer = 'Maintainer',
Reader = 'Reader',
+11 -4
View File
@@ -19,7 +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,
@@ -27,13 +27,20 @@ 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);
const service = new Service({ gitHubConfig: gitHub }, db, app, registry);
const service = new Service(
{ gitHubConfig: gitHub, registryConfig },
db,
app,
registry
);
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
const typeDefs = fs
.readFileSync(path.join(__dirname, 'schema.gql'))
.toString();
const resolvers = await createResolvers(service);
await createAndStartServer(server, typeDefs, resolvers, service);
+111 -40
View File
@@ -6,48 +6,72 @@ import { DateTime } from 'luxon';
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config';
import { ApplicationDeploymentRequest } from './entity/Project';
import { ApplicationRecord } from './entity/Deployment';
import { PackageJSON } from './types';
import {
ApplicationRecord,
Deployment,
ApplicationDeploymentRequest
} from './entity/Deployment';
import { AppDeploymentRecord, PackageJSON } from './types';
const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord';
const DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
// TODO: Move registry code to laconic-sdk/watcher-ts
export class Registry {
private registry: LaconicRegistry;
private registryConfig: RegistryConfig;
constructor (registryConfig : RegistryConfig) {
constructor (registryConfig: RegistryConfig) {
this.registryConfig = registryConfig;
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
this.registry = new LaconicRegistry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
}
async createApplicationRecord ({
appName,
packageJSON,
commitHash,
appType,
repoUrl
}: {
packageJSON: PackageJSON
commitHash: string,
appType: string,
repoUrl: string
}): Promise<{registryRecordId: string, registryRecordData: ApplicationRecord}> {
appName: string;
packageJSON: PackageJSON;
commitHash: string;
appType: string;
repoUrl: string;
}): Promise<{
applicationRecordId: string;
applicationRecordData: ApplicationRecord;
}> {
// Use laconic-sdk to publish record
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
// Fetch previous records
const records = await this.registry.queryRecords({
type: APP_RECORD_TYPE,
name: packageJSON.name
}, true);
const records = await this.registry.queryRecords(
{
type: APP_RECORD_TYPE,
name: packageJSON.name
},
true
);
// Get next version of record
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
const bondRecords = records.filter(
(record: any) => record.bondId === this.registryConfig.bondId
);
const [latestBondRecord] = bondRecords.sort(
(a: any, b: any) =>
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
);
const nextVersion = semverInc(
latestBondRecord?.attributes.version ?? '0.0.0',
'patch'
);
assert(nextVersion, 'Application record version not valid');
@@ -58,11 +82,16 @@ export class Registry {
repository_ref: commitHash,
repository: [repoUrl],
app_type: appType,
...(packageJSON.name && { name: packageJSON.name }),
name: appName,
...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }),
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }),
...(packageJSON.author && {
author:
typeof packageJSON.author === 'object'
? JSON.stringify(packageJSON.author)
: packageJSON.author
}),
...(packageJSON.version && { app_version: packageJSON.version })
};
@@ -79,24 +108,43 @@ export class Registry {
log('Application record data:', applicationRecord);
// TODO: Discuss computation of CRN
const crn = this.getCrn(packageJSON.name ?? '');
const crn = this.getCrn(appName);
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee);
await this.registry.setName(
{ cid: result.data.id, crn },
this.registryConfig.privateKey,
this.registryConfig.fee
);
await this.registry.setName(
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
this.registryConfig.privateKey,
this.registryConfig.fee
);
await this.registry.setName(
{
cid: result.data.id,
crn: `${crn}@${applicationRecord.repository_ref}`
},
this.registryConfig.privateKey,
this.registryConfig.fee
);
return { registryRecordId: result.data.id, registryRecordData: applicationRecord };
return {
applicationRecordId: result.data.id,
applicationRecordData: applicationRecord
};
}
async createApplicationDeploymentRequest (data: {
deployment: Deployment,
appName: string,
commitHash: string,
packageJsonName: string,
repository: string,
environmentVariables: { [key: string]: string }
}): Promise<{
registryRecordId: string,
registryRecordData: ApplicationDeploymentRequest
applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> {
const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]);
@@ -108,13 +156,13 @@ export class Registry {
// Create record of type ApplicationDeploymentRequest and publish
const applicationDeploymentRequest = {
type: DEPLOYMENT_RECORD_TYPE,
type: APP_DEPLOYMENT_REQUEST_TYPE,
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
@@ -122,9 +170,11 @@ export class Registry {
env: data.environmentVariables
}),
meta: JSON.stringify({
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
note: `Added by Snowball @ ${DateTime.utc().toFormat(
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
)}`,
repository: data.repository,
repository_ref: data.commitHash
repository_ref: data.deployment.commitHash
})
};
@@ -140,17 +190,38 @@ export class Registry {
log(`Application deployment request record published: ${result.data.id}`);
log('Application deployment request data:', applicationDeploymentRequest);
return { registryRecordId: result.data.id, registryRecordData: applicationDeploymentRequest };
return {
applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest
};
}
getCrn (packageJsonName: string): string {
const [arg1, arg2] = packageJsonName.split('/');
/**
* Fetch ApplicationDeploymentRecords for deployments
*/
async getDeploymentRecords (
deployments: Deployment[]
): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
const records = await this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE
},
true
);
if (arg2) {
const authority = arg1.replace('@', '');
return `crn://${authority}/applications/${arg2}`;
}
// Filter records with ApplicationRecord ids
return records.filter((record: AppDeploymentRecord) =>
deployments.some(
(deployment) =>
deployment.applicationRecordId === record.attributes.application
)
);
}
return `crn://${arg1}/applications/${arg1}`;
getCrn (appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist");
return `crn://${this.registryConfig.authority}/applications/${appName}`;
}
}
+125 -39
View File
@@ -14,11 +14,11 @@ 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);
return service.getOrganizationsByUserId(context.user);
},
project: async (_: any, { projectId }: { projectId: string }) => {
@@ -26,14 +26,17 @@ export const createResolvers = async (service: Service): Promise<any> => {
},
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
return service.getProjectsInOrganization(context.userId, organizationSlug);
return service.getProjectsInOrganization(context.user, organizationSlug);
},
deployments: async (_: any, { projectId }: { projectId: string }) => {
return service.getDeployementsByProjectId(projectId);
return service.getDeploymentsByProjectId(projectId);
},
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
environmentVariables: async (
_: any,
{ projectId }: { projectId: string }
) => {
return service.getEnvironmentVariablesByProjectId(projectId);
},
@@ -42,31 +45,47 @@ export const createResolvers = async (service: Service): Promise<any> => {
},
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
return service.searchProjects(context.userId, searchText);
return service.searchProjects(context.user, searchText);
},
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
domains: async (
_: any,
{
projectId,
filter
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
) => {
return service.getDomainsByProjectId(projectId, filter);
}
},
// TODO: Return error in GQL response
Mutation: {
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
removeProjectMember: async (
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any
) => {
try {
return await service.removeProjectMember(context.userId, projectMemberId);
return await service.removeProjectMember(context.user, projectMemberId);
} catch (err) {
log(err);
return false;
}
},
updateProjectMember: async (_: any, { projectMemberId, data }: {
projectMemberId: string,
data: {
permissions: Permission[]
updateProjectMember: async (
_: any,
{
projectMemberId,
data
}: {
projectMemberId: string;
data: {
permissions: Permission[];
};
}
}) => {
) => {
try {
return await service.updateProjectMember(projectMemberId, data);
} catch (err) {
@@ -75,13 +94,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
addProjectMember: async (_: any, { projectId, data }: {
projectId: string,
data: {
email: string,
permissions: Permission[]
addProjectMember: async (
_: any,
{
projectId,
data
}: {
projectId: string;
data: {
email: string;
permissions: Permission[];
};
}
}) => {
) => {
try {
return Boolean(await service.addProjectMember(projectId, data));
} catch (err) {
@@ -90,25 +115,51 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => {
addEnvironmentVariables: async (
_: any,
{
projectId,
data
}: {
projectId: string;
data: { environments: string[]; key: string; value: string }[];
}
) => {
try {
return Boolean(await service.addEnvironmentVariables(projectId, data));
return Boolean(
await service.addEnvironmentVariables(projectId, data)
);
} catch (err) {
log(err);
return false;
}
},
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
updateEnvironmentVariable: async (
_: any,
{
environmentVariableId,
data
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
}
) => {
try {
return await service.updateEnvironmentVariable(environmentVariableId, data);
return await service.updateEnvironmentVariable(
environmentVariableId,
data
);
} catch (err) {
log(err);
return false;
}
},
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string }
) => {
try {
return await service.removeEnvironmentVariable(environmentVariableId);
} catch (err) {
@@ -117,25 +168,39 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
updateDeploymentToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
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;
}
},
addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => {
addProject: async (
_: any,
{
organizationSlug,
data
}: { organizationSlug: string; data: DeepPartial<Project> },
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;
}
},
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
updateProject: async (
_: any,
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
) => {
try {
return await service.updateProject(projectId, data);
} catch (err) {
@@ -144,9 +209,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
redeployToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
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;
@@ -157,7 +226,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
try {
return await service.deleteProject(projectId);
} catch (err) {
log(err); return false;
log(err);
return false;
}
},
@@ -170,7 +240,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
rollbackDeployment: async (
_: any,
{
projectId,
deploymentId
}: { deploymentId: string; projectId: string }
) => {
try {
return await service.rollbackDeployment(projectId, deploymentId);
} catch (err) {
@@ -179,7 +255,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => {
addDomain: async (
_: any,
{ projectId, data }: { projectId: string; data: { name: string } }
) => {
try {
return Boolean(await service.addDomain(projectId, data));
} catch (err) {
@@ -188,7 +267,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => {
updateDomain: async (
_: any,
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
) => {
try {
return await service.updateDomain(domainId, data);
} catch (err) {
@@ -197,9 +279,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
authenticateGitHub: async (
_: any,
{ code }: { code: string },
context: any
) => {
try {
return await service.authenticateGitHub(code, context.userId);
return await service.authenticateGitHub(code, context.user);
} catch (err) {
log(err);
return false;
@@ -208,7 +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;
+13 -4
View File
@@ -91,7 +91,7 @@ type Deployment {
branch: String!
commitHash: String!
commitMessage: String!
url: String!
url: String
environment: Environment!
isCurrent: Boolean!
status: DeploymentStatus!
@@ -188,10 +188,19 @@ type Query {
type Mutation {
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
updateProjectMember(
projectMemberId: String!
data: UpdateProjectMemberInput
): Boolean!
removeProjectMember(projectMemberId: String!): Boolean!
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
addEnvironmentVariables(
projectId: String!
data: [AddEnvironmentVariableInput!]
): Boolean!
updateEnvironmentVariable(
environmentVariableId: String!
data: UpdateEnvironmentVariableInput!
): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(organizationSlug: String!, data: AddProjectInput): Project!
+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, () => {
+355 -142
View File
@@ -14,14 +14,17 @@ import { Project } from './entity/Project';
import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User';
import { Registry } from './registry';
import { GitHubConfig } from './config';
import { GitPushEventPayload } from './types';
import { GitHubConfig, RegistryConfig } from './config';
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
import { Role } from './entity/UserOrganization';
const log = debug('snowball:service');
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
interface Config {
gitHubConfig: GitHubConfig
gitHubConfig: GitHubConfig;
registryConfig: RegistryConfig;
}
export class Service {
@@ -30,11 +33,124 @@ export class Service {
private registry: Registry;
private config: Config;
private deployRecordCheckTimeout?: NodeJS.Timeout;
constructor (config: Config, db: Database, app: OAuthApp, registry: Registry) {
this.db = db;
this.oauthApp = app;
this.registry = registry;
this.config = config;
this.init();
}
/**
* Initialize services
*/
init (): void {
// Start check for ApplicationDeploymentRecords asynchronously
this.checkDeployRecordsAndUpdate();
}
/**
* Destroy services
*/
destroy (): void {
clearTimeout(this.deployRecordCheckTimeout);
}
/**
* Checks for ApplicationDeploymentRecord and update corresponding deployments
* Continues check in loop after a delay of DEPLOY_RECORD_CHECK_DELAY_MS
*/
async checkDeployRecordsAndUpdate (): Promise<void> {
// Fetch deployments in building state
const deployments = await this.db.getDeployments({
where: {
status: DeploymentStatus.Building
// TODO: Fetch and check records for recent deployments
}
});
if (deployments.length) {
log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
);
// Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRecords`);
// Update deployments for which ApplicationDeploymentRecords were returned
if (records.length) {
await this.updateDeploymentsWithRecordData(records);
}
}
this.deployRecordCheckTimeout = setTimeout(() => {
this.checkDeployRecordsAndUpdate();
}, this.config.registryConfig.fetchDeploymentRecordDelay);
}
/**
* Update deployments with ApplicationDeploymentRecord data
*/
async updateDeploymentsWithRecordData (
records: AppDeploymentRecord[]
): Promise<void> {
// Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({
where: records.map((record) => ({
applicationRecordId: record.attributes.application
})),
order: {
createdAt: 'DESC'
}
});
// Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId);
}
return acc;
},
new Set<string>()
);
// Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds(
Array.from(productionDeploymentProjectIds),
{ isCurrent: false }
);
const recordToDeploymentsMap = deployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationRecordId] = deployment;
return acc;
},
{}
);
// Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.application];
await this.db.updateDeploymentById(deployment.id, {
applicationDeploymentRecordId: record.id,
applicationDeploymentRecordData: record.attributes,
url: record.attributes.url,
status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production
});
log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
);
});
await Promise.all(deploymentUpdatePromises);
}
async getUser (userId: string): Promise<User | null> {
@@ -45,15 +161,48 @@ 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(user && user.gitHubToken, 'User needs to be authenticated with GitHub token');
assert(
user && user.gitHubToken,
'User needs to be authenticated with GitHub token'
);
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;
}
@@ -62,45 +211,59 @@ 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;
}
async getDeployementsByProjectId (projectId: string): Promise<Deployment[]> {
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
const dbDeployments = await this.db.getDeploymentsByProjectId(projectId);
return dbDeployments;
}
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId);
async getEnvironmentVariablesByProjectId (
projectId: string
): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables =
await this.db.getEnvironmentVariablesByProjectId(projectId);
return dbEnvironmentVariables;
}
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId);
async getProjectMembersByProjectId (
projectId: string
): Promise<ProjectMember[]> {
const dbProjectMembers =
await this.db.getProjectMembersByProjectId(projectId);
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;
}
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
async getDomainsByProjectId (
projectId: string,
filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> {
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
return dbDomains;
}
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
async updateProjectMember (
projectMemberId: string,
data: { permissions: Permission[] }
): Promise<boolean> {
return this.db.updateProjectMemberById(projectMemberId, data);
}
async addProjectMember (projectId: string,
async addProjectMember (
projectId: string,
data: {
email: string,
permissions: Permission[]
}): Promise<ProjectMember> {
email: string;
permissions: Permission[];
}
): Promise<ProjectMember> {
// TODO: Send invitation
let user = await this.db.getUser({
where: {
@@ -128,50 +291,62 @@ 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');
}
}
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data.map((environmentVariable) => {
return environmentVariable.environments.map((environment) => {
return ({
key: environmentVariable.key,
value: environmentVariable.value,
environment: environment as Environment,
project: Object.assign(new Project(), {
id: projectId
})
async addEnvironmentVariables (
projectId: string,
data: { environments: string[]; key: string; value: string }[]
): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data
.map((environmentVariable) => {
return environmentVariable.environments.map((environment) => {
return {
key: environmentVariable.key,
value: environmentVariable.value,
environment: environment as Environment,
project: Object.assign(new Project(), {
id: projectId
})
};
});
});
}).flat();
})
.flat();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
formattedEnvironmentVariables
);
return savedEnvironmentVariables;
}
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
async updateEnvironmentVariable (
environmentVariableId: string,
data: DeepPartial<EnvironmentVariable>
): Promise<boolean> {
return this.db.updateEnvironmentVariable(environmentVariableId, data);
}
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
async removeEnvironmentVariable (
environmentVariableId: string
): Promise<boolean> {
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: {
@@ -183,15 +358,17 @@ export class Service {
throw new Error('Deployment does not exist');
}
const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch });
const prodBranchDomains = await this.db.getDomainsByProjectId(
oldDeployment.project.id,
{ branch: oldDeployment.project.prodBranch }
);
const octokit = await this.getOctokit(userId);
const octokit = await this.getOctokit(user.id);
const newDeployement = await this.createDeployment(userId,
const newDeployment = await this.createDeployment(user.id,
octokit,
{
project: oldDeployment.project,
isCurrent: true,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
@@ -199,7 +376,7 @@ export class Service {
commitMessage: oldDeployment.commitMessage
});
return newDeployement;
return newDeployment;
}
async createDeployment (
@@ -209,7 +386,9 @@ export class Service {
recordData: { repoUrl?: string } = {}
): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found');
log(`Creating deployment in project ${data.project.name} from branch ${data.branch}`);
log(
`Creating deployment in project ${data.project.name} from branch ${data.branch}`
);
const [owner, repo] = data.project.repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({
@@ -224,61 +403,78 @@ export class Service {
}
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
const packageJSON = JSON.parse(atob(packageJSONData.content));
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
assert(packageJSON.name, "name field doesn't exist in package.json");
if (!recordData.repoUrl) {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
const { data: repoDetails } = await octokit.rest.repos.get({
owner,
repo
});
recordData.repoUrl = repoDetails.html_url;
}
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { registryRecordId, registryRecordData } = await this.registry.createApplicationRecord({
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl: recordData.repoUrl
});
// Check if new deployment is set to current
if (data.isCurrent) {
// Update previous current deployment
await this.db.updateDeployment({
project: {
id: data.project.id
},
isCurrent: true
}, { isCurrent: false });
}
const { applicationRecordId, applicationRecordData } =
await this.registry.createApplicationRecord({
appName: repo,
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl: recordData.repoUrl
});
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
await this.db.updateDeployment({
domainId: data.domain?.id
}, {
domain: null
});
if (data.domain) {
await this.db.updateDeployment({
domainId: data.domain.id
}, {
domain: null
});
}
const newDeployement = await this.db.addDeployement({
const newDeployment = await this.db.addDeployment({
project: data.project,
branch: data.branch,
commitHash: data.commitHash,
commitMessage: data.commitMessage,
environment: data.environment,
isCurrent: data.isCurrent,
status: DeploymentStatus.Building,
registryRecordId,
registryRecordData,
applicationRecordId,
applicationRecordData,
domain: data.domain,
createdBy: Object.assign(new User(), {
id: userId
})
});
log(`Created deployment ${newDeployement.id} and published application record ${registryRecordId}`);
return newDeployement;
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
@@ -288,12 +484,14 @@ 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 { data: [latestCommit] } = await octokit.rest.repos.listCommits({
const {
data: [latestCommit]
} = await octokit.rest.repos.listCommits({
owner,
repo,
sha: project.prodBranch,
@@ -303,11 +501,10 @@ export class Service {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
// Create deployment with prod branch and latest commit
const newDeployment = await this.createDeployment(userId,
await this.createDeployment(user.id,
octokit,
{
project,
isCurrent: true,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
@@ -319,27 +516,6 @@ export class Service {
}
);
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(project.id, { environment: Environment.Production });
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
acc[env.key] = env.value;
return acc;
}, {} as { [key: string]: string });
const { registryRecordId, registryRecordData } = await this.registry.createApplicationDeploymentRequest(
{
appName: newDeployment.registryRecordData.name!,
commitHash: latestCommit.sha,
repository: repoDetails.html_url,
environmentVariables: environmentVariablesObj
});
await this.db.updateProjectById(project.id, {
registryRecordId,
registryRecordData
});
await this.createRepoHook(octokit, project);
return project;
@@ -352,7 +528,10 @@ export class Service {
owner,
repo,
config: {
url: new URL('api/github/webhook', this.config.gitHubConfig.webhookUrl).href,
url: new URL(
'api/github/webhook',
this.config.gitHubConfig.webhookUrl
).href,
content_type: 'json'
},
events: ['push']
@@ -360,9 +539,13 @@ export class Service {
} catch (err) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
if (
!(err instanceof RequestError &&
err.status === 422 &&
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR))
!(
err instanceof RequestError &&
err.status === 422 &&
(err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
)
)
) {
throw err;
}
@@ -374,7 +557,9 @@ export class Service {
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
const { repository, ref, head_commit: headCommit } = data;
log(`Handling GitHub push event from repository: ${repository.full_name}`);
const projects = await this.db.getProjects({ where: { repository: repository.full_name } });
const projects = await this.db.getProjects({
where: { repository: repository.full_name }
});
if (!projects.length) {
log(`No projects found for repository ${repository.full_name}`);
@@ -386,24 +571,29 @@ export class Service {
for await (const project of projects) {
const octokit = await this.getOctokit(project.ownerId);
const [domain] = await this.db.getDomainsByProjectId(project.id, { branch });
const [domain] = await this.db.getDomainsByProjectId(project.id, {
branch
});
// Create deployment with branch and latest commit in GitHub data
await this.createDeployment(project.ownerId,
octokit,
{
project,
isCurrent: project.prodBranch === branch,
branch,
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message
});
await this.createDeployment(project.ownerId, octokit, {
project,
branch,
environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message
});
}
}
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
async updateProject (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
return this.db.updateProjectById(projectId, data);
}
@@ -420,13 +610,15 @@ export class Service {
});
if (domainsRedirectedFrom.length > 0) {
throw new Error('Cannot delete domain since it has redirects from other domains');
throw new Error(
'Cannot delete domain since it has redirects from other domains'
);
}
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,
@@ -442,25 +634,27 @@ export class Service {
throw new Error('Deployment not found');
}
const octokit = await this.getOctokit(userId);
const octokit = await this.getOctokit(user.id);
const newDeployement = await this.createDeployment(userId,
const newDeployment = await this.createDeployment(user.id,
octokit,
{
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
isCurrent: true,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
return newDeployement;
return newDeployment;
}
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
async rollbackDeployment (
projectId: string,
deploymentId: string
): Promise<boolean> {
// TODO: Implement transactions
const oldCurrentDeployment = await this.db.getDeployment({
relations: {
@@ -475,19 +669,28 @@ export class Service {
});
if (!oldCurrentDeployment) {
throw new Error('Current deployement doesnot exist');
throw new Error('Current deployment doesnot exist');
}
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
oldCurrentDeployment.id,
{ isCurrent: false, domain: null }
);
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain });
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
deploymentId,
{ isCurrent: true, domain: oldCurrentDeployment?.domain }
);
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
}
async addDomain (projectId: string, data: { name: string }): Promise<{
primaryDomain: Domain,
redirectedDomain: Domain
async addDomain (
projectId: string,
data: { name: string }
): Promise<{
primaryDomain: Domain;
redirectedDomain: Domain;
}> {
const currentProject = await this.db.getProjectById(projectId);
@@ -512,12 +715,20 @@ export class Service {
redirectTo: savedPrimaryDomain
};
const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails);
const savedRedirectedDomain = await this.db.addDomain(
redirectedDomainDetails
);
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain };
return {
primaryDomain: savedPrimaryDomain,
redirectedDomain: savedRedirectedDomain
};
}
async updateDomain (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
async updateDomain (
domainId: string,
data: DeepPartial<Domain>
): Promise<boolean> {
const domain = await this.db.getDomain({
where: {
id: domainId
@@ -558,7 +769,9 @@ export class Service {
}
if (redirectedDomain.redirectToId) {
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
throw new Error(
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
);
}
newDomain.redirectTo = redirectedDomain;
@@ -569,17 +782,17 @@ export class Service {
return updateResult;
}
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
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);
}
}
+26 -2
View File
@@ -1,6 +1,6 @@
export interface PackageJSON {
name?: string;
version?: string;
name: string;
version: string;
author?: string;
description?: string;
homepage?: string;
@@ -25,3 +25,27 @@ export interface GitPushEventPayload {
message: string;
};
}
export interface AppDeploymentRecordAttributes {
application: string;
dns: string;
meta: string;
name: string;
request: string;
type: string;
url: string;
version: string;
}
interface RegistryRecord {
id: string;
names: string[] | null;
owners: string[];
bondId: string;
createTime: string;
expiryTime: string;
}
export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes;
}
+47
View File
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
import path from 'path';
import toml from 'toml';
import debug from 'debug';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
const log = debug('snowball:utils');
@@ -19,3 +20,49 @@ export const getConfig = async <ConfigType>(
return config;
};
export const checkFileExists = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
log(err);
return false;
}
};
export const getEntities = async (filePath: string): Promise<any> => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData);
return entities;
};
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
entityType: EntityTarget<Entity>,
dataSource: DataSource,
entities: any,
relations?: any | undefined
): Promise<Entity[]> => {
const entityRepository = dataSource.getRepository(entityType);
const savedEntity: Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
if (relations) {
for (const field in relations) {
const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity);
}
return savedEntity;
};
+1 -1
View File
@@ -18,4 +18,4 @@ const main = async () => {
deleteFile(config.database.dbPath);
};
main().catch(err => log(err));
main().catch((err) => log(err));
+71 -49
View File
@@ -1,14 +1,16 @@
[
{
"projectIndex": 0,
"domainIndex":0,
"domainIndex": 0,
"createdByIndex": 0,
"id":"ffhae3zq",
"status": "Building",
"id": "ffhae3zq",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -16,14 +18,16 @@
},
{
"projectIndex": 0,
"domainIndex":1,
"domainIndex": 1,
"createdByIndex": 0,
"id":"vehagei8",
"id": "vehagei8",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"registryRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -31,14 +35,16 @@
},
{
"projectIndex": 0,
"domainIndex":2,
"domainIndex": 2,
"createdByIndex": 0,
"id":"qmgekyte",
"status": "Error",
"id": "qmgekyte",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -48,12 +54,14 @@
"projectIndex": 0,
"domainIndex": null,
"createdByIndex": 0,
"id":"f8wsyim6",
"id": "f8wsyim6",
"status": "Ready",
"environment": "Production",
"isCurrent": false,
"registryRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "prod",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -61,14 +69,16 @@
},
{
"projectIndex": 1,
"domainIndex":3,
"domainIndex": 3,
"createdByIndex": 1,
"id":"eO8cckxk",
"status": "Building",
"id": "eO8cckxk",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -76,14 +86,16 @@
},
{
"projectIndex": 1,
"domainIndex":4,
"domainIndex": 4,
"createdByIndex": 1,
"id":"yaq0t5yw",
"id": "yaq0t5yw",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"registryRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -91,14 +103,16 @@
},
{
"projectIndex": 1,
"domainIndex":5,
"domainIndex": 5,
"createdByIndex": 1,
"id":"hwwr6sbx",
"status": "Error",
"id": "hwwr6sbx",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"registryRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -106,14 +120,16 @@
},
{
"projectIndex": 2,
"domainIndex":9,
"domainIndex": 9,
"createdByIndex": 2,
"id":"ndxje48a",
"status": "Building",
"id": "ndxje48a",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -121,14 +137,16 @@
},
{
"projectIndex": 2,
"domainIndex":7,
"domainIndex": 7,
"createdByIndex": 2,
"id":"gtgpgvei",
"id": "gtgpgvei",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"registryRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -136,14 +154,16 @@
},
{
"projectIndex": 2,
"domainIndex":8,
"domainIndex": 8,
"createdByIndex": 2,
"id":"b4bpthjr",
"status": "Error",
"id": "b4bpthjr",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"registryRecordData": {},
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {},
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -153,12 +173,14 @@
"projectIndex": 3,
"domainIndex": 6,
"createdByIndex": 2,
"id":"b4bpthjr",
"status": "Building",
"id": "b4bpthjr",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"registryRecordData": {},
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
+2 -2
View File
@@ -6,7 +6,7 @@
},
{
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
"name": "AirFoil",
"slug": "airfoil-2"
"name": "Laconic",
"slug": "laconic-2"
}
]
+6 -6
View File
@@ -1,37 +1,37 @@
[
{
"projectIndex": 0,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "Live",
"branch": "main"
},
{
"projectIndex": 0,
"name": "saugatt.com",
"name": "example.org",
"status": "Pending",
"branch": "test"
},
{
"projectIndex": 1,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "Live",
"branch": "main"
},
{
"projectIndex": 1,
"name": "saugatt.com",
"name": "example.org",
"status": "Pending",
"branch": "test"
},
{
"projectIndex": 2,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "Live",
"branch": "main"
},
{
"projectIndex": 2,
"name": "saugatt.com",
"name": "example.org",
"status": "Pending",
"branch": "test"
},
+9 -31
View File
@@ -2,77 +2,55 @@
{
"memberIndex": 1,
"projectIndex": 0,
"permissions": [
"View"
],
"permissions": ["View"],
"isPending": false
},
{
"memberIndex": 2,
"projectIndex": 0,
"permissions": [
"View",
"Edit"
],
"permissions": ["View", "Edit"],
"isPending": false
},
{
"memberIndex": 2,
"projectIndex": 1,
"permissions": [
"View"
],
"permissions": ["View"],
"isPending": false
},
{
"memberIndex": 0,
"projectIndex": 2,
"permissions": [
"View"
],
"permissions": ["View"],
"isPending": false
},
{
"memberIndex": 1,
"projectIndex": 2,
"permissions": [
"View",
"Edit"
],
"permissions": ["View", "Edit"],
"isPending": false
},
{
"memberIndex": 0,
"projectIndex": 3,
"permissions": [
"View"
],
"permissions": ["View"],
"isPending": false
},
{
"memberIndex": 2,
"projectIndex": 3,
"permissions": [
"View",
"Edit"
],
"permissions": ["View", "Edit"],
"isPending": false
},
{
"memberIndex": 1,
"projectIndex": 4,
"permissions": [
"View"
],
"permissions": ["View"],
"isPending": false
},
{
"memberIndex": 2,
"projectIndex": 4,
"permissions": [
"View",
"Edit"
],
"permissions": ["View", "Edit"],
"isPending": false
}
]
-10
View File
@@ -10,8 +10,6 @@
"framework": "test",
"webhooks": [],
"icon": "",
"registryRecordId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"subDomain": "testProject.snowball.xyz"
},
{
@@ -25,8 +23,6 @@
"framework": "test-2",
"webhooks": [],
"icon": "",
"registryRecordId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"subDomain": "testProject-2.snowball.xyz"
},
{
@@ -40,8 +36,6 @@
"framework": "test-3",
"webhooks": [],
"icon": "",
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"subDomain": "iglootools.snowball.xyz"
},
{
@@ -55,8 +49,6 @@
"framework": "test-4",
"webhooks": [],
"icon": "",
"registryRecordId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"subDomain": "iglootools-2.snowball.xyz"
},
{
@@ -70,8 +62,6 @@
"framework": "test-5",
"webhooks": [],
"icon": "",
"registryRecordId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"subDomain": "snowball-2.snowball.xyz"
}
]
+3 -3
View File
@@ -1,21 +1,21 @@
[
{
"projectIndex": 0,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "Pending",
"redirectToIndex": 1,
"branch": "test"
},
{
"projectIndex": 1,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "Pending",
"redirectToIndex": 3,
"branch": "test"
},
{
"projectIndex": 2,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "Pending",
"redirectToIndex": 5,
"branch": "test"
+12 -9
View File
@@ -1,20 +1,23 @@
[
{
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
"name": "Saugat Yadav",
"email": "saugaty@airfoil.studio",
"isVerified": true
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"email": "snowball@snowballtools.xyz",
"isVerified": true,
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
},
{
"id": "e505b212-8da6-48b2-9614-098225dab34b",
"name": "Gideon Low",
"email": "gideonl@airfoil.studio",
"isVerified": true
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
"email": "alice@snowballtools.xyz",
"isVerified": true,
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
},
{
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
"name": "Sushan Yadav",
"email": "sushany@airfoil.studio",
"isVerified": true
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
"email": "bob@snowballtools.xyz",
"isVerified": true,
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
}
]
+84 -50
View File
@@ -1,5 +1,4 @@
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
import * as fs from 'fs/promises';
import { DataSource } from 'typeorm';
import debug from 'debug';
import path from 'path';
@@ -11,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
import { Domain } from '../src/entity/Domain';
import { ProjectMember } from '../src/entity/ProjectMember';
import { Deployment } from '../src/entity/Deployment';
import { getConfig } from '../src/utils';
import {
checkFileExists,
getConfig,
getEntities,
loadAndSaveData
} from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
@@ -20,50 +24,42 @@ const log = debug('snowball:initialize-database');
const USER_DATA_PATH = './fixtures/users.json';
const PROJECT_DATA_PATH = './fixtures/projects.json';
const ORGANIZATION_DATA_PATH = './fixtures/organizations.json';
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-orgnizations.json';
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-organizations.json';
const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json';
const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json';
const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData);
const entityRepository = dataSource.getRepository(entityType);
const savedEntity:Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
if (relations) {
for (const field in relations) {
const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity);
}
return savedEntity;
};
const generateTestData = async (dataSource: DataSource) => {
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
const userEntities = await getEntities(
path.resolve(__dirname, USER_DATA_PATH)
);
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
const orgEntities = await getEntities(
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
);
const savedOrgs = await loadAndSaveData(
Organization,
dataSource,
orgEntities
);
const projectRelations = {
owner: savedUsers,
organization: savedOrgs
};
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
const projectEntities = await getEntities(
path.resolve(__dirname, PROJECT_DATA_PATH)
);
const savedProjects = await loadAndSaveData(
Project,
dataSource,
projectEntities,
projectRelations
);
const domainRepository = dataSource.getRepository(Domain);
@@ -71,14 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects
};
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations);
const primaryDomainsEntities = await getEntities(
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
);
const savedPrimaryDomains = await loadAndSaveData(
Domain,
dataSource,
primaryDomainsEntities,
domainPrimaryRelations
);
const domainRedirectedRelations = {
project: savedProjects,
redirectTo: savedPrimaryDomains
};
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations);
const redirectDomainsEntities = await getEntities(
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
);
await loadAndSaveData(
Domain,
dataSource,
redirectDomainsEntities,
domainRedirectedRelations
);
const savedDomains = await domainRepository.find();
@@ -87,14 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs
};
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
const userOrganizationsEntities = await getEntities(
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
);
await loadAndSaveData(
UserOrganization,
dataSource,
userOrganizationsEntities,
userOrganizationRelations
);
const projectMemberRelations = {
member: savedUsers,
project: savedProjects
};
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
const projectMembersEntities = await getEntities(
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
);
await loadAndSaveData(
ProjectMember,
dataSource,
projectMembersEntities,
projectMemberRelations
);
const deploymentRelations = {
project: savedProjects,
@@ -102,23 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers
};
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
const deploymentsEntities = await getEntities(
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
);
await loadAndSaveData(
Deployment,
dataSource,
deploymentsEntities,
deploymentRelations
);
const environmentVariableRelations = {
project: savedProjects
};
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
};
const checkFileExists = async (filePath: string) => {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
log(err);
return false;
}
const environmentVariablesEntities = await getEntities(
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
);
await loadAndSaveData(
EnvironmentVariable,
dataSource,
environmentVariablesEntities,
environmentVariableRelations
);
};
const main = async () => {
+14 -6
View File
@@ -11,22 +11,30 @@ 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);
// 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);
await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee);
await registry.createBond(
{ denom: DENOM, amount: BOND_AMOUNT },
registryConfig.privateKey,
registryConfig.fee
);
for await (const name of AUTHORITY_NAMES) {
for await (const name of authorityNames) {
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
log('Reserved authority name:', name);
await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee);
await registry.setAuthorityBond(
{ name, bondId },
registryConfig.privateKey,
registryConfig.fee
);
log(`Bond ${bondId} set for authority ${name}`);
}
}
@@ -0,0 +1,81 @@
import debug from 'debug';
import { DataSource } from 'typeorm';
import path from 'path';
import { Registry } from '@cerc-io/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records');
async function main () {
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
const dataSource = new DataSource({
type: 'better-sqlite3',
database: database.dbPath,
synchronize: true,
entities: [path.join(__dirname, '../src/entity/*')]
});
await dataSource.initialize();
const deploymentRepository = dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find({
relations: {
project: true
},
where: {
status: DeploymentStatus.Building
}
});
for await (const deployment of deployments) {
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
const applicationDeploymentRecord = {
type: 'ApplicationDeploymentRecord',
version: '0.0.1',
name: deployment.applicationRecordData.name,
application: deployment.applicationRecordId,
// TODO: Create DNS record
dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm',
// Using dummy values
meta: JSON.stringify({
config: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
}),
request: deployment.applicationDeploymentRequestId,
url
};
const result = await registry.setRecord(
{
privateKey: registryConfig.privateKey,
record: applicationDeploymentRecord,
bondId: registryConfig.bondId
},
'',
registryConfig.fee
);
log('Application deployment record data:', applicationDeploymentRecord);
log(`Application deployment record published: ${result.data.id}`);
}
}
main().catch((err) => {
log(err);
});
+5 -2
View File
@@ -1,4 +1,7 @@
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
REACT_APP_SERVER_URL = 'http://localhost:8000'
REACT_APP_GITHUB_CLIENT_ID =
REACT_APP_GITHUB_TEMPLATE_REPO =
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
REACT_APP_WALLET_CONNECT_ID =
+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

+20 -3
View File
@@ -5,6 +5,15 @@
"dependencies": {
"@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7",
"@tanstack/react-query": "^5.22.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -12,14 +21,19 @@
"@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",
"date-fns": "^3.0.1",
"downshift": "^8.2.3",
"axios": "^1.6.7",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"downshift": "^8.3.2",
"eslint-config-react-app": "^7.0.1",
"gql-client": "^1.0.0",
"luxon": "^3.4.4",
"octokit": "^3.1.2",
"react": "^18.2.0",
"react-calendar": "^4.8.0",
"react-code-blocks": "^0.1.6",
"react-day-picker": "^8.9.1",
"react-dom": "^18.2.0",
@@ -30,10 +44,13 @@
"react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-timer-hook": "^3.0.7",
"siwe": "^2.1.4",
"tailwind-variants": "^0.2.0",
"typescript": "^4.9.5",
"usehooks-ts": "^2.10.0",
"vertical-stepper-nav": "^1.0.2",
"viem": "^2.7.11",
"wagmi": "^2.5.7",
"web-vitals": "^2.1.4"
},
"scripts": {
@@ -73,6 +90,6 @@
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "^7.33.2",
"prettier": "^3.1.0",
"tailwindcss": "^3.3.6"
"tailwindcss": "^3.4.1"
}
}
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"
}
+6 -6
View File
@@ -9,8 +9,8 @@ import {
projectsRoutesWithoutSearch,
} from './pages/org-slug/projects/routes';
import ProjectSearchLayout from './layouts/ProjectSearch';
import { OctokitProvider } from './context/OctokitContext';
import Index from './pages';
import Login from './pages/Login';
const router = createBrowserRouter([
{
@@ -44,14 +44,14 @@ const router = createBrowserRouter([
path: '/',
element: <Index />,
},
{
path: '/login',
element: <Login />,
},
]);
function App() {
return (
<OctokitProvider>
<RouterProvider router={router} />
</OctokitProvider>
);
return <RouterProvider router={router} />;
}
export default App;
+3 -3
View File
@@ -2,7 +2,7 @@
{
"id": 1,
"projectid": 1,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "live",
"record": null,
"isRedirectedto": false
@@ -10,7 +10,7 @@
{
"id": 2,
"projectid": 1,
"name": "saugatt.com",
"name": "example.org",
"status": "pending",
"record": {
"type": "A",
@@ -22,7 +22,7 @@
{
"id": 3,
"projectid": 1,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "pending",
"record": {
"type": "CNAME",
-17
View File
@@ -1,17 +0,0 @@
[
{
"name": "Saugat Yadav",
"email": "saugaty@airfoil.studio",
"id": 1
},
{
"name": "Gideon Low",
"email": "gideonl@airfoil.studio",
"id": 2
},
{
"name": "Sushan Yadav",
"email": "sushany@airfoil.studio",
"id": 3
}
]
@@ -1,6 +1,6 @@
[
"[20:50:03.502] Running build in Washington, D.C., USA (East) iad1",
"[20:50:03.641] Cloning github.com/saugatyadav11/nextjs2 (Branch: main, Commit: 4a5f47f)",
"[20:50:03.641] Cloning github.com/cerc-io/nextjs2 (Branch: main, Commit: 4a5f47f)",
"[20:50:04.004] Previous build cache not available",
"[20:50:04.118] Cloning completed: 480.574ms",
"[20:50:04.382] Running 'vercel build'",
-190
View File
@@ -1,190 +0,0 @@
[
{
"id": 1,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"domain": null,
"organization": "Airfoil",
"url": "iglootools.co",
"createdAt": "2023-12-07T04:20:00",
"createdBy": "Alice",
"deployment": "iglootools.snowballtools.co",
"source": "feature/add-remote-control",
"latestCommit": {
"message": "subscription added",
"createdAt": "2023-12-11T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1
},
{
"id": 2,
"icon": "^",
"name": "snowball-starter-kit",
"title": "Snowball Starter Kit",
"domain": null,
"organization": "Snowball",
"url": "starterkit.snowballtools.com",
"createdAt": "2023-12-04T04:20:00",
"createdBy": "Bob",
"deployment": "deploy.snowballtools.com",
"source": "prod/add-docker-compose",
"latestCommit": {
"message": "component updates",
"createdAt": "2023-12-11T04:20:00",
"branch": "staging"
},
"repositoryId": 1,
"members": [
{
"id": 2,
"permissions": []
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 2
},
{
"id": 3,
"icon": "^",
"name": "web3-android",
"title": "Web3 Android",
"domain": null,
"organization": "Personal",
"url": "web3fordroids.com",
"createdAt": "2023-12-01T04:20:00",
"createdBy": "Charlie",
"deployment": "deploy.web3fordroids.com",
"source": "dev/style-page",
"latestCommit": {
"message": "No repo connected",
"createdAt": "2023-12-01T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1
},
{
"id": 4,
"icon": "^",
"name": "passkeys-demo",
"title": "Passkeys Demo",
"domain": null,
"organization": "Airfoil",
"url": "passkeys.iglootools.xyz",
"createdAt": "2023-12-01T04:20:00",
"createdBy": "David",
"deployment": "demo.passkeys.xyz",
"source": "dev/style-page",
"latestCommit": {
"message": "hello world",
"createdAt": "2023-12-01T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1
},
{
"id": 5,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"domain": null,
"organization": "Airfoil",
"url": "iglootools.xyz",
"createdAt": "2023-12-11T04:20:00",
"createdBy": "Erin",
"deployment": "staging.snowballtools.com",
"source": "prod/fix-error",
"latestCommit": {
"message": "404 added",
"createdAt": "2023-12-09T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 3,
"permissions": []
}
],
"ownerId": 3
},
{
"id": 6,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"domain": null,
"organization": "Airfoil",
"url": "iglootools.xyz",
"createdAt": "2023-12-11T04:20:00",
"createdBy": "Frank",
"deployment": "iglootools.snowballtools.com",
"source": "prod/fix-error",
"latestCommit": {
"message": "design system integrated",
"createdAt": "2023-12-09T04:20:00",
"branch": "prod"
},
"repositoryId": 1,
"members": [
{
"id": 2,
"permissions": []
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 2
}
]
@@ -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": "^"
}
]
+32
View File
@@ -0,0 +1,32 @@
export default [
{
id: '1',
name: 'Progressive Web App (PWA)',
icon: '^',
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
},
{
id: '2',
name: 'Image Upload PWA',
icon: '^',
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
},
{
id: '3',
name: 'Kotlin',
icon: '^',
repoFullName: '',
},
{
id: '4',
name: 'React Native',
icon: '^',
repoFullName: '',
},
{
id: '5',
name: 'Swift',
icon: '^',
repoFullName: '',
},
];
@@ -126,7 +126,8 @@ const DatePicker = ({
crossOrigin={undefined}
/>
</PopoverHandler>
<PopoverContent>
{/* TODO: Figure out what placeholder is for */}
<PopoverContent placeholder={''}>
{mode === 'single' && (
<DayPicker
mode="single"
@@ -145,19 +146,23 @@ const DatePicker = ({
/>
<HorizontalLine />
<div className="flex justify-end">
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full mr-2"
variant="outlined"
onClick={() => setIsOpen(false)}
placeholder={''}
>
Cancel
</Button>
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full"
color="gray"
onClick={() => handleRangeSelect()}
placeholder={''}
>
Select
</Button>
@@ -1,7 +1,7 @@
import React from 'react';
const HorizontalLine = () => {
return <hr className="h-px bg-gray-300 border-0" />;
return <hr className="h-px bg-gray-100 border-0" />;
};
export default HorizontalLine;
+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>
);
};
+86 -33
View File
@@ -2,15 +2,26 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
import { Organization } from 'gql-client';
import { Typography, Option } from '@material-tailwind/react';
import { Option } from '@material-tailwind/react';
import { useDisconnect } from 'wagmi';
import { useGQLClient } from '../context/GQLClientContext';
import AsyncSelect from './shared/AsyncSelect';
import {
ChevronGrabberHorizontal,
FolderIcon,
GlobeIcon,
LifeBuoyIcon,
QuestionMarkRoundIcon,
SettingsSlidersIcon,
} from './shared/CustomIcon';
import { Tabs } from 'components/shared/Tabs';
const Sidebar = () => {
const { orgSlug } = useParams();
const navigate = useNavigate();
const client = useGQLClient();
const { disconnect } = useDisconnect();
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
const [organizations, setOrganizations] = useState<Organization[]>([]);
@@ -25,59 +36,101 @@ const Sidebar = () => {
setSelectedOrgSlug(orgSlug);
}, [orgSlug]);
const handleLogOut = useCallback(() => {
disconnect();
navigate('/login');
}, [disconnect, navigate]);
return (
<div className="flex flex-col h-full p-4">
<div className="flex flex-col h-full p-4 pt-10">
<div className="grow">
<div>
<Link to={`/${orgSlug}`}>
<h3 className="text-black text-2xl">Snowball</h3>
</Link>
</div>
<Link to={`/${orgSlug}`}>
<div className="flex items-center space-x-3 mb-10 ml-2">
<img
src="/logo.svg"
alt="Snowball Logo"
className="h-8 w-8 rounded-lg"
/>
<span className="text-2xl font-bold text-snowball-900">
Snowball
</span>
</div>
</Link>
<AsyncSelect
className="bg-white py-2"
containerProps={{ className: 'h-14 border-none' }}
labelProps={{ className: 'before:border-none after:border-none' }}
className="bg-white rounded-lg shadow border-none"
value={selectedOrgSlug}
onChange={(value) => {
setSelectedOrgSlug(value!);
navigate(`/${value}`);
}}
selected={(_, index) => (
<div className="flex gap-2">
<div>^</div>
<div className="flex items-center space-x-3">
<img
src="/logo.svg"
alt="Application Logo"
className="h-8 w-8 rounded-lg"
/>
<div>
<span>{organizations[index!]?.name}</span>
<Typography>Organization</Typography>
<div className="text-sm font-semibold">
{organizations[index!]?.name}
</div>
<div className="text-xs text-gray-500">Organization</div>
</div>
</div>
)}
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
>
{/* TODO: Show label organization and manage in option */}
{organizations.map((org) => (
<Option key={org.id} value={org.slug}>
^ {org.name}
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
<div className="flex items-center space-x-3">
<img
src="/logo.svg"
alt="Application Logo"
className="h-8 w-8 rounded-lg"
/>
<div>
<div className="text-sm font-semibold">{org.name}</div>
<div className="text-xs text-gray-500">Organization</div>
</div>
</div>
</Option>
))}
</AsyncSelect>
<div>
<NavLink
to={`/${orgSlug}`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
>
<Typography>Projects</Typography>
</NavLink>
</div>
<div>
<NavLink
to={`/${orgSlug}/settings`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
>
<Typography>Settings</Typography>
</NavLink>
</div>
<Tabs defaultValue="Projects" orientation="vertical" className="mt-10">
<Tabs.List>
{[
{ title: 'Projects', icon: <FolderIcon /> },
{ title: 'Settings', icon: <SettingsSlidersIcon /> },
].map(({ title, icon }, index) => (
<NavLink to={`/${orgSlug}/${title}`} key={index}>
<Tabs.Trigger icon={icon} value={title}>
{title}
</Tabs.Trigger>
</NavLink>
))}
</Tabs.List>
</Tabs>
</div>
<div className="grow flex flex-col justify-end">
<div>Documentation</div>
<div>Support</div>
<div className="grow flex flex-col justify-end mb-8">
<Tabs defaultValue="Projects" orientation="vertical">
{/* TODO: use proper link buttons */}
<Tabs.List>
<Tabs.Trigger icon={<GlobeIcon />} value="">
<a className="cursor-pointer" onClick={handleLogOut}>
Log Out
</a>
</Tabs.Trigger>
<Tabs.Trigger icon={<QuestionMarkRoundIcon />} value="">
<a className="cursor-pointer">Documentation</a>
</Tabs.Trigger>
<Tabs.Trigger icon={<LifeBuoyIcon />} value="">
<a className="cursor-pointer">Support</a>
</Tabs.Trigger>
</Tabs.List>
</Tabs>
</div>
</div>
);
@@ -21,11 +21,15 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
return (
<div className="bg-white border border-gray-200 rounded-lg shadow">
<div className="flex gap-2 p-2 items-center">
<Avatar variant="rounded" src={project.icon || '/gray.png'} />
<Avatar
variant="rounded"
src={project.icon || '/gray.png'}
placeholder={''}
/>
<div className="grow">
<Link to={`projects/${project.id}`}>
<Typography>{project.name}</Typography>
<Typography color="gray" variant="small">
<Typography placeholder={''}>{project.name}</Typography>
<Typography color="gray" variant="small" placeholder={''}>
{project.deployments[0]?.domain?.name ??
'No Production Deployment'}
</Typography>
@@ -35,25 +39,27 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
<MenuHandler>
<button>...</button>
</MenuHandler>
<MenuList>
<MenuItem>^ Project settings</MenuItem>
<MenuItem className="text-red-500">^ Delete project</MenuItem>
<MenuList placeholder={''}>
<MenuItem placeholder={''}>^ Project settings</MenuItem>
<MenuItem className="text-red-500" placeholder={''}>
^ Delete project
</MenuItem>
</MenuList>
</Menu>
</div>
<div className="border-t-2 border-solid p-4 bg-gray-50">
{project.deployments.length > 0 ? (
<>
<Typography variant="small" color="gray">
<Typography variant="small" color="gray" placeholder={''}>
^ {project.deployments[0].commitMessage}
</Typography>
<Typography variant="small" color="gray">
<Typography variant="small" color="gray" placeholder={''}>
{relativeTimeMs(project.deployments[0].createdAt)} on ^&nbsp;
{project.deployments[0].branch}
</Typography>
</>
) : (
<Typography variant="small" color="gray">
<Typography variant="small" color="gray" placeholder={''}>
No Production deployment
</Typography>
)}
@@ -71,12 +71,13 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
(!inputValue || !isOpen) && 'hidden'
}`}
placeholder={''}
>
<List {...getMenuProps()}>
{items.length ? (
<>
<div className="p-3">
<Typography variant="small" color="gray">
<Typography variant="small" color="gray" placeholder={''}>
Suggestions
</Typography>
</div>
@@ -84,19 +85,25 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
<ListItem
selected={highlightedIndex === index || selectedItem === item}
key={item.id}
placeholder={''}
{...getItemProps({ item, index })}
>
<ListItemPrefix>
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
<ListItemPrefix placeholder={''}>
<Avatar
src={item.icon || '/gray.png'}
variant="rounded"
placeholder={''}
/>
</ListItemPrefix>
<div>
<Typography variant="h6" color="blue-gray">
<Typography variant="h6" color="blue-gray" placeholder={''}>
{item.name}
</Typography>
<Typography
variant="small"
color="gray"
className="font-normal"
placeholder={''}
>
{item.organization.name}
</Typography>
@@ -106,7 +113,9 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
</>
) : (
<div className="p-3">
<Typography>^ No projects matching this name</Typography>
<Typography placeholder={''}>
^ No projects matching this name
</Typography>
</div>
)}
</List>
@@ -1,9 +1,13 @@
import { Button } from '@material-tailwind/react';
import React from 'react';
import OauthPopup from 'react-oauth-popup';
import { useGQLClient } from '../../../context/GQLClientContext';
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
import { Button } from 'components/shared/Button';
import {
GitIcon,
EllipsesIcon,
SnowballIcon,
} from 'components/shared/CustomIcon';
const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
@@ -14,7 +18,9 @@ interface ConnectAccountInterface {
onAuth: (token: string) => void;
}
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
onAuth: onToken,
}: ConnectAccountInterface) => {
const client = useGQLClient();
const handleCode = async (code: string) => {
@@ -27,17 +33,26 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
// TODO: Use correct height
return (
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
<div>^</div>
<div>
<p>Connect to your git account</p>
<p>
Once connected, you can import a repository from your
<br />
account or start with one of our templates.
</p>
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex">
<div className="w-16 h-16 bg-blue-100 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 inline-flex">
<GitIcon />
</div>
<EllipsesIcon className="items-center gap-1.5 flex" />
<div className="w-16 h-16 bg-blue-400 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 flex">
<SnowballIcon />
</div>
</div>
<div className="mt-2 flex">
<div>
<div className="text-center text-slate-900 text-xl font-medium leading-7">
Connect to your Git account
</div>
<div className="text-center text-slate-600 text-base font-normal leading-normal">
Once connected, you can import a repository from your account or start
with one of our templates.
</div>
</div>
<div className="mt-2 flex gap-3">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={handleCode}
@@ -46,11 +61,15 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
width={1000}
height={1000}
>
<Button className="rounded-full mx-2">Connect to Github</Button>
<Button>Connect to GitHub</Button>
</OauthPopup>
<Button className="rounded-full mx-2">Connect to Gitea</Button>
<Button>Connect to Gitea</Button>
</div>
<ConnectAccountTabPanel />
{/* TODO: Add ConnectAccountTabPanel */}
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
<ConnectAccountTabPanel />
</div> */}
</div>
);
};
@@ -1,21 +1,24 @@
import React from 'react';
import { Tabs, TabsHeader, Tab } from '@material-tailwind/react';
import { Tabs } from 'components/shared/Tabs';
const ConnectAccountTabPanel = () => {
const ConnectAccountTabPanel: React.FC = () => {
return (
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
<TabsHeader className="grid grid-cols-2">
<Tab className="row-span-1" value="import">
Import a repository
</Tab>
<Tab className="row-span-2" value="template">
Start with a template
</Tab>
</TabsHeader>
{/* <TabsBody> */}
{/* TODO: Add content */}
{/* </TabsBody> */}
<Tabs
defaultValue="Connect Accounts Tab Panel"
orientation="horizontal"
className="mt-10"
>
<Tabs.List>
{[
{ title: 'Import a repository' },
{ title: 'Start with a template' },
].map(({ title }, index) => (
<Tabs.Trigger value={title} key={index}>
{title}
</Tabs.Trigger>
))}
</Tabs.List>
</Tabs>
);
};
@@ -1,12 +1,13 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { Button, Typography } from '@material-tailwind/react';
import { DeployStep, DeployStatus } from './DeployStep';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import ConfirmDialog from '../../shared/ConfirmDialog';
import ConfirmDialog from 'components/shared/ConfirmDialog';
const TIMEOUT_DURATION = 5000;
const Deploy = () => {
const [searchParams] = useSearchParams();
const projectId = searchParams.get('projectId');
@@ -21,6 +22,14 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`);
}, []);
useEffect(() => {
const timerID = setTimeout(() => {
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
}, TIMEOUT_DURATION);
return () => clearInterval(timerID);
}, []);
return (
<div>
<div className="flex justify-between mb-6">
@@ -34,7 +43,12 @@ const Deploy = () => {
</div>
</div>
<div>
<Button onClick={handleOpen} variant="outlined" size="sm">
<Button
onClick={handleOpen}
variant="outlined"
size="sm"
placeholder={''}
>
^ Cancel
</Button>
</div>
@@ -46,7 +60,7 @@ const Deploy = () => {
handleConfirm={handleCancel}
color="red"
>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
This will halt the deployment and you will have to start the process
from scratch.
</Typography>
@@ -74,14 +88,6 @@ const Deploy = () => {
status={DeployStatus.NOT_STARTED}
step="4"
/>
<Button
onClick={() => {
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
}}
>
VIEW DEMO
</Button>
</div>
);
};
@@ -62,7 +62,12 @@ const DeployStep = ({
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
{processLogs.map((log, key) => {
return (
<Typography variant="small" color="gray" key={key}>
<Typography
variant="small"
color="gray"
key={key}
placeholder={''}
>
{log}
</Typography>
);
@@ -75,6 +80,7 @@ const DeployStep = ({
toast.success('Logs copied');
}}
color="blue"
placeholder={''}
>
^ Copy log
</Button>
@@ -1,7 +1,8 @@
import React, { useCallback } from 'react';
import toast from 'react-hot-toast';
import { useNavigate, useParams } from 'react-router-dom';
import { Chip, IconButton } from '@material-tailwind/react';
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
import { relativeTimeISO } from '../../../utils/time';
import { GitRepositoryDetails } from '../../../types';
@@ -14,6 +15,7 @@ interface ProjectRepoCardProps {
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
const client = useGQLClient();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState(false);
const { orgSlug } = useParams();
@@ -22,6 +24,7 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
return;
}
setIsLoading(true);
const { addProject } = await client.addProject(orgSlug!, {
name: `${repository.owner!.login}-${repository.name}`,
prodBranch: repository.default_branch!,
@@ -30,7 +33,13 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
template: 'webapp',
});
navigate(`import?projectId=${addProject.id}`);
if (Boolean(addProject)) {
setIsLoading(false);
navigate(`import?projectId=${addProject.id}`);
} else {
setIsLoading(false);
toast.error('Failed to create project');
}
}, [client, repository]);
return (
@@ -53,9 +62,15 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
</div>
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
</div>
<div className="hidden group-hover:block">
<IconButton size="sm">{'>'}</IconButton>
</div>
{isLoading ? (
<Spinner className="h-4 w-4" />
) : (
<div className="hidden group-hover:block">
<IconButton size="sm" placeholder={''}>
{'>'}
</IconButton>
</div>
)}
</div>
);
};
@@ -3,11 +3,12 @@ import { Octokit } from 'octokit';
import assert from 'assert';
import { useDebounce } from 'usehooks-ts';
import { Button, Typography, Option, Select } from '@material-tailwind/react';
import { Button, Typography, Option } from '@material-tailwind/react';
import SearchBar from '../../SearchBar';
import ProjectRepoCard from './ProjectRepoCard';
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
import AsyncSelect from '../../shared/AsyncSelect';
const DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5;
@@ -109,8 +110,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
<div className="p-4">
<div className="flex gap-2 mb-2">
<div className="basis-1/3">
{/* TODO: Fix selection of Git user at start */}
<Select
<AsyncSelect
value={selectedAccount}
onChange={(value) => setSelectedAccount(value!)}
>
@@ -119,7 +119,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
^ {account.login}
</Option>
))}
</Select>
</AsyncSelect>
</div>
<div className="basis-2/3">
<SearchBar
@@ -136,11 +136,12 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
) : (
<div className="mt-4 p-6 flex items-center justify-center">
<div className="text-center">
<Typography>No repository found</Typography>
<Typography placeholder={''}>No repository found</Typography>
<Button
className="rounded-full mt-5"
size="sm"
onClick={handleResetFilters}
placeholder={''}
>
^ Reset filters
</Button>
@@ -18,11 +18,15 @@ interface TemplateCardProps {
const CardDetails = ({ template }: { template: TemplateDetails }) => {
return (
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
<Typography className="grow">
<Typography className="grow" placeholder={''}>
{template.icon} {template.name}
</Typography>
<div>
<IconButton size="sm" className="rounded-full hidden group-hover:block">
<IconButton
size="sm"
className="rounded-full hidden group-hover:block"
placeholder={''}
>
{'>'}
</IconButton>
</div>
@@ -1,6 +1,6 @@
import React from 'react';
import { Typography, IconButton } from '@material-tailwind/react';
import { Typography, IconButton, Avatar } from '@material-tailwind/react';
import { relativeTimeISO } from '../../../utils/time';
import { GitCommitWithBranch } from '../../../types';
@@ -11,21 +11,31 @@ interface ActivityCardProps {
const ActivityCard = ({ activity }: ActivityCardProps) => {
return (
<div className="group flex hover:bg-gray-200 rounded mt-1">
<div className="w-4">^</div>
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
<div className="w-8">
<Avatar
src={activity.author?.avatar_url}
variant="rounded"
size="sm"
placeholder={''}
/>
</div>
<div className="grow">
<Typography>{activity.commit.author?.name}</Typography>
<Typography variant="small" color="gray">
<Typography placeholder={''}>{activity.commit.author?.name}</Typography>
<Typography variant="small" color="gray" placeholder={''}>
{relativeTimeISO(activity.commit.author!.date!)} ^{' '}
{activity.branch.name}
</Typography>
<Typography variant="small" color="gray">
<Typography variant="small" color="gray" placeholder={''}>
{activity.commit.message}
</Typography>
</div>
<div className="mr-2 self-center hidden group-hover:block">
<IconButton size="sm" className="rounded-full bg-gray-600">
<IconButton
size="sm"
className="rounded-full bg-gray-600"
placeholder={''}
>
{'>'}
</IconButton>
</div>
@@ -17,9 +17,9 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader>Assign Domain</DialogHeader>
<DialogBody>
<Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader placeholder={''}>Assign Domain</DialogHeader>
<DialogBody placeholder={''}>
In order to assign a domain to your production deployments, configure it
in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */}
@@ -36,12 +36,13 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
theme={atomOneLight}
/>
</DialogBody>
<DialogFooter className="flex justify-start">
<DialogFooter className="flex justify-start" placeholder={''}>
<Button
className="rounded-3xl"
variant="gradient"
color="blue"
onClick={handleOpen}
placeholder={''}
>
<span>Okay</span>
</Button>
@@ -16,6 +16,7 @@ import {
Typography,
Chip,
ChipProps,
Tooltip,
} from '@material-tailwind/react';
import { relativeTimeMs } from '../../../../utils/time';
@@ -24,6 +25,7 @@ import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
import AssignDomainDialog from './AssignDomainDialog';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import { formatAddress } from '../../../../utils/format';
interface DeployDetailsCardProps {
deployment: Deployment;
@@ -87,54 +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">
<Typography className=" basis-3/4">{deployment.url}</Typography>
<Chip
value={deployment.status}
color={STATUS_COLORS[deployment.status] ?? 'gray'}
variant="ghost"
icon={<i>^</i>}
/>
{deployment.url && (
<Typography className="basis-3/4" placeholder={''}>
{deployment.url}
</Typography>
)}
</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>
<a
href={'https://' + deployment.url}
target="_blank"
rel="noreferrer"
>
<MenuItem>^ Visit</MenuItem>
<MenuList placeholder={''}>
<a href={deployment.url} target="_blank" rel="noreferrer">
<MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
^ Visit
</MenuItem>
</a>
<MenuItem
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
placeholder={''}
>
^ Assign domain
</MenuItem>
<MenuItem
onClick={() => setChangeToProduction(!changeToProduction)}
disabled={!(deployment.environment !== Environment.Production)}
placeholder={''}
>
^ Change to production
</MenuItem>
@@ -147,6 +160,7 @@ const DeploymentDetailsCard = ({
deployment.isCurrent
)
}
placeholder={''}
>
^ Redeploy to production
</MenuItem>
@@ -157,6 +171,7 @@ const DeploymentDetailsCard = ({
deployment.environment !== Environment.Production ||
!Boolean(currentDeployment)
}
placeholder={''}
>
^ Rollback to this version
</MenuItem>
@@ -175,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>
);
@@ -204,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>
)}
@@ -232,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>
@@ -250,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`}
@@ -28,16 +29,19 @@ const DeploymentDialogBodyCard = ({
color={chip.color}
/>
)}
<Typography variant="small" className="text-black">
{deployment.url}
</Typography>
<Typography variant="small">
{deployment.url && (
<Typography variant="small" className="text-black" placeholder={''}>
{deployment.url}
</Typography>
)}
<Typography variant="small" placeholder={''}>
^ {deployment.branch} ^{' '}
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
{deployment.commitMessage}
</Typography>
<Typography variant="small">
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
<Typography variant="small" placeholder={''}>
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
{formatAddress(deployment.createdBy.name ?? '')}
</Typography>
</Card>
);
@@ -44,18 +44,18 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
}, [value]);
return (
<div className="grid grid-cols-4 gap-2 text-sm text-gray-600">
<div className="col-span-2">
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
<div className="col-span-4">
<SearchBar
placeholder="Search branches"
value={searchedBranch}
onChange={(event) => setSearchedBranch(event.target.value)}
/>
</div>
<div className="col-span-1">
<div className="col-span-2">
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
</div>
<div className="col-span-1 relative">
<div className="col-span-2 relative">
<Select
value={selectedStatus}
onChange={(value) => setSelectedStatus(value as StatusOptions)}
@@ -77,6 +77,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
className="rounded-full"
size="sm"
placeholder={''}
>
X
</IconButton>
@@ -21,7 +21,9 @@ const AddEnvironmentVariableRow = ({
return (
<div className="flex gap-1 p-2">
<div>
<Typography variant="small">Key</Typography>
<Typography variant="small" placeholder={''}>
Key
</Typography>
<Input
crossOrigin={undefined}
{...register(`variables.${index}.key`, {
@@ -30,7 +32,9 @@ const AddEnvironmentVariableRow = ({
/>
</div>
<div>
<Typography variant="small">Value</Typography>
<Typography variant="small" placeholder={''}>
Value
</Typography>
<Input
crossOrigin={undefined}
{...register(`variables.${index}.value`, {
@@ -43,6 +47,7 @@ const AddEnvironmentVariableRow = ({
size="sm"
onClick={() => onDelete()}
disabled={isDeleteDisabled}
placeholder={''}
>
{'>'}
</IconButton>
@@ -61,23 +61,26 @@ const AddMemberDialog = ({
}, []);
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader className="flex justify-between">
<Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader className="flex justify-between" placeholder={''}>
<div>Add member</div>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1 rounded-3xl"
placeholder={''}
>
X
</Button>
</DialogHeader>
<form onSubmit={handleSubmit(submitHandler)}>
<DialogBody className="flex flex-col gap-2 p-4">
<Typography variant="small">
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
<Typography variant="small" placeholder={''}>
We will send an invitation link to this email address.
</Typography>
<Typography variant="small">Email address</Typography>
<Typography variant="small" placeholder={''}>
Email address
</Typography>
<Input
type="email"
crossOrigin={undefined}
@@ -85,8 +88,10 @@ const AddMemberDialog = ({
required: 'email field cannot be empty',
})}
/>
<Typography variant="small">Permissions</Typography>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Permissions
</Typography>
<Typography variant="small" placeholder={''}>
You can change this later if required.
</Typography>
<Checkbox
@@ -102,8 +107,13 @@ const AddMemberDialog = ({
color="blue"
/>
</DialogBody>
<DialogFooter className="flex justify-start">
<Button variant="outlined" onClick={handleOpen} className="mr-1">
<DialogFooter className="flex justify-start" placeholder={''}>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1"
placeholder={''}
>
Cancel
</Button>
<Button
@@ -111,6 +121,7 @@ const AddMemberDialog = ({
color="blue"
type="submit"
disabled={!isValid}
placeholder={''}
>
Send invite
</Button>
@@ -53,20 +53,21 @@ const DeleteProjectDialog = ({
}, [client, project, handleOpen]);
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader className="flex justify-between">
<Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader className="flex justify-between" placeholder={''}>
<div>Delete project?</div>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1 rounded-3xl"
placeholder={''}
>
X
</Button>
</DialogHeader>
<form onSubmit={handleSubmit(deleteProjectHandler)}>
<DialogBody className="flex flex-col gap-2">
<Typography variant="paragraph">
<DialogBody className="flex flex-col gap-2" placeholder={''}>
<Typography variant="paragraph" placeholder={''}>
Deleting your project is irreversible. Enter your projects
name&nbsp;
<span className="bg-blue-100 text-blue-700">({project.name})</span>
@@ -80,12 +81,17 @@ const DeleteProjectDialog = ({
validate: (value) => value === project.name,
})}
/>
<Typography variant="small" color="red">
<Typography variant="small" color="red" placeholder={''}>
^ Deleting your project is irreversible.
</Typography>
</DialogBody>
<DialogFooter className="flex justify-start">
<Button variant="outlined" onClick={handleOpen} className="mr-1">
<DialogFooter className="flex justify-start" placeholder={''}>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1"
placeholder={''}
>
Cancel
</Button>
<Button
@@ -93,6 +99,7 @@ const DeleteProjectDialog = ({
color="red"
type="submit"
disabled={!isValid}
placeholder={''}
>
Yes, Delete project
</Button>
@@ -29,11 +29,11 @@ const DisplayEnvironmentVariables = ({
</div>
<Collapse open={openCollapse}>
{variables.length === 0 ? (
<Card className="bg-gray-300 flex items-center p-4">
<Typography variant="small" className="text-black">
<Card className="bg-gray-300 flex items-center p-4" placeholder={''}>
<Typography variant="small" className="text-black" placeholder={''}>
No environment variables added yet.
</Typography>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Once you add them, theyll show up here.
</Typography>
</Card>
@@ -12,7 +12,6 @@ import {
Card,
} from '@material-tailwind/react';
import { RepositoryDetails } from '../../../../types';
import ConfirmDialog from '../../../shared/ConfirmDialog';
import EditDomainDialog from './EditDomainDialog';
import { useGQLClient } from '../../../../context/GQLClientContext';
@@ -27,7 +26,7 @@ enum RefreshStatus {
interface DomainCardProps {
domains: Domain[];
domain: Domain;
repo: RepositoryDetails;
branches: string[];
project: Project;
onUpdate: () => Promise<void>;
}
@@ -44,7 +43,7 @@ const DOMAIN_RECORD = {
const DomainCard = ({
domains,
domain,
repo,
branches,
project,
onUpdate,
}: DomainCardProps) => {
@@ -69,7 +68,7 @@ const DomainCard = ({
<>
<div className="flex justify-between py-3">
<div className="flex justify-start gap-1">
<Typography variant="h6">
<Typography variant="h6" placeholder={''}>
<i>^</i> {domain.name}
</Typography>
<Chip
@@ -98,18 +97,20 @@ const DomainCard = ({
<MenuHandler>
<button className="border-2 rounded-full w-8 h-8">...</button>
</MenuHandler>
<MenuList>
<MenuList placeholder={''}>
<MenuItem
className="text-black"
onClick={() => {
setEditDialogOpen((preVal) => !preVal);
}}
placeholder={''}
>
^ Edit domain
</MenuItem>
<MenuItem
className="text-red-500"
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
placeholder={''}
>
^ Delete domain
</MenuItem>
@@ -128,7 +129,7 @@ const DomainCard = ({
}}
color="red"
>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Once deleted, the project{' '}
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
{project.name}
@@ -141,15 +142,21 @@ const DomainCard = ({
</ConfirmDialog>
</div>
<Typography variant="small">Production</Typography>
<Typography variant="small" placeholder={''}>
Production
</Typography>
{domain.status === DomainStatus.Pending && (
<Card className="bg-gray-200 p-4 text-sm">
<Card className="bg-gray-200 p-4 text-sm" placeholder={''}>
{refreshStatus === RefreshStatus.IDLE ? (
<Typography variant="small">
<Typography variant="small" placeholder={''}>
^ Add these records to your domain and refresh to check
</Typography>
) : refreshStatus === RefreshStatus.CHECKING ? (
<Typography variant="small" className="text-blue-500">
<Typography
variant="small"
className="text-blue-500"
placeholder={''}
>
^ Checking records for {domain.name}
</Typography>
) : (
@@ -188,7 +195,7 @@ const DomainCard = ({
domains={domains}
open={editDialogOpen}
domain={domain}
repo={repo}
branches={branches}
onUpdate={onUpdate}
/>
</>
@@ -15,7 +15,6 @@ import {
Option,
} from '@material-tailwind/react';
import { RepositoryDetails } from '../../../../types';
import { useGQLClient } from '../../../../context/GQLClientContext';
const DEFAULT_REDIRECT_OPTIONS = ['none'];
@@ -25,7 +24,7 @@ interface EditDomainDialogProp {
open: boolean;
handleOpen: () => void;
domain: Domain;
repo: RepositoryDetails;
branches: string[];
onUpdate: () => Promise<void>;
}
@@ -40,7 +39,7 @@ const EditDomainDialog = ({
open,
handleOpen,
domain,
repo,
branches,
onUpdate,
}: EditDomainDialogProp) => {
const client = useGQLClient();
@@ -120,30 +119,35 @@ const EditDomainDialog = ({
branch: domain.branch,
redirectedTo: getRedirectUrl(domain),
});
}, [domain, repo]);
}, [domain]);
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader className="flex justify-between">
<Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader className="flex justify-between" placeholder={''}>
<div>Edit domain</div>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1 rounded-3xl"
placeholder={''}
>
X
</Button>
</DialogHeader>
<form onSubmit={handleSubmit(updateDomainHandler)}>
<DialogBody className="flex flex-col gap-2 p-4">
<Typography variant="small">Domain name</Typography>
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
<Typography variant="small" placeholder={''}>
Domain name
</Typography>
<Input crossOrigin={undefined} {...register('name')} />
<Typography variant="small">Redirect to</Typography>
<Typography variant="small" placeholder={''}>
Redirect to
</Typography>
<Controller
name="redirectedTo"
control={control}
render={({ field }) => (
<Select {...field} disabled={isDisableDropdown}>
<Select {...field} disabled={isDisableDropdown} placeholder={''}>
{redirectOptions.map((option, key) => (
<Option key={key} value={option}>
^ {option}
@@ -155,29 +159,44 @@ const EditDomainDialog = ({
{isDisableDropdown && (
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
<div>^</div>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Domain {domainRedirectedFrom ? domainRedirectedFrom.name : ''}
redirects to this domain so you can not redirect this doman
further.
</Typography>
</div>
)}
<Typography variant="small">Git branch</Typography>
<Typography variant="small" placeholder={''}>
Git branch
</Typography>
<Input
crossOrigin={undefined}
{...register('branch', {
validate: (value) => repo.branch.includes(value),
validate: (value) =>
Boolean(branches.length) ? branches.includes(value) : true,
})}
disabled={watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]}
disabled={
!Boolean(branches.length) ||
watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]
}
/>
{!isValid && (
<Typography variant="small" className="text-red-500">
<Typography
variant="small"
className="text-red-500"
placeholder={''}
>
We couldn&apos;t find this branch in the connected Git repository.
</Typography>
)}
</DialogBody>
<DialogFooter className="flex justify-start">
<Button variant="outlined" onClick={handleOpen} className="mr-1">
<DialogFooter className="flex justify-start" placeholder={''}>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1"
placeholder={''}
>
Cancel
</Button>
<Button
@@ -185,6 +204,7 @@ const EditDomainDialog = ({
color="blue"
type="submit"
disabled={!isDirty}
placeholder={''}
>
Save changes
</Button>
@@ -84,7 +84,9 @@ const EditEnvironmentVariableRow = ({
<>
<div className="flex gap-1 p-2">
<div>
<Typography variant="small">Key</Typography>
<Typography variant="small" placeholder={''}>
Key
</Typography>
<Input
crossOrigin={undefined}
disabled={!edit}
@@ -92,7 +94,9 @@ const EditEnvironmentVariableRow = ({
/>
</div>
<div>
<Typography variant="small">Value</Typography>
<Typography variant="small" placeholder={''}>
Value
</Typography>
<Input
crossOrigin={undefined}
disabled={!edit}
@@ -114,6 +118,7 @@ const EditEnvironmentVariableRow = ({
<IconButton
onClick={handleSubmit(updateEnvironmentVariableHandler)}
size="sm"
placeholder={''}
>
{'S'}
</IconButton>
@@ -125,6 +130,7 @@ const EditEnvironmentVariableRow = ({
reset();
setEdit((preVal) => !preVal);
}}
placeholder={''}
>
{'C'}
</IconButton>
@@ -138,6 +144,7 @@ const EditEnvironmentVariableRow = ({
onClick={() => {
setEdit((preVal) => !preVal);
}}
placeholder={''}
>
{'E'}
</IconButton>
@@ -146,6 +153,7 @@ const EditEnvironmentVariableRow = ({
<IconButton
size="sm"
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
placeholder={''}
>
{'D'}
</IconButton>
@@ -162,7 +170,7 @@ const EditEnvironmentVariableRow = ({
handleConfirm={removeEnvironmentVariableHandler}
color="red"
>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Are you sure you want to delete the variable&nbsp;
<span className="bg-blue-100">{variable.key}</span>?
</Typography>
@@ -7,9 +7,11 @@ import {
Option,
Chip,
IconButton,
Tooltip,
} from '@material-tailwind/react';
import ConfirmDialog from '../../../shared/ConfirmDialog';
import { formatAddress } from '../../../../utils/format';
const PERMISSION_OPTIONS = [
{
@@ -48,6 +50,7 @@ const MemberCard = ({
onRemoveProjectMember,
onUpdateProjectMember,
}: MemberCardProps) => {
const [ethAddress, emailDomain] = member.email.split('@');
const [selectedPermission, setSelectedPermission] = useState(
permissions.join('+'),
);
@@ -79,8 +82,16 @@ const MemberCard = ({
>
<div>^</div>
<div className="basis-1/2">
{member.name && <Typography variant="small">{member.name}</Typography>}
<Typography variant="small">{member.email}</Typography>
{member.name && (
<Tooltip content={member.name}>
{formatAddress(member.name ?? '')}
</Tooltip>
)}
<Tooltip content={member.email}>
<p>
{formatAddress(ethAddress)}@{emailDomain}
</p>
</Tooltip>
</div>
<div className="basis-1/2">
{!isPending ? (
@@ -93,6 +104,7 @@ const MemberCard = ({
selected={(_, index) => (
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
)}
placeholder={''}
>
{DROPDOWN_OPTIONS.map((permission, key) => (
<Option key={key} value={permission.value}>
@@ -121,6 +133,7 @@ const MemberCard = ({
onClick={() => {
setRemoveMemberDialogOpen((prevVal) => !prevVal);
}}
placeholder={''}
>
D
</IconButton>
@@ -141,9 +154,10 @@ const MemberCard = ({
}}
color="red"
>
<Typography variant="small">
Once removed, {member.name} ({member.email}) will not be able to
access this project.
<Typography variant="small" placeholder={''}>
Once removed, {formatAddress(member.name ?? '')} (
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
this project.
</Typography>
</ConfirmDialog>
</div>
@@ -17,14 +17,19 @@ const RepoConnectedSection = ({
<div className="flex gap-4">
<div>^</div>
<div className="grow">
<Typography variant="small">{linkedRepo.full_name}</Typography>
<Typography variant="small">Connected just now</Typography>
<Typography variant="small" placeholder={''}>
{linkedRepo.full_name}
</Typography>
<Typography variant="small" placeholder={''}>
Connected just now
</Typography>
</div>
<div>
<Button
onClick={() => setDisconnectRepoDialogOpen(true)}
variant="outlined"
size="sm"
placeholder={''}
>
^ Disconnect
</Button>
@@ -39,7 +44,7 @@ const RepoConnectedSection = ({
}}
color="red"
>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Any data tied to your Git project may become misconfigured. Are you
sure you want to continue?
</Typography>
@@ -54,14 +54,18 @@ const SetupDomain = () => {
className="flex flex-col gap-6 w-full"
>
<div>
<Typography variant="h5">Setup domain name</Typography>
<Typography variant="small">
<Typography variant="h5" placeholder={''}>
Setup domain name
</Typography>
<Typography variant="small" placeholder={''}>
Add your domain and setup redirects
</Typography>
</div>
<div className="w-auto">
<Typography variant="small">Domain name</Typography>
<Typography variant="small" placeholder={''}>
Domain name
</Typography>
<Input
type="text"
variant="outlined"
@@ -76,7 +80,7 @@ const SetupDomain = () => {
{isValid && (
<div>
<Typography>Primary domain</Typography>
<Typography placeholder={''}>Primary domain</Typography>
<div className="flex flex-col gap-3">
<Radio
label={domainStr}
@@ -108,6 +112,7 @@ const SetupDomain = () => {
className="w-fit"
color={isValid ? 'blue' : 'gray'}
type="submit"
placeholder={''}
>
<i>^</i> Next
</Button>
@@ -23,6 +23,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
navigator.clipboard.writeText(webhookUrl);
toast.success('Copied to clipboard');
}}
placeholder={''}
>
C
</Button>
@@ -32,6 +33,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
onClick={() => {
setDeleteDialogOpen(true);
}}
placeholder={''}
>
X
</Button>
@@ -48,7 +50,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
}}
color="red"
>
<Typography variant="small">
<Typography variant="small" placeholder={''}>
Are you sure you want to delete the variable{' '}
<span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
</Typography>
@@ -9,7 +9,7 @@ const AsyncSelect = React.forwardRef((props: SelectProps, ref: any) => {
useEffect(() => setKey((preVal) => preVal + 1), [props]);
return <Select key={key} ref={ref} {...props} />;
return <Select key={key} ref={ref} {...props} placeholder={''} />;
});
AsyncSelect.displayName = 'AsyncSelect';

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