Compare commits

..
Author SHA1 Message Date
Eric Lewis 9f25059c3c feat: see all button functions 2024-04-09 14:34:04 -04:00
Eric Lewis f38dfb5604 chore: bump deploy 2024-04-04 10:50:39 -04:00
nabarunandneeraj 351db16336 Upgrade laconic-sdk package for showing error with reason in console (#165)
* Upgrade laconic sdk version

* Update toast message for add project

* Log error messages

* Update bond id in readme

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-04-03 20:26:34 +05:30
Wahyu Kurniawan 78f04c3669 Merge pull request #162 from snowball-tools/ayungavis/T-4943-product-reskin-confirmdialog-styling 2024-03-25 23:04:22 +07:00
Vivian Phung 44a6a8902f Merge pull request #140 from snowball-tools/readme-cleanup
readme
2024-03-15 15:05:36 -04:00
Wahyu Kurniawan 4fcf8e92b7 ♻️ refactor: create another example page for modal only 2024-03-14 22:07:36 +07:00
Wahyu Kurniawan 193dbb058a 🎨 style: adjust overflow text width on mobile 2024-03-14 22:07:05 +07:00
Wahyu Kurniawan 8834de893e 📝 docs: put all the new dialog into component page 2024-03-14 21:58:04 +07:00
Wahyu Kurniawan 9b3ac4654f ♻️ refactor: create transfer project dialog component 2024-03-14 21:57:26 +07:00
Wahyu Kurniawan e4fdae3329 ♻️ refactor: create disconnect repository dialog component 2024-03-14 21:57:00 +07:00
Wahyu Kurniawan 4c936b1eb7 ♻️ refactor: create remove member dialog component 2024-03-14 21:56:29 +07:00
Wahyu Kurniawan f5807c1126 ♻️ refactor: create delete webhook dialog component 2024-03-14 21:56:00 +07:00
Wahyu Kurniawan c1696fbf48 ♻️ refactor: create delete variable dialog component 2024-03-14 21:55:30 +07:00
Wahyu Kurniawan 45e8e9a7f4 ♻️ refactor: create delete domain dialog component 2024-03-14 21:55:02 +07:00
Wahyu Kurniawan 28740ffbee ♻️ refactor: create change state to production dialog reusable component 2024-03-14 21:54:30 +07:00
Wahyu Kurniawan 97289d85a3 ♻️ refactor: create cancel deployment dialog component 2024-03-14 21:53:48 +07:00
Wahyu Kurniawan f3ce0d0621 🎨 style: adjust deployment body card design to match with the figma 2024-03-14 21:52:55 +07:00
Wahyu Kurniawan c2417a9daa ️ feat: export the tag props 2024-03-14 21:52:07 +07:00
Wahyu Kurniawan d50a318e16 ️ feat: create chevron doublw down icon component 2024-03-14 21:50:11 +07:00
Wahyu Kurniawan 3674750011 ️ feat: update confirm dialog to use new modal component 2024-03-14 21:49:45 +07:00
Wahyu Kurniawan 102c861617 🎨 style: adjust modal theme and add wavy border on the header 2024-03-14 21:49:13 +07:00
Wahyu Kurniawan 56e9be59ad 🐛 fix: missing props 2024-03-08 15:32:11 +07:00
Wahyu Kurniawan 300b8e4b5e 📝 docs: add modal to the example page 2024-03-08 15:30:53 +07:00
Wahyu Kurniawan 296e149391 🎨 style: update confirm dialog to use custom modal component 2024-03-08 15:29:06 +07:00
Wahyu Kurniawan cbda1cc652 🎨 style: add new keyframe animation for modal 2024-03-08 15:28:23 +07:00
Wahyu Kurniawan 16e7b22507 ️ feat: create modal comopnent 2024-03-08 15:27:53 +07:00
Wahyu Kurniawan 93fb696d5c Merge pull request #161 from snowball-tools/ayungavis/T-4936-make-deployed-lines-responsive
[T-4936: style] Make deployed lines responsive
2024-03-08 14:54:27 +07:00
Wahyu Kurniawan 32305cedbc 🎨 style: add overflow for the content 2024-03-08 12:44:48 +07:00
Wahyu Kurniawan 519618b456 ♻️ refactor: remove duplicate component and use absolute for the mobile version 2024-03-08 12:42:42 +07:00
Andre Hadianto 4057480df2 Merge pull request #160 from snowball-tools/andrehadianto/T-4939-create-project-connect-github-assets
[T-4939] create project connect GitHub assets
2024-03-08 12:57:02 +08:00
Wahyu Kurniawan de197759de 🎨 style: remove width and height icon style from tag size 2024-03-08 10:36:14 +07:00
Wahyu Kurniawan 65b66fe383 ♻️ refactor: remove empty space 2024-03-08 10:35:22 +07:00
Wahyu Kurniawan c6ea3a8f53 🎨 style: adjust responsive for deployment details card 2024-03-08 09:58:26 +07:00
Wahyu Kurniawan 6d861c71cc 🎨 style: adjust filter form mobile 2024-03-08 09:57:50 +07:00
Andre Hadianto 1d96f6430f Merge pull request #159 from snowball-tools/andrehadianto/T-4968-qol-and-chores
[T-4968] qol and chores
2024-03-07 18:44:04 +08:00
Andre H b922e632bf 🔧 chore: add mock card to template 2024-03-07 18:23:43 +08:00
Andre H b1f318bbbd ️ feat: implement mock connect git card 2024-03-07 18:23:22 +08:00
Andre H 667deb78fb 🎨 style: tabs to use overflow-auto instead of overflow-scroll 2024-03-07 14:23:59 +08:00
Andre H 095008867f 🔧 chore: show loading state on fetching repo activities 2024-03-07 14:23:34 +08:00
Andre H 41033c5241 ️ feat: reskin "no repository found" component 2024-03-07 14:23:11 +08:00
Andre H cdb995205a 🎨 style: truncate overflow text on projectCard 2024-03-07 14:22:32 +08:00
Andre H 3a84832da5 🔧 chore: add tooltip on hover projectcard name 2024-03-07 14:22:10 +08:00
Wahyu Kurniawan 3fdc0b2dff Merge pull request #156 from snowball-tools/ayungavis/T-4931-layout-mobile-sidebar-layout
[T-4931: style] Mobile sidebar layout and search bar
2024-03-07 12:52:12 +07:00
Andre Hadianto 89da03cec6 Merge pull request #158 from snowball-tools/andrehadianto/T-4961-create-project-success-page
[T-4961] create project success page
2024-03-07 13:51:14 +08:00
Andre Hadianto 9b6cd9baae Merge pull request #157 from snowball-tools/andrehadianto/T-4944-layout-template-steps-layout
[T-4944] layout template steps layout
2024-03-07 12:24:29 +08:00
Andre H 658cc0b6b2 🔧 chore: change to isDesktopView for >960px 2024-03-07 12:18:41 +08:00
Andre H be8f0bc4ad 🔧 chore: use `` for text 2024-03-07 12:17:06 +08:00
Andre H 2a8fc30979 🔧 chore: add TODO 2024-03-07 12:16:23 +08:00
Andre H 4074dd4001 🔧 chore: use link-emphasized button 2024-03-07 12:16:03 +08:00
Andre H 028831f806 🔧 chore: change isDesktopView to isTabletView 2024-03-07 11:45:51 +08:00
Andre H 646099707c 🔧 chore: use lottie for success logo 2024-03-07 11:42:12 +08:00
Andre H 6b0548ec47 ⚒️ build: add lottie-react 2024-03-07 11:41:41 +08:00
Andre H 2ed07a6987 ️ feat: reskin success create project 2024-03-07 10:18:04 +08:00
Andre H e9ab034625 🔧 chore: add new icons 2024-03-07 10:17:30 +08:00
Wahyu Kurniawan 02500b3cba 🎨 style: make the sidebar scrollable 2024-03-06 21:03:01 +07:00
Wahyu Kurniawan 4c8d0e2436 🎨 style: change the background color of the avatar 2024-03-06 19:14:32 +07:00
Wahyu Kurniawan efa74898af 🎨 style: adjust padding of the content layout 2024-03-06 19:07:41 +07:00
Wahyu Kurniawan 532732943e ♻️ refactor: add empty space 2024-03-06 16:38:45 +07:00
Wahyu Kurniawan be4900e63f Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4931-layout-mobile-sidebar-layout 2024-03-06 16:28:50 +07:00
Wahyu Kurniawan 9e4e203f5f 🎨 style: remove bottom border radius for mobile 2024-03-06 15:44:35 +07:00
Wahyu Kurniawan d812cdf05f 🎨 style: adjust logo and mobile header padding on mobile 2024-03-06 15:39:46 +07:00
Wahyu Kurniawan b605c1bc5b 🎨 style: adjust media query minimum width for desktop to match with tailwind large responsive min width 2024-03-06 15:27:29 +07:00
Wahyu Kurniawan 8d3ef369bb ♻️ refactor: change project bar item component to button 2024-03-06 15:14:09 +07:00
Wahyu Kurniawan 7aac8cdcef 🚀 perf: change the debounce to 300 ms 2024-03-06 15:11:16 +07:00
Wahyu Kurniawan 6c31113bca 🎨 style: adjust grid width of the project list 2024-03-06 15:08:45 +07:00
Andre Hadianto 30ecd41975 Merge pull request #141 from snowball-tools/andrehadianto/T-4904-home-org-switcher
[T-4904] home org switcher
2024-03-06 15:01:39 +08:00
Andre Hadianto 410375f8c7 Merge branch 'main' into andrehadianto/T-4904-home-org-switcher 2024-03-06 15:01:29 +08:00
Andre H 226d02ea59 🔧 chore: change media query to lg 2024-03-06 14:29:28 +08:00
Wahyu Kurniawan ca64d00355 🎨 style: adjust grid for the project list 2024-03-06 13:22:39 +07:00
Wahyu Kurniawan 287fe360d3 ♻️ refactor: use useMemo to render organization and sidebar menu 2024-03-06 13:22:17 +07:00
Andre H f23c757de9 🔧 chore: create project create repo buttonSize to be responsive 2024-03-06 14:15:19 +08:00
Andre H 3737550f7f 🔧 chore: useMediaQuery change md to >720px 2024-03-06 14:14:49 +08:00
Andre H 274762cbfe ️ feat: create project mobile layout 2024-03-06 14:14:10 +08:00
Wahyu Kurniawan 90295fd620 ♻️ refactor: use dialog overlay instead of div 2024-03-06 13:09:23 +07:00
Wahyu Kurniawan b75957929a 🎨 style: fixed content moves down issue when opening the sidebar 2024-03-06 12:46:49 +07:00
Wahyu Kurniawan daffcf1203 🎨 style: adjust mobile sidebar animation 2024-03-06 11:53:44 +07:00
Wahyu Kurniawan eccf78afdb 🎨 style: add background tot the project search header 2024-03-06 11:40:07 +07:00
Wahyu Kurniawan 0901036f71 Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4931-layout-mobile-sidebar-layout 2024-03-06 11:31:37 +07:00
Wahyu Kurniawan 8d3ef3bafc Merge pull request #142 from snowball-tools/ayungavis/T-4917-project-deployments-layout-and-empty-state
[T-4917: style] Re-styling and refactor project page deployments layout and empty state
2024-03-06 11:29:21 +07:00
Wahyu Kurniawan b6e02fb19d 🐛 fix: on reset error when value is undefined 2024-03-06 11:23:25 +07:00
Wahyu Kurniawan 462d247a86 Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4917-project-deployments-layout-and-empty-state 2024-03-06 11:18:38 +07:00
Wahyu Kurniawan 80097a32ac ️ feat: make the default status to undefined 2024-03-06 11:17:08 +07:00
Wahyu Kurniawan a80d40156f 🎨 style: remove bouncing animation 2024-03-06 11:11:16 +07:00
Wahyu Kurniawan ced50bf7f2 🎨 style: add animation to the layout 2024-03-06 10:55:37 +07:00
Wahyu Kurniawan 90b583aa19 Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4931-layout-mobile-sidebar-layout 2024-03-06 10:54:24 +07:00
Wahyu Kurniawan 48e3581322 ️ feat: add handler when click the project item on mobile 2024-03-06 10:42:06 +07:00
Wahyu Kurniawan 1ddc3b81c7 ️ feat: add search dialog on mobile 2024-03-06 10:31:11 +07:00
Wahyu Kurniawan 5a86abd133 🎨 style: adjust input and search bar input style 2024-03-06 10:30:50 +07:00
Wahyu Kurniawan 6ddf44cddd ♻️ refactor: project search bar 2024-03-06 10:30:26 +07:00
Wahyu Kurniawan b03200c256 🎨 style: add animation when sidebar open on mobile 2024-03-06 09:34:41 +07:00
Wahyu Kurniawan 1648deb64f 🔧 chore: install framer-motion 2024-03-06 09:34:06 +07:00
Andre Hadianto 621ca8926e Merge pull request #150 from snowball-tools/andrehadianto/T-4912-create-project-create-repository-page
[T-4912] create project create repository page
2024-03-06 09:39:33 +08:00
Andre H 8c0162f9f3 🔧 chore: replace Avatar with TemplateIcon on Template 2024-03-06 09:35:18 +08:00
Wahyu Kurniawan e05e2c63c2 🎨 style: adjust sidebar for mobile 2024-03-06 08:25:29 +07:00
Wahyu Kurniawan 3b1f03bcb6 🎨 style: replacee grid of the layout to use flex and fixed width of the sidebar 2024-03-06 08:24:11 +07:00
Wahyu Kurniawan 80fee1f585 ️ feat: create logout icon 2024-03-06 08:23:09 +07:00
Andre Hadianto d50f559a07 Merge branch 'main' into andrehadianto/T-4912-create-project-create-repository-page 2024-03-06 09:04:28 +08:00
Andre Hadianto 46d44378c5 Merge pull request #155 from snowball-tools/andrehadianto/T-4940-project-overview-mobile-layout
[T-4940] project overview mobile layout
2024-03-06 09:02:02 +08:00
Wahyu Kurniawan 3e42899f2e Merge pull request #138 from snowball-tools/ayungavis/T-4911-create-project-import-a-repository-section
[T-4911: feat] Re-styling and refactor create project import a repository section
2024-03-05 22:41:43 +07:00
Andre Hadianto 75a048c7a9 Merge pull request #153 from snowball-tools/andrehadianto/T-4935-timeline-component
[T-4935] timeline component
2024-03-05 18:32:08 +08:00
Wahyu Kurniawan b5f50fe16f 🎨 style: adjust layout for mobile 2024-03-05 16:16:45 +07:00
Wahyu Kurniawan aa4094669d ️ feat: create logo component 2024-03-05 16:16:20 +07:00
Wahyu Kurniawan 33fbdf60ae ️ feat: create some icons needed 2024-03-05 16:15:37 +07:00
Wahyu Kurniawan abe1401a6d 🎨 style: adjust the style of project search bar 2024-03-05 16:15:11 +07:00
Andre H 2182beca22 🔧 chore: remove database tab 2024-03-05 14:22:59 +08:00
Andre H 96f519d5e3 ️ feat: mobile layout for project overview 2024-03-05 14:19:47 +08:00
Andre H f6db1e119b 🔧 chore: add overflow-scroll to Tabs component 2024-03-05 14:18:51 +08:00
Andre H c079a9c336 🔧 chore: prevent Avatar from shrinking 2024-03-05 14:18:31 +08:00
Andre H f9ac778e47 ⚒️ build: add usehooks-ts 2024-03-05 14:16:54 +08:00
Andre H e8ae417772 🔧 chore: whitespace nowrap to prevent text warping on steps 2024-03-05 09:39:00 +08:00
Wahyu Kurniawan 18dde52c9a ️ feat: update select input default icon and options icon 2024-03-05 05:07:21 +07:00
Wahyu Kurniawan d9bae720d7 ️ feat: update date picker input icon 2024-03-05 05:06:53 +07:00
Wahyu Kurniawan e9f32ff668 ♻️ refactor: make the conditional logic to boolean 2024-03-05 05:06:28 +07:00
Wahyu Kurniawan 8a1e84386a ️ feat: create some icons needed 2024-03-05 05:05:54 +07:00
Wahyu Kurniawan 3335c26f82 Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4917-project-deployments-layout-and-empty-state 2024-03-05 04:37:49 +07:00
Andre H 0a2686d5ec 🔧 chore: add divider to render page 2024-03-04 17:50:23 +08:00
Andre H c1da92bc99 🔧 chore: adjust design to figma 2024-03-04 17:50:04 +08:00
Andre H add2559860 🔧 chore: adjust other CheckRoundFilledIcon user 2024-03-04 17:41:33 +08:00
Andre H 460f8a80b8 🔧 chore: replace Stepper with Steps component 2024-03-04 11:52:55 +08:00
Andre H f5296e9b8d 🔧 chore: first index to be '1' for Steps 2024-03-04 11:52:31 +08:00
Andre H 0aeea36dbd Merge remote-tracking branch 'origin/main' into andrehadianto/T-4935-timeline-component 2024-03-04 11:46:27 +08:00
Andre H 8f0c39022d 🔧 chore: add render component 2024-03-04 10:40:06 +08:00
Andre H 6c42a22972 ️ feat: implement steps/ timeline 2024-03-04 10:38:12 +08:00
Andre H adb64f2db1 🔧 chore: remove outer padding on the icon 2024-03-04 10:14:21 +08:00
Zachery 64e3aa5b25 [T-4913] Create Project - Deploy page (#152) 2024-03-03 13:41:25 +08:00
Zachery 4fc654f763 [T-4933] Create Project - Mobile modal layout (#151)
* feat: create project mobile modal layout

* style: make dialog content stretch full height
2024-03-01 15:22:16 +08:00
Andre H be0ea904ee 🔧 chore: REMOVE framework for now 2024-03-01 14:07:12 +08:00
Andre H 8114b10932 ♻️ refactor: use shared Avatar 2024-03-01 14:05:17 +08:00
Andre H 0ee27df25e 🔧 chore: font hierarchy 2024-03-01 14:00:41 +08:00
Andre H 893a499be5 🔧 chore: dont render when props not provided 2024-03-01 14:00:08 +08:00
Andre H 596d8eb326 🔧 chore: add flex to Input and Select 2024-03-01 13:49:30 +08:00
Andre H e9d38e0d3b 🔧 chore: add htmlFor to label in radioItem 2024-03-01 13:48:54 +08:00
Andre H ab77ce681a 🔧 chore: radioItem leftIcon use span 2024-03-01 13:48:33 +08:00
Vivian Phung 5be6e14db9 Update README.md 2024-02-29 21:59:41 -07:00
Sushan Yadav 409b654f9b Project Deployments - Deployed line items (#147)
* feat: add deployment lines

* fix: typo DeploymentMenu
2024-03-01 10:05:50 +05:45
Andre H 4a86952810 🔧 chore: prevent 'gap' due to empty div on Select 2024-03-01 11:26:38 +08:00
Andre H 207b32ce65 ️ feat: integrate useform 2024-03-01 11:23:38 +08:00
Andre H 6e653774be 🔧 chore: reskin with shared components 2024-03-01 11:13:17 +08:00
Andre H 0b64ccc364 🔧 chore: replace space-y-2 with gap-y-2 for input 2024-03-01 11:12:47 +08:00
Andre H a8ad6c6eec ️ feat: implement card style for radio 2024-03-01 10:41:12 +08:00
nabarun 65f64a3dcd Fix frontend deployment config (#148) 2024-02-29 22:45:33 +05:30
nabarun d64cf46d89 Fetch latest registry record version and use in deploy script (#146)
* Fetch latest registry record version and increment

* Move reference up

* Remove steps to manually update record values

* Update registry config with new key and bond
2024-02-29 22:29:23 +05:30
nabarunandneeraj 1ff5ab3dfd Assign project domain to latest production deployment (#145)
* Create ApplicationDeploymentRequest with project DNS

* Add comment for filtering AppDeploymentRecord

* Fix lint

* Handle delete gitHub event

* Add sleep before registry write methods

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-29 18:33:34 +05:30
Andre H d9392c095d ️ feat: reskin template header 2024-02-29 15:31:10 +07:00
Andre H 2cd1865d19 🔧 chore: add linkchain icon 2024-02-29 15:30:34 +07:00
Eric Lewisandnabarun a7810a34c9 feat: one click deployer (#139)
* feat: one click deployer

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

* fix

* repurpose existing script

* clean up

* fix

* fix

* Update latest record version

---------

Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
2024-02-29 13:11:01 +05:30
Vivian Phung ff9b08e91d Merge pull request #143 from snowball-tools/template-card-redirect-bug
Template card redirect bug
2024-02-29 00:38:12 -07:00
nabarun 94a9bf88a9 Remove check for .env in build script (#144) 2024-02-29 12:51:32 +05:30
Vivian Phung dd5f93050d fix navigate 2024-02-29 00:01:56 -07:00
Andre H 11aeda9453 🔧 chore: remove squiggly line 2024-02-29 13:59:53 +07:00
Andre H dfa38a4cba 🔧 chore: remove useMemo 2024-02-29 13:58:45 +07:00
Andre H 638e31f7fe 🔧 chore: use wavyBorder 2024-02-29 13:58:22 +07:00
Vivian Phung 5946755749 fix navigate 2024-02-28 23:52:37 -07:00
Vivian Phung 4532a962d8 fix navigate 2024-02-28 23:44:10 -07:00
Wahyu Kurniawan 83fec1d273 🎨 style: refactor and re-styling deployment tab page 2024-02-29 11:17:41 +07:00
Wahyu Kurniawan 6db5871868 🎨 style: make the input width to full 2024-02-29 11:17:41 +07:00
Wahyu Kurniawan e0c5895e9c ️ feat: add onReset prop 2024-02-29 11:17:41 +07:00
Wahyu Kurniawan 630af612a2 ️ feat: add some components 2024-02-29 11:17:41 +07:00
Wahyu Kurniawan 87307381cb 🎨 style: refactor and re-styling filter form component 2024-02-29 11:17:40 +07:00
Sushan Yadav 6a108c1a1b Merge pull request #137 from snowball-tools/sushan/T-4914-project-overview-layout
Project overview layout
2024-02-29 09:57:12 +05:45
Andre Hadianto aa5507f309 Merge branch 'main' into andrehadianto/T-4904-home-org-switcher 2024-02-29 10:34:36 +08:00
Andre H 91a80cac8a ️ feat: add squiggly line and personal TODO 2024-02-29 09:30:53 +07:00
Andre H ee397d0e6e 🔧 chore: add squigglyline icon 2024-02-29 09:30:29 +07:00
Wahyu Kurniawan 98ec1915ca ♻️ refactor: remove unused comment 2024-02-29 09:21:49 +07:00
Wahyu Kurniawan f08081932c ♻️ refactor: use custom toast component and handle error 2024-02-29 09:21:28 +07:00
Andre H fe427a9fb8 🐛 fix: onChange not triggered. thanks @ayungavis 2024-02-29 09:16:01 +07:00
Andre H a5b4103515 🔧 chore: bigger img on userselectitem 2024-02-29 09:15:18 +07:00
Andre H f77f7c120a 🔧 chore: follow select changes 2024-02-29 08:52:26 +07:00
Vivian Phung 22f3cbafb7 readme 2024-02-28 17:27:27 -07:00
Andre H 30c50a3eec 🔧 chore: use UserSelect in sidebar 2024-02-28 23:08:21 +07:00
Andre H 0381fdbbed ️ feat: impelment user select 2024-02-28 23:07:54 +07:00
Andre H 2f466d4fbb 🔧 chore: remove dot when description dont exist 2024-02-28 23:07:31 +07:00
Andre H ee35e64f69 🔧 chore: add icons 2024-02-28 23:06:54 +07:00
Sushan Yadav 0fb378386a chore: update comment 2024-02-28 20:51:58 +05:45
Sushan Yadav 563059e887 chore: comment fixes - tab issues and hardcoded strings 2024-02-28 20:46:08 +05:45
Wahyu Kurniawan 075cec58e5 🎨 style: add github icon 2024-02-28 21:27:04 +07:00
Wahyu Kurniawan d52a34da35 🐛 fix: controlled and uncontroller console error 2024-02-28 21:18:29 +07:00
Wahyu Kurniawan eb4ccfbc9c 🎨 style: make the input width full 2024-02-28 21:17:59 +07:00
Wahyu Kurniawan 769593913e 🐛 fix: button inside button console browser error 2024-02-28 21:17:30 +07:00
Wahyu Kurniawan eda2cc76ed ♻️ refactor: restructured and restyling project repo card component 2024-02-28 21:16:59 +07:00
Wahyu Kurniawan 99eb514306 ♻️ refactor: restructured and restryling repository list component 2024-02-28 21:16:26 +07:00
Sushan Yadav f34eb48d1e feat: add layout 2024-02-28 17:35:02 +05:45
Sushan Yadav acf5aab26f Merge branch 'main' into sushan/T-4914-project-overview-layout 2024-02-28 16:57:32 +05:45
Sushan Yadav 6b6582c287 feat: top section 2024-02-28 16:56:28 +05:45
Sushan Yadav b43ee3b7bb feat: add deplyment infos 2024-02-28 16:39:26 +05:45
Wahyu Kurniawan b1bf47d104 [T-4921: feat] Update project card component (#136)
* 🎨 style: add hover interaction to the card

* ️ feat: make the whole card clickable

* 🎨 style: adjust hovered background for wavy border

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

* 🎨 style: add transition when hover

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

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

* ♻️ refactor: org slug dashboard layout

* ♻️ refactor: create project layout and restyling it

* ♻️ refactor: remove unused style

* ️ feat: restyling template card

* ️ feat: create template icon component

* ️ feat: use `h2` for layout title

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

* ♻️ refactor: WavyBorder component and update CreateProjectLayout

* 🎨 style: update button medium size padding

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

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

* refactor: update wavy border svg

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

---------

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

* feat: add sepeator calc in comment

* chore: limit commit message to 4 line

* comment fixes

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

* ♻️ refactor: move sidebar inside shared components

* ️ feat: add polymorphic prop type for heading component

* 🎨 style: re-styling project list page

* ♻️ refactor: remove `.env`

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

* 🎨 style: add font `InterDisplay` to tailwind

* 🔧 chore: update `yarn.lock`

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

* ️ feat: create project card component

* ️ feat: add new size in avatar

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

* ️ feat: create some icons for project card component

* 🔧 chore: install and setup jetbrains mono font family

* ️ feat: create `getInitials` util function

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

* ️ feat: add warning error icon

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

* Update readme with steps for new deployment

* Add troubleshooting section

* Move config env update

* Add step to commit updated records

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

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

* 🐛 fix: console error becasue button inside button

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

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

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

* 🔧 chore: install `downshift`

* ️ feat: create select component

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

* ️ feat: separate select item to be a component

* ️ feat: separate select value to be a component

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

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

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

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

* 🐛 fix: no result not showing

* ️ feat: make the select to be controller component

* ♻️ refactor: remove console log

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

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

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

* Set template URL from env

* Handle review changes

* Update readme and build app script

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
2024-02-26 17:54:35 +05:30
Vivian Phung 0dfecd024d merge 2024-02-24 00:01:06 -05:00
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
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
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
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
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
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
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
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
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
Wahyu Kurniawan 0f7c6c73c9 ️ feat: make the button component to forward ref 2024-02-20 22:59:31 +07:00
Wahyu Kurniawan 2ac657c32e 🎨 style: add new spacing 2024-02-19 21:16:55 +07:00
Wahyu Kurniawan 943d427db1 📝 docs: add button icon only on component example 2024-02-19 21:16:39 +07:00
Wahyu Kurniawan 7e6b74208d ️ feat: create cloneIcon util function 2024-02-19 21:16:17 +07:00
Wahyu Kurniawan 2b78cab849 ️ feat: create custom icon and plus icon component 2024-02-19 21:15:51 +07:00
Wahyu Kurniawan 862862a9c5 🎨 style: update button and button icon only theme 2024-02-19 21:15:29 +07:00
Wahyu Kurniawan 6a3af491ca Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4834-button 2024-02-19 20:20:31 +07:00
Wahyu Kurniawan 92f7d07f3f 🔧 chore: install tailwind-variants and @fontsource/inter 2024-02-19 20:16:25 +07:00
Wahyu Kurniawan 6fb94aeb11 🔧 chore: add absolute path 2024-02-19 20:15:56 +07:00
Wahyu Kurniawan 1c848c5a89 🎨 style: add inter font family 2024-02-19 20:15:32 +07:00
Wahyu Kurniawan 17b5ff9a74 🎨 style: add colors, font family, and shadow 2024-02-19 20:14:49 +07:00
Wahyu Kurniawan 56adfc4806 🐛 fix: typescript error 2024-02-19 20:14:23 +07:00
Wahyu Kurniawan b51d050ee6 📝 docs: add button to the example comopnent pag 2024-02-19 20:13:56 +07:00
Wahyu Kurniawan 8021ff7ff9 🔧 chore: add vscode settings on frontend package 2024-02-19 20:13:36 +07:00
Wahyu Kurniawan e850435c2d ️ feat: create button component 2024-02-19 20:13:11 +07:00
Zachery 8a43698c43 Merge pull request #75 from snowball-tools/zachery/T-4843-focus-ring-class
feat: focus ring class
2024-02-19 20:55:52 +08:00
Zachery Ng 499dba2c5e chore: add full palette colors 2024-02-19 20:12:25 +08:00
Zachery Ng d498ba7e9a feat: focus ring class 2024-02-19 20:05:29 +08:00
Andre H 082353e9bc ️ feat: implement components page to act as storybook 2024-02-16 12:31:13 +07:00
367 changed files with 17378 additions and 5459 deletions
+6
View File
@@ -1 +1,7 @@
node_modules/
yarn-error.log
.yarnrc.yml
.yarn/
.yarnrc
packages/backend/environments/local.toml
+7
View File
@@ -0,0 +1,7 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
"tv\\('([^)]*)\\')",
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
]
}
+201 -225
View File
@@ -6,9 +6,10 @@
```bash
git clone git@github.com:snowball-tools/snowballtools-base.git
cd snowballtools-base
```
- In root of the repo, install depedencies
- Install dependencies in root
```bash
yarn
@@ -20,237 +21,212 @@
yarn build --ignore frontend
```
## Backend
- Change directory to `packages/backend`
```bash
cd packages/backend
```
- 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
### Backend Production
- Let us assume the following domains for backend and frontend
Let us assume the following domains for backend and frontend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
- Update the following in backend [config file](packages/backend/environments/local.toml)
- Configuration Files
- For Backend:
```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 = ""
```bash
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
```
- Production
- Update the following in backend [config file](packages/backend/environments/local.toml)
```toml
[server]
...
[server.session]
# Secret should be changed to a different random string
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
# Set URL of the frontend app
appOriginUrl = "https://dashboard.snowballtools.com"
# Set to true for session cookies to work behind proxy
trustProxy = true
# Set empty domain when using secure connection
domain = ""
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `https://dashboard.snowballtools.com`
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
- Generate a new client secret after app is created
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://api.snowballtools.com"
```
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
```toml
[registryConfig]
fetchDeploymentRecordDelay = 5000
# Use actual port for REST endpoint
restEndpoint = "http://api.laconic.com:1317"
# Use actual port for GQL endpoint
gqlEndpoint = "http://api.laconic.com:9473/api"
# Set private key of account to be used in Laconicd
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
# Set Bond ID to be used for publishing records
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
chainId = "laconic_9000-1"
# Set authority that is existing in the chain
authority = "laconic"
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "750000"
```
- Development
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `http://localhost:3000`
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Setup Laconicd
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
```bash
laconic-so --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
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```bash
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
```
- For Frontend:
```bash
cp packages/frontend/.env.example packages/frontend/.env
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- 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_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
```
- Production
- Set the following values in [.env](packages/frontend/.env) file
```env
REACT_APP_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Set project home page URL to `https://dashboard.snowballtools.com`
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
python3 -m http.server -d build 3000
```
- Development
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
```env
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>
```
- The React application will be running in `http://localhost:3000/`
## Development
- Start the backend server
```bash
yarn start:backend
```
- 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
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
...
[gitHub]
webhookUrl = "https://api.snowballtools.com"
...
```
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
```toml
...
[registryConfig]
fetchDeploymentRecordDelay = 5000
# Use actual port for REST endpoint
restEndpoint = "http://api.laconic.com:1317"
# Use actual port for GQL endpoint
gqlEndpoint = "http://api.laconic.com:9473/api"
# Set private key of account to be used in Laconicd
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
# Set Bond ID to be used for publishing records
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
chainId = "laconic_9000-1"
# Set authority that is existing in the chain
authority = "laconic"
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "750000"
...
```
- Start the server in `packages/backend`
- Start the frontend
```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
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```bash
...
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
...
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
...
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
...
```
- Start the server in `packages/backend`
```bash
yarn start:dev
```
## Frontend
- Change directory to `packages/frontend` in a new terminal
```bash
cd packages/frontend
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```env
# Set actual owner/name of the template repo that will be used for creating new repo
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
```
### Frontend Production
- Let us assume the following domains for backend and frontend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
- Set the following values in [.env](packages/frontend/.env) file
```env
# Backend server endpoint
REACT_APP_SERVER_URL = 'https://api.snowballtools.com'
```
- 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
```env
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
python3 -m http.server -d build 3000
```
### 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/`
+2 -1
View File
@@ -12,7 +12,8 @@ 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_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
EOF
+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
@@ -17,11 +17,6 @@
clientId = ""
clientSecret = ""
[gitea]
[gitea.oAuth]
clientId = ""
clientSecret = ""
[registryConfig]
fetchDeploymentRecordDelay = 5000
restEndpoint = "http://localhost:1317"
+1 -3
View File
@@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@cerc-io/laconic-sdk": "^0.1.14",
"@cerc-io/laconic-sdk": "^0.1.16",
"@graphql-tools/schema": "^10.0.2",
"@graphql-tools/utils": "^10.0.12",
"@octokit/oauth-app": "^6.1.0",
@@ -21,7 +21,6 @@
"luxon": "^3.4.4",
"nanoid": "3",
"nanoid-dictionary": "^5.0.0-beta.1",
"node-fetch": "2",
"octokit": "^3.1.2",
"reflect-metadata": "^0.2.1",
"semver": "^7.6.0",
@@ -47,7 +46,6 @@
"devDependencies": {
"@types/express-session": "^1.17.10",
"@types/fs-extra": "^11.0.4",
"@types/node-fetch": "^2.6.11",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"better-sqlite3": "^9.2.2",
+2 -2
View File
@@ -24,7 +24,7 @@ export interface GitHubConfig {
oAuth: {
clientId: string;
clientSecret: string;
}
};
}
export interface RegistryConfig {
@@ -39,7 +39,7 @@ export interface RegistryConfig {
amount: string;
denom: string;
gas: string;
}
};
}
export interface MiscConfig {
+2 -1
View File
@@ -1,5 +1,6 @@
import process from 'process';
export const DEFAULT_CONFIG_FILE_PATH = process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
export const DEFAULT_CONFIG_FILE_PATH =
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
export const DEFAULT_GQL_PATH = '/graphql';
+138 -47
View File
@@ -1,4 +1,10 @@
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';
@@ -74,14 +80,18 @@ export class Database {
return updateResult.affected > 0;
}
async getOrganizations (options: FindManyOptions<Organization>): Promise<Organization[]> {
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> {
async getOrganization (
options: FindOneOptions<Organization>
): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options);
@@ -123,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')
@@ -136,19 +150,29 @@ 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;
@@ -157,7 +181,9 @@ export class Database {
/**
* Get deployments with specified filter
*/
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
async getDeployments (
options: FindManyOptions<Deployment>
): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find(options);
@@ -182,7 +208,9 @@ export class Database {
});
}
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);
@@ -210,8 +238,11 @@ export class Database {
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: {
@@ -228,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: {
@@ -244,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;
@@ -255,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;
@@ -295,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: {
@@ -307,8 +372,7 @@ export class Database {
where: {
id: projectMemberId
}
}
);
});
if (projectMemberWithProject.length === 0) {
throw new Error('Member does not exist');
@@ -317,34 +381,49 @@ 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 updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
async updateDeploymentsByProjectIds (
projectIds: string[],
data: DeepPartial<Deployment>
): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository
@@ -378,9 +457,15 @@ export class Database {
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);
}
@@ -427,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({
+16 -16
View File
@@ -27,26 +27,26 @@ export enum DeploymentStatus {
}
export interface ApplicationDeploymentRequest {
type: string
version: string
name: string
application: string
config: string,
meta: string
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()
+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 -1
View File
@@ -76,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()
+8 -4
View File
@@ -39,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[];
}
+9 -2
View File
@@ -31,9 +31,16 @@ export const main = async (): Promise<void> => {
await db.init();
const registry = new Registry(registryConfig);
const service = new Service({ gitHubConfig: gitHub, registryConfig }, 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);
+103 -34
View File
@@ -6,23 +6,33 @@ import { DateTime } from 'luxon';
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config';
import { ApplicationRecord, Deployment, ApplicationDeploymentRequest } from './entity/Deployment';
import {
ApplicationRecord,
Deployment,
ApplicationDeploymentRequest
} from './entity/Deployment';
import { AppDeploymentRecord, PackageJSON } from './types';
import { sleep } from './utils';
const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
const SLEEP_DURATION = 1000;
// 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 ({
@@ -32,24 +42,38 @@ export class Registry {
appType,
repoUrl
}: {
appName: string,
packageJSON: PackageJSON
commitHash: string,
appType: string,
repoUrl: string
}): Promise<{applicationRecordId: string, applicationRecordData: 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');
@@ -64,7 +88,12 @@ export class Registry {
...(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 })
};
@@ -84,22 +113,45 @@ export class Registry {
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 sleep(SLEEP_DURATION);
await this.registry.setName(
{ cid: result.data.id, crn },
this.registryConfig.privateKey,
this.registryConfig.fee
);
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
await sleep(SLEEP_DURATION);
await this.registry.setName(
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
this.registryConfig.privateKey,
this.registryConfig.fee
);
await sleep(SLEEP_DURATION);
await this.registry.setName(
{
cid: result.data.id,
crn: `${crn}@${applicationRecord.repository_ref}`
},
this.registryConfig.privateKey,
this.registryConfig.fee
);
return {
applicationRecordId: result.data.id,
applicationRecordData: applicationRecord
};
}
async createApplicationDeploymentRequest (data: {
deployment: Deployment,
appName: string,
packageJsonName: string,
repository: string,
environmentVariables: { [key: string]: string }
environmentVariables: { [key: string]: string },
dns: string,
}): Promise<{
applicationDeploymentRequestId: string,
applicationDeploymentRequestData: ApplicationDeploymentRequest
applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> {
const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]);
@@ -115,7 +167,7 @@ export class Registry {
version: '1.0.0',
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
application: `${crn}@${applicationRecord.attributes.app_version}`,
dns: `${data.deployment.project.name}-${data.deployment.id}`,
dns: data.dns,
// TODO: Not set in test-progressive-web-app CI
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
@@ -125,12 +177,15 @@ 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.deployment.commitHash
})
};
await sleep(SLEEP_DURATION);
const result = await this.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
@@ -143,21 +198,35 @@ export class Registry {
log(`Application deployment request record published: ${result.data.id}`);
log('Application deployment request data:', applicationDeploymentRequest);
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
return {
applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest
};
}
/**
* Fetch ApplicationDeploymentRecords for deployments
*/
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
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);
const records = await this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE
},
true
);
// Filter records with ApplicationRecord ids
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
// Filter records with ApplicationRecord ID and Deployment specific URL
return records.filter((record: AppDeploymentRecord) =>
deployments.some(
(deployment) =>
deployment.applicationRecordId === record.attributes.application &&
record.attributes.url.includes(deployment.id)
)
);
}
getCrn (appName: string): string {
+114 -38
View File
@@ -6,7 +6,6 @@ import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain';
import { Project } from './entity/Project';
import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { GitType } from './types';
const log = debug('snowball:resolver');
@@ -34,7 +33,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getDeploymentsByProjectId(projectId);
},
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
environmentVariables: async (
_: any,
{ projectId }: { projectId: string }
) => {
return service.getEnvironmentVariablesByProjectId(projectId);
},
@@ -46,14 +48,24 @@ export const createResolvers = async (service: Service): Promise<any> => {
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.user, projectMemberId);
} catch (err) {
@@ -62,12 +74,18 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
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) {
@@ -76,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) {
@@ -91,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) {
@@ -118,7 +168,11 @@ 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.user, deploymentId));
} catch (err) {
@@ -127,7 +181,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
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.user, organizationSlug, data);
} catch (err) {
@@ -136,7 +197,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
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) {
@@ -145,7 +209,11 @@ 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.user, deploymentId));
} catch (err) {
@@ -158,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;
}
},
@@ -171,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) {
@@ -180,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) {
@@ -189,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) {
@@ -198,7 +279,11 @@ 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.user);
} catch (err) {
@@ -207,15 +292,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
authenticateGit: async (_: any, { type, code }: { type: GitType, code: string }, context: any) => {
try {
return await service.authenticateGit(type, code, context.user);
} catch (err) {
log(err);
return false;
}
},
unauthenticateGitHub: async (_: any, __: object, context: any) => {
try {
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
+12 -9
View File
@@ -26,11 +26,6 @@ enum DomainStatus {
Pending
}
enum GitType {
GitHub
Gitea
}
type User {
id: String!
name: String
@@ -193,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!
@@ -208,6 +212,5 @@ type Mutation {
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
authenticateGitHub(code: String!): AuthResult!
authenticateGit(type: GitType!, code: String!): AuthResult!
unauthenticateGitHub: Boolean!
}
+208 -152
View File
@@ -2,7 +2,6 @@ import assert from 'assert';
import debug from 'debug';
import { DeepPartial, FindOptionsWhere } from 'typeorm';
import { Octokit, RequestError } from 'octokit';
import fetch from 'node-fetch';
import { OAuthApp } from '@octokit/oauth-app';
@@ -16,17 +15,16 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User';
import { Registry } from './registry';
import { GitHubConfig, RegistryConfig } from './config';
import { AppDeploymentRecord, GitPushEventPayload, GitType, PackageJSON } from './types';
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';
const GITEA_ACCESS_TOKEN_ENDPOINT = 'https://git.vdb.to/login/oauth/access_token';
interface Config {
gitHubConfig: GitHubConfig
registryConfig: RegistryConfig
gitHubConfig: GitHubConfig;
registryConfig: RegistryConfig;
}
export class Service {
@@ -74,7 +72,9 @@ export class Service {
});
if (deployments.length) {
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`);
log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
);
// Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments);
@@ -94,10 +94,12 @@ export class Service {
/**
* Update deployments with ApplicationDeploymentRecord data
*/
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
async updateDeploymentsWithRecordData (
records: AppDeploymentRecord[]
): Promise<void> {
// Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({
where: records.map(record => ({
where: records.map((record) => ({
applicationRecordId: record.attributes.application
})),
order: {
@@ -106,38 +108,46 @@ export class Service {
});
// 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);
}
const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId);
}
return acc;
}, new Set<string>());
return acc;
},
new Set<string>()
);
// Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: 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;
}, {});
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
}
);
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}`);
log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
);
});
await Promise.all(deploymentUpdatePromises);
@@ -183,7 +193,10 @@ export class Service {
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 });
}
@@ -208,13 +221,19 @@ export class Service {
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;
}
@@ -223,20 +242,28 @@ export class Service {
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: {
@@ -281,29 +308,41 @@ export class Service {
}
}
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);
}
@@ -319,7 +358,10 @@ 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(user.id);
@@ -340,11 +382,12 @@ export class Service {
async createDeployment (
userId: string,
octokit: Octokit,
data: DeepPartial<Deployment>,
recordData: { repoUrl?: string } = {}
data: DeepPartial<Deployment>
): 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({
@@ -363,19 +406,20 @@ export class Service {
assert(packageJSON.name, "name field doesn't exist in package.json");
if (!recordData.repoUrl) {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
recordData.repoUrl = repoDetails.html_url;
}
const repoUrl = (await octokit.rest.repos.get({
owner,
repo
})).data.html_url;
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
appName: repo,
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl: recordData.repoUrl
});
const { applicationRecordId, applicationRecordData } =
await this.registry.createApplicationRecord({
appName: repo,
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl
});
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
@@ -416,11 +460,23 @@ export class Service {
{
deployment: newDeployment,
appName: repo,
packageJsonName: packageJSON.name,
repository: recordData.repoUrl,
environmentVariables: environmentVariablesObj
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`
});
// To set project DNS
if (data.environment === Environment.Production) {
await this.registry.createApplicationDeploymentRequest(
{
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`
});
}
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
return newDeployment;
@@ -441,15 +497,15 @@ export class Service {
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,
per_page: 1
});
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
// Create deployment with prod branch and latest commit
await this.createDeployment(user.id,
octokit,
@@ -460,9 +516,6 @@ export class Service {
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message
},
{
repoUrl: repoDetails.html_url
}
);
@@ -478,7 +531,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']
@@ -486,9 +542,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;
}
@@ -498,9 +558,17 @@ 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 { repository, ref, head_commit: headCommit, deleted } = data;
if (deleted) {
log(`Branch ${ref} deleted for project ${repository.full_name}`);
return;
}
log(`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`);
const projects = await this.db.getProjects({
where: { repository: repository.full_name }
});
if (!projects.length) {
log(`No projects found for repository ${repository.full_name}`);
@@ -512,23 +580,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,
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);
}
@@ -545,7 +619,9 @@ 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);
@@ -584,7 +660,10 @@ export class Service {
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: {
@@ -602,16 +681,25 @@ export class Service {
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);
@@ -636,12 +724,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
@@ -682,7 +778,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;
@@ -703,48 +801,6 @@ export class Service {
return { token };
}
async authenticateGit (type: GitType, code:string, user: User): Promise<{token: string}> {
let token: string;
switch (type) {
case GitType.GitHub:
({ authentication: { token } } = await this.oauthApp.createToken({
code
}));
break;
case GitType.Gitea: {
const response = await fetch(GITEA_ACCESS_TOKEN_ENDPOINT, {
method: 'post',
body: JSON.stringify({
// TODO: Fetch from config
client_id: '',
client_secret: '',
code,
grant_type: 'authorization_code',
// TODO: Get frontend app URL from config
redirect_uri: 'http://localhost:3000/organization/projects/create'
}),
headers: { 'Content-Type': 'application/json' }
});
assert(response.ok, `HTTP Error Response: ${response.status} ${response.statusText}`);
const data: any = await response.json();
({ access_token: token } = data);
break;
}
default: throw new Error(`Type ${type} not handled for Git authentication`);
}
assert(token, `Access token is not set for type ${type}`);
await this.db.updateUser(user, { gitHubToken: token });
return { token };
}
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
return this.db.updateUser(user, data);
}
+2 -6
View File
@@ -24,6 +24,7 @@ export interface GitPushEventPayload {
id: string;
message: string;
};
deleted: boolean;
}
export interface AppDeploymentRecordAttributes {
@@ -47,10 +48,5 @@ interface RegistryRecord {
}
export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes
}
export enum GitType {
GitHub = 'GitHub',
Gitea = 'Gitea',
attributes: AppDeploymentRecordAttributes;
}
+9 -2
View File
@@ -37,10 +37,15 @@ export const getEntities = async (filePath: string): Promise<any> => {
return entities;
};
export const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, entities: any, relations?: any | undefined): Promise<Entity[]> => {
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[] = [];
const savedEntity: Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
@@ -61,3 +66,5 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType:
return savedEntity;
};
export const sleep = async (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
+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));
+20 -20
View File
@@ -1,9 +1,9 @@
[
{
"projectIndex": 0,
"domainIndex":0,
"domainIndex": 0,
"createdByIndex": 0,
"id":"ffhae3zq",
"id": "ffhae3zq",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
@@ -18,9 +18,9 @@
},
{
"projectIndex": 0,
"domainIndex":1,
"domainIndex": 1,
"createdByIndex": 0,
"id":"vehagei8",
"id": "vehagei8",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
@@ -35,9 +35,9 @@
},
{
"projectIndex": 0,
"domainIndex":2,
"domainIndex": 2,
"createdByIndex": 0,
"id":"qmgekyte",
"id": "qmgekyte",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
@@ -54,7 +54,7 @@
"projectIndex": 0,
"domainIndex": null,
"createdByIndex": 0,
"id":"f8wsyim6",
"id": "f8wsyim6",
"status": "Ready",
"environment": "Production",
"isCurrent": false,
@@ -69,9 +69,9 @@
},
{
"projectIndex": 1,
"domainIndex":3,
"domainIndex": 3,
"createdByIndex": 1,
"id":"eO8cckxk",
"id": "eO8cckxk",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
@@ -86,9 +86,9 @@
},
{
"projectIndex": 1,
"domainIndex":4,
"domainIndex": 4,
"createdByIndex": 1,
"id":"yaq0t5yw",
"id": "yaq0t5yw",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
@@ -103,9 +103,9 @@
},
{
"projectIndex": 1,
"domainIndex":5,
"domainIndex": 5,
"createdByIndex": 1,
"id":"hwwr6sbx",
"id": "hwwr6sbx",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
@@ -120,9 +120,9 @@
},
{
"projectIndex": 2,
"domainIndex":9,
"domainIndex": 9,
"createdByIndex": 2,
"id":"ndxje48a",
"id": "ndxje48a",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
@@ -137,9 +137,9 @@
},
{
"projectIndex": 2,
"domainIndex":7,
"domainIndex": 7,
"createdByIndex": 2,
"id":"gtgpgvei",
"id": "gtgpgvei",
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
@@ -154,9 +154,9 @@
},
{
"projectIndex": 2,
"domainIndex":8,
"domainIndex": 8,
"createdByIndex": 2,
"id":"b4bpthjr",
"id": "b4bpthjr",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
@@ -173,7 +173,7 @@
"projectIndex": 3,
"domainIndex": 6,
"createdByIndex": 2,
"id":"b4bpthjr",
"id": "b4bpthjr",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
+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
}
]
+80 -18
View File
@@ -10,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 { checkFileExists, getConfig, getEntities, loadAndSaveData } from '../src/utils';
import {
checkFileExists,
getConfig,
getEntities,
loadAndSaveData
} from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
@@ -27,19 +32,34 @@ const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
const generateTestData = async (dataSource: DataSource) => {
const userEntities = await getEntities(path.resolve(__dirname, USER_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 orgEntities = await getEntities(
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
);
const savedOrgs = await loadAndSaveData(
Organization,
dataSource,
orgEntities
);
const projectRelations = {
owner: savedUsers,
organization: savedOrgs
};
const projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH));
const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations);
const projectEntities = await getEntities(
path.resolve(__dirname, PROJECT_DATA_PATH)
);
const savedProjects = await loadAndSaveData(
Project,
dataSource,
projectEntities,
projectRelations
);
const domainRepository = dataSource.getRepository(Domain);
@@ -47,16 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects
};
const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH));
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, 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
};
const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH));
await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations);
const redirectDomainsEntities = await getEntities(
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
);
await loadAndSaveData(
Domain,
dataSource,
redirectDomainsEntities,
domainRedirectedRelations
);
const savedDomains = await domainRepository.find();
@@ -65,16 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs
};
const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH));
await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations);
const userOrganizationsEntities = await getEntities(
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
);
await loadAndSaveData(
UserOrganization,
dataSource,
userOrganizationsEntities,
userOrganizationRelations
);
const projectMemberRelations = {
member: savedUsers,
project: savedProjects
};
const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH));
await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations);
const projectMembersEntities = await getEntities(
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
);
await loadAndSaveData(
ProjectMember,
dataSource,
projectMembersEntities,
projectMemberRelations
);
const deploymentRelations = {
project: savedProjects,
@@ -82,15 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers
};
const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH));
await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations);
const deploymentsEntities = await getEntities(
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
);
await loadAndSaveData(
Deployment,
dataSource,
deploymentsEntities,
deploymentRelations
);
const environmentVariableRelations = {
project: savedProjects
};
const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH));
await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations);
const environmentVariablesEntities = await getEntities(
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
);
await loadAndSaveData(
EnvironmentVariable,
dataSource,
environmentVariablesEntities,
environmentVariableRelations
);
};
const main = async () => {
+10 -2
View File
@@ -21,12 +21,20 @@ async function main () {
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 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}`);
}
}
@@ -14,7 +14,11 @@ 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 registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
const dataSource = new DataSource({
type: 'better-sqlite3',
+48
View File
@@ -0,0 +1,48 @@
# deployer
- Install dependencies
```bash
yarn
```
```bash
brew install jq # if you do not have jq installed already
```
- Run script to deploy app
```
./deploy-frontend.sh
```
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
...
```
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
## Troubleshoot
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
- Check records in [registry console app](https://console.laconic.com/#/registry).
- If deployment fails due to low bond balance
- Check balances
```bash
# Account balance
yarn laconic cns account get
# Bond balance
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
```
- Command to refill bond
```bash
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
```
+9
View File
@@ -0,0 +1,9 @@
services:
cns:
restEndpoint: http://console.laconic.com:1317
gqlEndpoint: http://console.laconic.com:9473/api
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
chainId: laconic_9000-1
gas: 1200000
fees: 200000aphoton
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Repository URL
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
# Extract version from ../frontend/package.json
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
# Current date and time for note
CURRENT_DATE_TIME=$(date -u)
CONFIG_FILE=config.yml
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
NEW_APPLICATION_VERSION=0.0.1
fi
# Generate application-deployment-request.yml
cat > ./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version
cat > ./records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: snowballtools-base-frontend
app_version: $PACKAGE_VERSION
EOF
echo "Files generated successfully."
RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
RECORD_FILE=records/application-deployment-request.yml
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
+9
View File
@@ -0,0 +1,9 @@
{
"name": "deployer",
"version": "1.0.0",
"main": "index.js",
"private": true,
"devDependencies": {
"@cerc-io/laconic-registry-cli": "^0.1.10"
}
}
@@ -0,0 +1,18 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
@@ -0,0 +1,8 @@
record:
type: ApplicationRecord
version: 0.0.2
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: snowballtools-base-frontend
app_version: 0.1.8
-8
View File
@@ -1,8 +0,0 @@
REACT_APP_SERVER_URL = 'http://localhost:8000'
REACT_APP_GITHUB_CLIENT_ID =
REACT_APP_GITHUB_TEMPLATE_REPO =
REACT_APP_GITEA_CLIENT_ID =
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
+21 -5
View File
@@ -1,9 +1,20 @@
{
"name": "frontend",
"version": "0.1.0",
"version": "0.1.8",
"private": true,
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.22.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
@@ -16,13 +27,17 @@
"@web3modal/wagmi": "^4.0.5",
"assert": "^2.1.0",
"axios": "^1.6.7",
"date-fns": "^3.0.1",
"downshift": "^8.2.3",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"downshift": "^8.3.2",
"eslint-config-react-app": "^7.0.1",
"framer-motion": "^11.0.8",
"gql-client": "^1.0.0",
"lottie-react": "^2.4.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",
@@ -34,8 +49,9 @@
"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",
"usehooks-ts": "^2.15.1",
"vertical-stepper-nav": "^1.0.2",
"viem": "^2.7.11",
"wagmi": "^2.5.7",
@@ -78,6 +94,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"
}
}
+21 -10
View File
@@ -3,18 +3,29 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="snowball tools dashboard"
/>
<meta name="description" content="snowball tools dashboard" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<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="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" />
<title>Snowball</title>
</head>
+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

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

After

Width:  |  Height:  |  Size: 1.1 KiB

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

After

Width:  |  Height:  |  Size: 1.5 KiB

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

After

Width:  |  Height:  |  Size: 507 B

+2 -2
View File
@@ -1,7 +1,6 @@
import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import OrgSlug from './pages/OrgSlug';
import Projects from './pages/org-slug';
import Settings from './pages/org-slug/Settings';
import {
@@ -11,11 +10,12 @@ import {
import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages';
import Login from './pages/Login';
import { DashboardLayout } from 'pages/org-slug/layout';
const router = createBrowserRouter([
{
path: ':orgSlug',
element: <OrgSlug />,
element: <DashboardLayout />,
children: [
{
element: <ProjectSearchLayout />,
@@ -1,27 +0,0 @@
[
{
"id": "1",
"name": "Progressive Web App (PWA)",
"icon": "^"
},
{
"id": "2",
"name": "Kotlin",
"icon": "^"
},
{
"id": "3",
"name": "React Native",
"icon": "^"
},
{
"id": "4",
"name": "Swift",
"icon": "^"
},
{
"id": "5",
"name": "Web app",
"icon": "^"
}
]
+37
View File
@@ -0,0 +1,37 @@
export default [
{
id: '1',
name: 'Progressive Web App (PWA)',
icon: 'pwa',
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '2',
name: 'Image Upload PWA',
icon: 'pwa',
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '3',
name: 'Kotlin',
icon: 'kotlin',
repoFullName: '',
isComingSoon: true,
},
{
id: '4',
name: 'React Native',
icon: 'react-native',
repoFullName: '',
isComingSoon: true,
},
{
id: '5',
name: 'Swift',
icon: 'swift',
repoFullName: '',
isComingSoon: true,
},
];
@@ -126,7 +126,8 @@ const DatePicker = ({
crossOrigin={undefined}
/>
</PopoverHandler>
<PopoverContent>
{/* TODO: Figure out what placeholder is for */}
<PopoverContent placeholder={''}>
{mode === 'single' && (
<DayPicker
mode="single"
@@ -145,19 +146,23 @@ const DatePicker = ({
/>
<HorizontalLine />
<div className="flex justify-end">
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full mr-2"
variant="outlined"
onClick={() => setIsOpen(false)}
placeholder={''}
>
Cancel
</Button>
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full"
color="gray"
onClick={() => handleRangeSelect()}
placeholder={''}
>
Select
</Button>
@@ -1,13 +1,22 @@
import { Duration } from 'luxon';
import React from 'react';
import React, { ComponentPropsWithoutRef } from 'react';
import { cn } from 'utils/classnames';
const FormatMillisecond = ({ time }: { time: number }) => {
export interface FormatMilliSecondProps
extends ComponentPropsWithoutRef<'div'> {
time: number;
}
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
const formatTime = Duration.fromMillis(time)
.shiftTo('days', 'hours', 'minutes', 'seconds')
.toObject();
return (
<div>
<div
{...props}
className={cn('text-sm text-elements-mid-em', props?.className)}
>
{formatTime.days !== 0 && <span>{formatTime.days}d&nbsp;</span>}
{formatTime.hours !== 0 && <span>{formatTime.hours}h&nbsp;</span>}
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m&nbsp;</span>}
@@ -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;
+24
View File
@@ -0,0 +1,24 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Heading } from './shared/Heading';
interface LogoProps {
orgSlug?: string;
}
export const Logo = ({ orgSlug }: LogoProps) => {
return (
<Link to={`/${orgSlug}`}>
<div className="flex items-center gap-3 px-0 lg:px-2">
<img
src="/logo.svg"
alt="Snowball Logo"
className="lg:h-10 lg:w-10 h-8 w-8 rounded-lg"
/>
<Heading className="lg:text-[24px] text-[19px] font-semibold">
Snowball
</Heading>
</div>
</Link>
);
};
+7 -16
View File
@@ -1,33 +1,24 @@
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"
className="w-full lg:w-[459px]"
{...props}
inputRef={ref}
/>
<div className="!absolute left-3 top-[13px]">
<i>^</i>
</div>
</div>
);
};
@@ -1,96 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
import { Organization } from 'gql-client';
import { Typography, Option } from '@material-tailwind/react';
import { useDisconnect } from 'wagmi';
import { useGQLClient } from '../context/GQLClientContext';
import AsyncSelect from './shared/AsyncSelect';
const Sidebar = () => {
const { orgSlug } = useParams();
const navigate = useNavigate();
const client = useGQLClient();
const { disconnect } = useDisconnect();
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const fetchUserOrganizations = useCallback(async () => {
const { organizations } = await client.getOrganizations();
setOrganizations(organizations);
}, [orgSlug]);
useEffect(() => {
fetchUserOrganizations();
setSelectedOrgSlug(orgSlug);
}, [orgSlug]);
const handleLogOut = useCallback(() => {
disconnect();
navigate('/login');
}, [disconnect, navigate]);
return (
<div className="flex flex-col h-full p-4">
<div className="grow">
<div>
<Link to={`/${orgSlug}`}>
<h3 className="text-black text-2xl">Snowball</h3>
</Link>
</div>
<AsyncSelect
className="bg-white py-2"
value={selectedOrgSlug}
onChange={(value) => {
setSelectedOrgSlug(value!);
navigate(`/${value}`);
}}
selected={(_, index) => (
<div className="flex gap-2">
<div>^</div>
<div>
<span>{organizations[index!]?.name}</span>
<Typography>Organization</Typography>
</div>
</div>
)}
>
{/* TODO: Show label organization and manage in option */}
{organizations.map((org) => (
<Option key={org.id} value={org.slug}>
^ {org.name}
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
</Option>
))}
</AsyncSelect>
<div>
<NavLink
to={`/${orgSlug}`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
>
<Typography>Projects</Typography>
</NavLink>
</div>
<div>
<NavLink
to={`/${orgSlug}/settings`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
>
<Typography>Settings</Typography>
</NavLink>
</div>
</div>
<div className="grow flex flex-col justify-end">
<a className="cursor-pointer" onClick={handleLogOut}>
Log Out
</a>
<a className="cursor-pointer">Documentation</a>
<a className="cursor-pointer">Support</a>
</div>
</div>
);
};
export default Sidebar;
@@ -1,7 +1,7 @@
import React from 'react';
import { useStopwatch } from 'react-timer-hook';
import FormatMillisecond from './FormatMilliSecond';
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
const setStopWatchOffset = (time: string) => {
const providedTime = new Date(time);
@@ -11,13 +11,17 @@ const setStopWatchOffset = (time: string) => {
return currentTime;
};
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => {
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
offsetTimestamp: Date;
}
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
const { totalSeconds } = useStopwatch({
autoStart: true,
offsetTimestamp: offsetTimestamp,
});
return <FormatMillisecond time={totalSeconds * 1000} />;
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
};
export { Stopwatch, setStopWatchOffset };
@@ -0,0 +1,30 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import React from 'react';
interface CancelDeploymentDialogProps extends ConfirmDialogProps {}
export const CancelDeploymentDialog = ({
open,
handleCancel,
handleConfirm,
...props
}: CancelDeploymentDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Cancel deployment?"
handleCancel={handleCancel}
open={open}
confirmButtonTitle="Yes, cancel deployment"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
>
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
This will halt the deployment and you&apos;ll have to start the process
from scratch.
</p>
</ConfirmDialog>
);
};
@@ -0,0 +1,90 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import { Deployment, Domain } from 'gql-client';
import React from 'react';
import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard';
import { Button } from 'components/shared/Button';
import {
ChevronDoubleDownIcon,
LinkChainIcon,
} from 'components/shared/CustomIcon';
import { TagProps } from 'components/shared/Tag';
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
deployment: Deployment;
newDeployment?: Deployment;
domains: Domain[];
}
export const ChangeStateToProductionDialog = ({
deployment,
newDeployment,
domains,
open,
handleCancel,
handleConfirm,
...props
}: ChangeStateToProductionDialogProps) => {
const currentChip = {
value: 'Live Deployment',
type: 'positive' as TagProps['type'],
};
const newChip = {
value: 'New Deployment',
type: 'attention' as TagProps['type'],
};
return (
<ConfirmDialog
{...props}
handleCancel={handleCancel}
open={open}
handleConfirm={handleConfirm}
>
<div className="flex flex-col gap-7">
<div className="flex flex-col gap-3">
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
Upon confirmation, this deployment will be changed to production.
</p>
<DeploymentDialogBodyCard
deployment={deployment}
chip={newDeployment ? currentChip : undefined}
/>
{newDeployment && (
<>
<div className="flex items-center justify-between w-full text-elements-info">
{Array.from({ length: 7 }).map((_, index) => (
<ChevronDoubleDownIcon key={index} />
))}
</div>
<DeploymentDialogBodyCard
deployment={newDeployment}
chip={newChip}
/>
</>
)}
</div>
<div className="flex flex-col items-start gap-3">
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
The new deployment will be associated with these domains:
</p>
{domains.length > 0 &&
domains.map((value) => {
return (
<Button
as="a"
href={value.name}
leftIcon={<LinkChainIcon size={18} />}
variant="link"
key={value.id}
>
{value.name}
</Button>
);
})}
</div>
</div>
</ConfirmDialog>
);
};
@@ -0,0 +1,42 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import React from 'react';
interface DeleteDomainDialogProps extends ConfirmDialogProps {
projectName: string;
domainName: string;
}
export const DeleteDomainDialog = ({
projectName,
domainName,
open,
handleCancel,
handleConfirm,
...props
}: DeleteDomainDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Delete domain?"
handleCancel={handleCancel}
open={open}
confirmButtonTitle="Yes, delete domain"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
>
<p className="text-sm text-elements-high-em">
Once deleted, the project{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{projectName}
</span>{' '}
will not be accessible from the domain{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{domainName}
</span>
.
</p>
</ConfirmDialog>
);
};
@@ -0,0 +1,36 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import React from 'react';
interface DeleteVariableDialogProps extends ConfirmDialogProps {
variableKey: string;
}
export const DeleteVariableDialog = ({
variableKey,
open,
handleCancel,
handleConfirm,
...props
}: DeleteVariableDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Delete variable"
handleCancel={handleCancel}
open={open}
confirmButtonTitle="Yes, confirm delete"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
>
<p className="text-sm text-elements-mid-em">
Are you sure you want to delete the variable{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{variableKey}
</span>
?
</p>
</ConfirmDialog>
);
};
@@ -0,0 +1,36 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import React from 'react';
interface DeleteWebhookDialogProps extends ConfirmDialogProps {
webhookUrl: string;
}
export const DeleteWebhookDialog = ({
webhookUrl,
open,
handleCancel,
handleConfirm,
...props
}: DeleteWebhookDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Delete webhook?"
handleCancel={handleCancel}
open={open}
confirmButtonTitle="Yes, confirm delete"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
>
<p className="text-sm text-elements-mid-em">
Are you sure you want to delete{' '}
<span className="text-sm font-mono text-elements-high-em px-0.5">
{webhookUrl}
</span>
?
</p>
</ConfirmDialog>
);
};
@@ -0,0 +1,30 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import React from 'react';
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
export const DisconnectRepositoryDialog = ({
open,
handleCancel,
handleConfirm,
...props
}: DisconnectRepositoryDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Disconnect repository?"
handleCancel={handleCancel}
open={open}
confirmButtonTitle="Yes, confirm disconnect"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
>
<p className="text-sm text-elements-high-em">
Any data tied to your Git project may become misconfigured. Are you sure
you want to continue?
</p>
</ConfirmDialog>
);
};
@@ -0,0 +1,38 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import React from 'react';
import { formatAddress } from 'utils/format';
interface RemoveMemberDialogProps extends ConfirmDialogProps {
memberName: string;
ethAddress: string;
emailDomain: string;
}
export const RemoveMemberDialog = ({
memberName,
ethAddress,
emailDomain,
open,
handleCancel,
handleConfirm,
...props
}: RemoveMemberDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Remove member?"
handleCancel={handleCancel}
open={open}
confirmButtonTitle="Yes, remove member"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
>
<p className="text-sm text-elements-high-em">
Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@
{emailDomain}) will not be able to access this project.
</p>
</ConfirmDialog>
);
};
@@ -0,0 +1,47 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import React from 'react';
interface TransferProjectDialogProps extends ConfirmDialogProps {
projectName: string;
from: string;
to: string;
}
export const TransferProjectDialog = ({
projectName,
from,
to,
open,
handleCancel,
handleConfirm,
...props
}: TransferProjectDialogProps) => {
return (
<ConfirmDialog
{...props}
dialogTitle="Transfer project?"
handleCancel={handleCancel}
open={open}
confirmButtonTitle="Yes, confirm transfer"
handleConfirm={handleConfirm}
>
<p className="text-sm text-elements-high-em">
Upon confirmation, your project{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{projectName}
</span>{' '}
will be transferred from{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{from}
</span>{' '}
to{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{to}
</span>
.
</p>
</ConfirmDialog>
);
};
@@ -1,65 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Project } from 'gql-client';
import {
Menu,
MenuHandler,
MenuList,
MenuItem,
Typography,
Avatar,
} from '@material-tailwind/react';
import { relativeTimeMs } from '../../utils/time';
interface ProjectCardProps {
project: Project;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
return (
<div className="bg-white border border-gray-200 rounded-lg shadow">
<div className="flex gap-2 p-2 items-center">
<Avatar variant="rounded" src={project.icon || '/gray.png'} />
<div className="grow">
<Link to={`projects/${project.id}`}>
<Typography>{project.name}</Typography>
<Typography color="gray" variant="small">
{project.deployments[0]?.domain?.name ??
'No Production Deployment'}
</Typography>
</Link>
</div>
<Menu placement="bottom-end">
<MenuHandler>
<button>...</button>
</MenuHandler>
<MenuList>
<MenuItem>^ Project settings</MenuItem>
<MenuItem className="text-red-500">^ Delete project</MenuItem>
</MenuList>
</Menu>
</div>
<div className="border-t-2 border-solid p-4 bg-gray-50">
{project.deployments.length > 0 ? (
<>
<Typography variant="small" color="gray">
^ {project.deployments[0].commitMessage}
</Typography>
<Typography variant="small" color="gray">
{relativeTimeMs(project.deployments[0].createdAt)} on ^&nbsp;
{project.deployments[0].branch}
</Typography>
</>
) : (
<Typography variant="small" color="gray">
No Production deployment
</Typography>
)}
</div>
</div>
);
};
export default ProjectCard;
@@ -0,0 +1,82 @@
import { VariantProps, tv } from 'tailwind-variants';
export const projectCardTheme = tv({
slots: {
wrapper: [
'bg-surface-card',
'shadow-card',
'rounded-2xl',
'flex',
'flex-col',
'group',
'cursor-pointer',
],
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
content: ['flex', 'flex-col', 'gap-1', 'flex-1', 'overflow-hidden'],
title: [
'text-sm',
'font-medium',
'text-elements-high-em',
'tracking-[-0.006em]',
'truncate',
],
description: ['text-xs', 'text-elements-low-em', 'truncate'],
icons: ['flex', 'items-center', 'gap-1'],
lowerContent: [
'transition-colors',
'duration-150',
'px-4',
'py-4',
'flex',
'flex-col',
'gap-2',
'rounded-b-2xl',
'group-hover:bg-surface-card-hovered',
],
latestDeployment: ['flex', 'items-center', 'gap-2'],
deploymentStatusContainer: [
'h-3',
'w-3',
'flex',
'items-center',
'justify-center',
],
deploymentStatus: ['w-1', 'h-1', 'rounded-full'],
deploymentName: ['text-xs', 'text-elements-low-em'],
deploymentText: [
'text-xs',
'text-elements-low-em',
'font-mono',
'flex',
'items-center',
'gap-2',
],
wavyBorder: [
'bg-surface-card',
'transition-colors',
'duration-150',
'group-hover:bg-surface-card-hovered',
],
},
variants: {
status: {
success: {
deploymentStatus: ['bg-emerald-500'],
},
'in-progress': {
deploymentStatus: ['bg-orange-400'],
},
failure: {
deploymentStatus: ['bg-rose-500'],
},
pending: {
deploymentStatus: ['bg-gray-500'],
},
},
},
defaultVariants: {
status: 'pending',
},
});
export type ProjectCardTheme = VariantProps<typeof projectCardTheme>;
@@ -0,0 +1,142 @@
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@material-tailwind/react';
import React, {
ComponentPropsWithoutRef,
MouseEvent,
useCallback,
} from 'react';
import { useNavigate } from 'react-router-dom';
import { Project } from 'gql-client';
import { Avatar } from 'components/shared/Avatar';
import { Button } from 'components/shared/Button';
import {
BranchIcon,
ClockIcon,
GitHubLogo,
HorizontalDotIcon,
WarningDiamondIcon,
} from 'components/shared/CustomIcon';
import { Tooltip } from 'components/shared/Tooltip';
import { WavyBorder } from 'components/shared/WavyBorder';
import { relativeTimeMs } from 'utils/time';
import { getInitials } from 'utils/geInitials';
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
export interface ProjectCardProps
extends ComponentPropsWithoutRef<'div'>,
ProjectCardTheme {
project: Project;
}
// TODO: Update the whole component to use `Link` from `react-router-dom` and remove the `useNavigate` hook,
// currently it's not possible to use `Link` because the dot menu is not a direct child of the `Link` component
export const ProjectCard = ({
className,
project,
status = 'failure',
...props
}: ProjectCardProps) => {
const theme = projectCardTheme();
const hasDeployment = project.deployments.length > 0;
// TODO: Update this to use the actual status from the API
const hasError = status === 'failure';
const navigate = useNavigate();
const handleOptionsClick = (
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
) => {
e.stopPropagation();
};
const handleClick = useCallback(() => {
navigate(`projects/${project.id}`);
}, [project.id, navigate]);
return (
<div
{...props}
className={theme.wrapper({ className })}
onClick={handleClick}
>
{/* Upper content */}
<div className={theme.upperContent()}>
{/* Icon container */}
<Avatar
size={48}
imageSrc={project.icon}
initials={getInitials(project.name)}
/>
{/* Title and website */}
<div className={theme.content()}>
<Tooltip content={project.name}>
<p className={theme.title()}>{project.name}</p>
</Tooltip>
<p className={theme.description()}>
{project.deployments[0]?.domain?.name ?? 'No domain'}
</p>
</div>
{/* Icons */}
<div className={theme.icons()}>
{hasError && <WarningDiamondIcon className="text-elements-danger" />}
<Menu placement="bottom-end">
<MenuHandler>
<Button
shape="default"
size="xs"
variant="ghost"
iconOnly
onClick={handleOptionsClick}
>
<HorizontalDotIcon />
</Button>
</MenuHandler>
<MenuList placeholder={''}>
<MenuItem placeholder={''}>Project settings</MenuItem>
<MenuItem className="text-red-500" placeholder={''}>
Delete project
</MenuItem>
</MenuList>
</Menu>
</div>
</div>
{/* Wave */}
<WavyBorder variant="stroke-and-fill" className={theme.wavyBorder()} />
{/* Lower content */}
<div className={theme.lowerContent()}>
{/* Latest deployment */}
<div className={theme.latestDeployment()}>
{/* Dot icon */}
<div className={theme.deploymentStatusContainer()}>
<div className={theme.deploymentStatus({ status })} />
</div>
<p className={theme.deploymentText()}>
{hasDeployment
? project.deployments[0]?.commitMessage
: 'No production deployment'}
</p>
</div>
{/* Deployment and branch name */}
<div className={theme.deploymentText()}>
{hasDeployment ? (
<>
<GitHubLogo />
<span>{relativeTimeMs(project.deployments[0].createdAt)} on</span>
<BranchIcon />
<span>{project.deployments[0].branch}</span>
</>
) : (
<>
<ClockIcon />
<span>Created {relativeTimeMs(project.createdAt)}</span>
</>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from './ProjectCard';
@@ -1,118 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useCombobox } from 'downshift';
import { Project } from 'gql-client';
import { useDebounce } from 'usehooks-ts';
import {
List,
ListItem,
ListItemPrefix,
Card,
Typography,
Avatar,
} from '@material-tailwind/react';
import SearchBar from '../SearchBar';
import { useGQLClient } from '../../context/GQLClientContext';
interface ProjectsSearchProps {
onChange?: (data: Project) => void;
}
const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
const [items, setItems] = useState<Project[]>([]);
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient();
const {
isOpen,
getMenuProps,
getInputProps,
getItemProps,
highlightedIndex,
inputValue,
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
if (onChange) {
onChange(newSelectedItem);
}
}
},
});
const debouncedInputValue = useDebounce<string>(inputValue, 500);
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue);
setItems(searchProjects);
},
[client],
);
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue);
}
}, [fetchProjects, debouncedInputValue]);
return (
<div className="relative">
<SearchBar {...getInputProps()} />
<Card
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
(!inputValue || !isOpen) && 'hidden'
}`}
>
<List {...getMenuProps()}>
{items.length ? (
<>
<div className="p-3">
<Typography variant="small" color="gray">
Suggestions
</Typography>
</div>
{items.map((item, index) => (
<ListItem
selected={highlightedIndex === index || selectedItem === item}
key={item.id}
{...getItemProps({ item, index })}
>
<ListItemPrefix>
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
</ListItemPrefix>
<div>
<Typography variant="h6" color="blue-gray">
{item.name}
</Typography>
<Typography
variant="small"
color="gray"
className="font-normal"
>
{item.organization.name}
</Typography>
</div>
</ListItem>
))}
</>
) : (
<div className="p-3">
<Typography>^ No projects matching this name</Typography>
</div>
)}
</List>
</Card>
</div>
);
};
export default ProjectSearchBar;
@@ -0,0 +1,93 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useCombobox } from 'downshift';
import { Project } from 'gql-client';
import { useDebounce } from 'usehooks-ts';
import SearchBar from 'components/SearchBar';
import { useGQLClient } from 'context/GQLClientContext';
import { cn } from 'utils/classnames';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
interface ProjectSearchBarProps {
onChange?: (data: Project) => void;
}
export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
const [items, setItems] = useState<Project[]>([]);
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient();
const {
isOpen,
getMenuProps,
getInputProps,
getItemProps,
highlightedIndex,
inputValue,
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
if (onChange) {
onChange(newSelectedItem);
}
}
},
});
const debouncedInputValue = useDebounce<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue);
setItems(searchProjects);
},
[client],
);
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue);
}
}, [fetchProjects, debouncedInputValue]);
return (
<div className="relative w-full lg:w-fit">
<SearchBar {...getInputProps()} />
<div
{...getMenuProps()}
className={cn(
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen },
)}
>
{items.length ? (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
{items.map((item, index) => (
<ProjectSearchBarItem
{...getItemProps({ item, index })}
key={item.id}
item={item}
active={highlightedIndex === index || selectedItem === item}
/>
))}
</>
) : (
<ProjectSearchBarEmpty />
)}
</div>
</div>
);
};
@@ -0,0 +1,112 @@
import React, { useCallback, useEffect, useState } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { Button } from 'components/shared/Button';
import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
import { Input } from 'components/shared/Input';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useDebounce } from 'usehooks-ts';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { useNavigate } from 'react-router-dom';
import { useCombobox } from 'downshift';
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
open?: boolean;
onClose?: () => void;
onClickItem?: (data: Project) => void;
}
export const ProjectSearchBarDialog = ({
onClose,
onClickItem,
...props
}: ProjectSearchBarDialogProps) => {
const [items, setItems] = useState<Project[]>([]);
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient();
const navigate = useNavigate();
const { getInputProps, getItemProps, inputValue, setInputValue } =
useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
onClickItem?.(newSelectedItem);
navigate(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
);
}
},
});
const debouncedInputValue = useDebounce<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue);
setItems(searchProjects);
},
[client],
);
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue);
}
}, [fetchProjects, debouncedInputValue]);
const handleClose = () => {
setInputValue('');
setItems([]);
onClose?.();
};
return (
<Dialog.Root {...props}>
<Dialog.Portal>
<Dialog.Overlay className="bg-base-bg fixed inset-0 md:hidden overflow-y-auto" />
<Dialog.Content>
<div className="h-full flex flex-col fixed top-0 inset-0">
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<Input
{...getInputProps()}
leftIcon={<SearchIcon />}
placeholder="Search"
appearance="borderless"
autoFocus
/>
<Button iconOnly variant="ghost" onClick={handleClose}>
<CrossIcon size={16} />
</Button>
</div>
{/* Content */}
<div className="flex flex-col gap-1 px-2 py-2">
{items.length > 0
? items.map((item, index) => (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
<ProjectSearchBarItem
{...getItemProps({ item, index })}
key={item.id}
item={item}
/>
</>
))
: inputValue && <ProjectSearchBarEmpty />}
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
};
@@ -0,0 +1,24 @@
import { InfoRoundFilledIcon } from 'components/shared/CustomIcon';
import React, { ComponentPropsWithoutRef } from 'react';
import { cn } from 'utils/classnames';
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
export const ProjectSearchBarEmpty = ({
className,
...props
}: ProjectSearchBarEmptyProps) => {
return (
<div
{...props}
className={cn('flex items-center px-2 py-2 gap-3', className)}
>
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning">
<InfoRoundFilledIcon size={16} />
</div>
<p className="text-elements-low-em text-sm tracking-[-0.006em]">
No projects matching this name
</p>
</div>
);
};
@@ -0,0 +1,59 @@
import { Avatar } from 'components/shared/Avatar';
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
import { Project } from 'gql-client';
import React, { ComponentPropsWithoutRef, forwardRef } from 'react';
import { OmitCommon } from 'types/common';
import { cn } from 'utils/classnames';
import { getInitials } from 'utils/geInitials';
/**
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
* @type {MergedComponentPropsWithoutRef}
*/
type MergedComponentPropsWithoutRef = OmitCommon<
ComponentPropsWithoutRef<'button'>,
Omit<
Overwrite<UseComboboxGetItemPropsReturnValue, Project[]>,
'index' | 'item'
>
>;
interface ProjectSearchBarItemProps extends MergedComponentPropsWithoutRef {
item: Project;
active?: boolean;
}
const ProjectSearchBarItem = forwardRef<
HTMLButtonElement,
ProjectSearchBarItemProps
>(({ item, active, ...props }, ref) => {
return (
<button
{...props}
ref={ref}
key={item.id}
className={cn(
'px-2 py-2 flex items-center gap-3 rounded-lg text-left hover:bg-base-bg-emphasized',
{
'bg-base-bg-emphasized': active,
},
)}
>
<Avatar
size={32}
imageSrc={item.icon}
initials={getInitials(item.name)}
/>
<div className="flex flex-col flex-1">
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
{item.name}
</p>
<p className="text-xs text-elements-low-em">{item.organization.name}</p>
</div>
</button>
);
});
ProjectSearchBarItem.displayName = 'ProjectSearchBarItem';
export { ProjectSearchBarItem };
@@ -0,0 +1,2 @@
export * from './ProjectSearchBar';
export * from './ProjectSearchBarDialog';
@@ -1,73 +1,103 @@
import React from 'react';
import OauthPopup from 'react-oauth-popup';
import { GitType } from 'gql-client';
import { Button } from '@material-tailwind/react';
import { useGQLClient } from '../../../context/GQLClientContext';
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
import { Button } from 'components/shared/Button';
import {
GitIcon,
EllipsesIcon,
SnowballIcon,
GithubIcon,
GitTeaIcon,
} from 'components/shared/CustomIcon';
import { useToast } from 'components/shared/Toast';
import { IconWithFrame } from 'components/shared/IconWithFrame';
import { Heading } from 'components/shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard';
const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
process.env.REACT_APP_GITHUB_CLIENT_ID
}&scope=${encodeURIComponent(SCOPES)}`;
const REDIRECT_URI = `${window.location.origin}/organization/projects/create`;
const GITEA_OAUTH_URL = `https://git.vdb.to/login/oauth/authorize?client_id=${process.env.REACT_APP_GITEA_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`;
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 (type: GitType, code: string) => {
const { toast, dismiss } = useToast();
const handleCode = async (code: string) => {
// Pass code to backend and get access token
const {
authenticateGit: { token },
} = await client.authenticateGit(type, code);
// TODO: Handle token according to Git type
authenticateGitHub: { token },
} = await client.authenticateGitHub(code);
onToken(token);
toast({
onDismiss: dismiss,
id: 'connected-to-github',
title: 'The Git account is connected.',
variant: 'success',
});
};
// TODO: Use correct height
return (
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
<div>^</div>
<div>
<p>Connect to your git account</p>
<p>
Once connected, you can import a repository from your
<br />
account or start with one of our templates.
</p>
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
<div className="flex flex-col items-center max-w-[420px]">
{/** Icons */}
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
<IconWithFrame icon={<GitIcon />} />
<EllipsesIcon className="items-center gap-1.5 flex" />
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
</div>
{/** Text */}
<div className="flex flex-col gap-1.5 mb-6">
<Heading className="text-xl font-medium">
Connect to your Git account
</Heading>
<p className="text-center text-elements-mid-em">
Once connected, you can import a repository from your account or
start with one of our templates.
</p>
</div>
{/** CTA Buttons */}
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={handleCode}
onClose={() => {}}
title="Snowball"
width={1000}
height={1000}
>
<Button
className="w-full sm:w-auto"
leftIcon={<GithubIcon />}
variant="tertiary"
>
Connect to GitHub
</Button>
</OauthPopup>
<Button
className="w-full sm:w-auto"
leftIcon={<GitTeaIcon />}
variant="tertiary"
>
Connect to GitTea
</Button>
</div>
</div>
<div className="mt-2 flex">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={(code) => handleCode(GitType.GitHub, code)}
onClose={() => {}}
title="Snowball"
width={1000}
height={1000}
>
<Button className="rounded-full mx-2">Connect to Github</Button>
</OauthPopup>
<OauthPopup
url={GITEA_OAUTH_URL}
onCode={(code) => handleCode(GitType.Gitea, code)}
onClose={() => {}}
title="Snowball"
width={1000}
height={1000}
>
<Button className="rounded-full mx-2">Connect to Gitea</Button>
</OauthPopup>
</div>
<ConnectAccountTabPanel />
{/* TODO: Add ConnectAccountTabPanel */}
<MockConnectGitCard />
{/* <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,13 +1,14 @@
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 { Stopwatch, setStopWatchOffset } from 'components/StopWatch';
import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon';
import { CancelDeploymentDialog } from 'components/projects/Dialog/CancelDeploymentDialog';
const INTERVAL_DURATION = 5000;
const TIMEOUT_DURATION = 5000;
const Deploy = () => {
const [searchParams] = useSearchParams();
const projectId = searchParams.get('projectId');
@@ -25,64 +26,64 @@ const Deploy = () => {
useEffect(() => {
const timerID = setTimeout(() => {
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
}, INTERVAL_DURATION);
}, TIMEOUT_DURATION);
return () => clearInterval(timerID);
}, []);
return (
<div>
<div className="flex justify-between mb-6">
<div>
<h4>Deployment started ...</h4>
<div className="flex">
^&nbsp;
<div className="space-y-7">
<div className="flex justify-between">
<div className="space-y-1.5">
<Heading as="h4" className="md:text-lg font-medium">
Deployment started ...
</Heading>
<div className="flex items-center gap-1.5">
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
<Stopwatch
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
/>
</div>
</div>
<div>
<Button onClick={handleOpen} variant="outlined" size="sm">
^ Cancel
</Button>
</div>
<ConfirmDialog
dialogTitle="Cancel deployment?"
handleOpen={handleOpen}
open={open}
confirmButtonTitle="Yes, Cancel deployment"
handleConfirm={handleCancel}
color="red"
<Button
onClick={handleOpen}
size="sm"
variant="tertiary"
leftIcon={<WarningIcon size={16} />}
>
<Typography variant="small">
This will halt the deployment and you will have to start the process
from scratch.
</Typography>
</ConfirmDialog>
Cancel
</Button>
<CancelDeploymentDialog
handleCancel={handleOpen}
open={open}
handleConfirm={handleCancel}
/>
</div>
<div>
<DeployStep
title="Building"
status={DeployStatus.COMPLETE}
step="1"
processTime="72000"
/>
<DeployStep
title="Deployment summary"
status={DeployStatus.PROCESSING}
step="2"
startTime={Date.now().toString()}
/>
<DeployStep
title="Running checks"
status={DeployStatus.NOT_STARTED}
step="3"
/>
<DeployStep
title="Assigning domains"
status={DeployStatus.NOT_STARTED}
step="4"
/>
</div>
<DeployStep
title="Building"
status={DeployStatus.COMPLETE}
step="1"
processTime="72000"
/>
<DeployStep
title="Deployment summary"
status={DeployStatus.PROCESSING}
step="2"
startTime={Date.now().toString()}
/>
<DeployStep
title="Running checks"
status={DeployStatus.NOT_STARTED}
step="3"
/>
<DeployStep
title="Assigning domains"
status={DeployStatus.NOT_STARTED}
step="4"
/>
</div>
);
};
@@ -1,11 +1,22 @@
import React, { useState } from 'react';
import toast from 'react-hot-toast';
import { Collapse, Button, Typography } from '@material-tailwind/react';
import { Collapse } from '@material-tailwind/react';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import FormatMillisecond from '../../FormatMilliSecond';
import processLogs from '../../../assets/process-logs.json';
import { cn } from 'utils/classnames';
import {
CheckRoundFilledIcon,
ClockOutlineIcon,
CopyIcon,
LoaderIcon,
MinusCircleIcon,
PlusIcon,
} from 'components/shared/CustomIcon';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { useIntersectionObserver } from 'usehooks-ts';
enum DeployStatus {
PROCESSING = 'progress',
@@ -28,55 +39,115 @@ const DeployStep = ({
startTime,
processTime,
}: DeployStepsProps) => {
const [collapse, setCollapse] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const { toast, dismiss } = useToast();
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
threshold: 1,
});
const disableCollapse = status !== DeployStatus.COMPLETE;
return (
<div className="border-b-2">
<div className="flex justify-between p-2 gap-2">
{status === DeployStatus.NOT_STARTED && <div>{step}</div>}
{status === DeployStatus.PROCESSING && <div>O</div>}
{status === DeployStatus.COMPLETE && (
<div>
<button
onClick={() => {
setCollapse(!collapse);
}}
>
{collapse ? '-' : '+'}
</button>
<div className="border-b border-border-separator">
{/* Collapisble trigger */}
<button
className={cn(
'flex justify-between w-full py-5 gap-2',
disableCollapse && 'cursor-auto',
)}
tabIndex={disableCollapse ? -1 : undefined}
onClick={() => {
if (!disableCollapse) {
setIsOpen((val) => !val);
}
}}
>
<div className={cn('grow flex items-center gap-3')}>
{/* Icon */}
<div className="w-6 h-6 grid place-content-center">
{status === DeployStatus.NOT_STARTED && (
<div className="grid place-content-center w-6 h-6 rounded-[48px] bg-base-bg-emphasized font-mono text-xs">
{step}
</div>
)}
{status === DeployStatus.PROCESSING && (
<LoaderIcon className="animate-spin text-elements-link" />
)}
{status === DeployStatus.COMPLETE && (
<div className="text-controls-primary">
{!isOpen && <PlusIcon size={24} />}
{isOpen && <MinusCircleIcon size={24} />}
</div>
)}
</div>
{/* Title */}
<span
className={cn(
'text-left text-sm md:text-base',
status === DeployStatus.PROCESSING && 'text-elements-link',
)}
>
{title}
</span>
</div>
{/* Timer */}
{status === DeployStatus.PROCESSING && (
<div className="flex items-center gap-1.5">
<ClockOutlineIcon size={16} className="text-elements-low-em" />
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
</div>
)}
<div className="grow">{title}</div>
{status === DeployStatus.PROCESSING && (
<>
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
</>
)}
{status === DeployStatus.COMPLETE && (
<>
^<FormatMillisecond time={Number(processTime)} />{' '}
</>
<div className="flex items-center gap-1.5">
<div className="w-4.5 h-4.5 grid place-content-center">
<CheckRoundFilledIcon
className="text-elements-success"
size={15}
/>
</div>
<FormatMillisecond time={Number(processTime)} />{' '}
</div>
)}
</div>
<Collapse open={collapse}>
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
</button>
{/* Collapsible */}
<Collapse open={isOpen}>
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
{/* Logs */}
{processLogs.map((log, key) => {
return (
<Typography variant="small" color="gray" key={key}>
<p className="font-mono" key={key}>
{log}
</Typography>
</p>
);
})}
<div className="sticky bottom-0 left-1/2 flex justify-center">
{/* End of logs ref used for hiding gradient overlay */}
<div ref={ref} />
{/* Overflow gradient overlay */}
{!hideGradientOverlay && (
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
)}
{/* Copy log button */}
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
<Button
size="sm"
size="xs"
onClick={() => {
navigator.clipboard.writeText(processLogs.join('\n'));
toast.success('Logs copied');
toast({
title: 'Logs copied',
variant: 'success',
id: 'logs',
onDismiss: dismiss,
});
}}
color="blue"
leftIcon={<CopyIcon size={16} />}
>
^ Copy log
Copy log
</Button>
</div>
</div>
@@ -0,0 +1,163 @@
import React, { useMemo } from 'react';
import { SegmentedControls } from 'components/shared/SegmentedControls';
import { useState } from 'react';
import {
GithubIcon,
LockIcon,
TemplateIcon,
TemplateIconType,
} from 'components/shared/CustomIcon';
import { relativeTimeISO } from 'utils/time';
import { useMediaQuery } from 'usehooks-ts';
export const MockConnectGitCard = () => {
const [segmentedControlsValue, setSegmentedControlsValue] =
useState<string>('import');
const isDesktopView = useMediaQuery('(min-width: 960px)'); // lg:
const segmentedControlsProps = isDesktopView ? {} : { size: 'sm' as const };
const fiveMinutesAgo = new Date(Date.now() - 1000 * 300).toISOString();
const SEGMENTED_CONTROLS_OPTIONS = [
{
label: 'Import a repository',
value: 'import',
},
{
label: 'Start with a template',
value: 'template',
},
];
const IMPORT_CONTENT = [
{
full_name: 'snowball/igloo',
updated_at: fiveMinutesAgo,
},
{
full_name: 'snowball/android-sdk',
updated_at: fiveMinutesAgo,
visibility: 'private',
},
{
full_name: 'snowball/landing-page',
updated_at: fiveMinutesAgo,
},
];
const TEMPLATE_CONTENT = [
{
name: 'Web app',
icon: 'web',
},
{
name: 'Progressive Web App (PWA)',
icon: 'pwa',
},
{
name: 'React Native',
icon: 'react-native',
},
{
name: 'Kotlin',
icon: 'kotlin',
},
{
name: 'Swift',
icon: 'swift',
},
];
const renderContent = useMemo(() => {
if (segmentedControlsValue === 'import') {
return (
<div className="flex flex-col gap-2 relative z-0">
{IMPORT_CONTENT.map((repo, index) => (
<>
<MockProjectCard key={index} {...repo} />
{index !== IMPORT_CONTENT.length - 1 && (
<div className="border-b border-base-border" />
)}
</>
))}
</div>
);
}
return (
<div className="grid grid-cols-1 lg:grid-cols-2 relative z-0">
{TEMPLATE_CONTENT.map((template, index) => (
<MockTemplateCard key={index} {...template} />
))}
</div>
);
}, [segmentedControlsValue]);
return (
<div className="relative bg-base-bg shadow-card rounded-2xl px-2 py-2 w-full max-w-[560px] flex flex-col gap-2">
{/* Content */}
<SegmentedControls
value={segmentedControlsValue}
onChange={setSegmentedControlsValue}
options={SEGMENTED_CONTROLS_OPTIONS}
type="full-width"
{...segmentedControlsProps}
/>
{renderContent}
{/* Shade */}
<div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white to-transparent" />
</div>
);
};
const MockProjectCard = ({
full_name,
updated_at,
visibility,
}: {
full_name: string;
updated_at?: string;
visibility?: string;
}) => {
return (
<div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative">
{/* Icon container */}
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
<GithubIcon />
</div>
{/* Content */}
<div className="flex flex-1 gap-3 flex-wrap">
<div className="flex flex-col items-start gap-1">
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
{full_name}
</p>
<p className="text-elements-low-em text-xs">
{updated_at && relativeTimeISO(updated_at)}
</p>
</div>
{visibility === 'private' && (
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
<LockIcon />
Private
</div>
)}
</div>
</div>
);
};
const MockTemplateCard = ({ icon, name }: { icon: string; name: string }) => {
return (
<div className="flex items-center gap-3 px-3 py-3 hover:bg-base-bg-emphasized rounded-2xl group relative cursor-default">
{/* Icon */}
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
<TemplateIcon type={icon as TemplateIconType} />
</div>
{/* Name */}
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
{name}
</p>
</div>
);
};
@@ -1,76 +0,0 @@
import React, { useCallback } from 'react';
import toast from 'react-hot-toast';
import { useNavigate, useParams } from 'react-router-dom';
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
import { relativeTimeISO } from '../../../utils/time';
import { GitRepositoryDetails } from '../../../types';
import { useGQLClient } from '../../../context/GQLClientContext';
interface ProjectRepoCardProps {
repository: GitRepositoryDetails;
}
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
const client = useGQLClient();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState(false);
const { orgSlug } = useParams();
const createProject = useCallback(async () => {
if (!repository) {
return;
}
setIsLoading(true);
const { addProject } = await client.addProject(orgSlug!, {
name: `${repository.owner!.login}-${repository.name}`,
prodBranch: repository.default_branch!,
repository: repository.full_name,
// TODO: Compute template from repo
template: 'webapp',
});
if (Boolean(addProject)) {
setIsLoading(false);
navigate(`import?projectId=${addProject.id}`);
} else {
setIsLoading(false);
toast.error('Failed to create project');
}
}, [client, repository]);
return (
<div
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
onClick={createProject}
>
<div>^</div>
<div className="grow">
<div>
<span className="text-black">{repository.full_name}</span>
{repository.visibility === 'private' && (
<Chip
className="normal-case inline ml-6 font-normal"
size="sm"
value="Private"
icon={'^'}
/>
)}
</div>
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
</div>
{isLoading ? (
<Spinner className="h-4 w-4" />
) : (
<div className="hidden group-hover:block">
<IconButton size="sm">{'>'}</IconButton>
</div>
)}
</div>
);
};
export default ProjectRepoCard;

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