chore: clean workspaces code (#3241)
### **PR Type** Enhancement ___ ### **Description** - Migrate from workspaces to organizations - Update GraphQL queries and mutations - Refactor components for organization structure - Adjust routing and navigation for orgs ___ ### **Changes walkthrough** 📝 <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>16 files</summary><table> <tr> <td><strong>graphql.ts</strong><dd><code>Update GraphQL types and queries for organization structure</code></dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-fbd5db84b560b1c91675004448c6c7fa0dcbfb28b9eb05d53b03e6cb7b83ebac">+84/-1054</a></td> </tr> <tr> <td><strong>MobileNav.tsx</strong><dd><code>Simplify MobileNav component and update navigation</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-88408885daaec8805bd085b53462c9f2d95db32f7e523912837a8167211b4fb2">+11/-126</a></td> </tr> <tr> <td><strong>ticket.tsx</strong><dd><code>Update support ticket form for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-a66cba186d2014b03f1a0e005147ae7b48e88933700fe065d235cd819a949a97">+28/-84</a> </td> </tr> <tr> <td><strong>ApplicationUnknown.tsx</strong><dd><code>Refactor ApplicationUnknown component for new structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-d1d7044dd66488c5bc787a89612754b283eedb404d4d6abcface2fa533d5c9d3">+17/-21</a> </td> </tr> <tr> <td><strong>DeleteAccount.tsx</strong><dd><code>Update imports and remove workspace references</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-3d84927ffa4b91d986ff6c6f601b3476503220e1c1d8cde25ebf72c8d0ed6b9e">+2/-26</a> </td> </tr> <tr> <td><strong>run-one-click-install.tsx</strong><dd><code>Refactor one-click install for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-00e84c02bfc3c34019e15f820b23e332eeb1933a745be330c3644cb0f63c92b5">+5/-9</a> </td> </tr> <tr> <td><strong>RemoveApplicationModal.tsx</strong><dd><code>Update mutation refetch query for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-e454a42c12dcbfcfaa463ab3421037408634e3a539f460525c79d68adfc118ab">+7/-2</a> </td> </tr> <tr> <td><strong>ApplicationInfo.tsx</strong><dd><code>Update mutation refetch query in ApplicationInfo</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-7372ad22d70c3c354d8e0dd442eb7e49f70f65a386b934b6eee7f8c4b89c3a3f">+8/-3</a> </td> </tr> <tr> <td><strong>useProjectRedirectWhenReady.ts</strong><dd><code>Update refetch query for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-a234bc908266de3091b23b5134a01fd769f96759eb52aa108d2ad4b796b0303f">+2/-6</a> </td> </tr> <tr> <td><strong>DatabaseMigrateVersionConfirmationDialog.tsx</strong><dd><code>Remove workspace refetch query in migration dialog</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-89e193ec45127a72f9491ad89eed5eda5939936686f88aadb48cfac350462271">+1/-5</a> </td> </tr> <tr> <td><strong>new.tsx</strong><dd><code>Update new project page for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-ef97470126e3edc146dda51337aaec556387e2f8a37afa70810d1dc94958f4fd">+3/-3</a> </td> </tr> <tr> <td><strong>BreadcrumbNav.tsx</strong><dd><code>Remove workspace references from BreadcrumbNav</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-2a69d273b2a9e8695d46f6c73dcbb6e161d3bb85f52deb65930018b17b148b3e">+3/-4</a> </td> </tr> <tr> <td><strong>BaseDirectorySettings.tsx</strong><dd><code>Update refetch query for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-50bcccdf949a19ce69fa86acdd63b5291fa2beaba07191a62c87d40ea5b94e88">+4/-4</a> </td> </tr> <tr> <td><strong>DeploymentBranchSettings.tsx</strong><dd><code>Update refetch query for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-d8fc80cc734f593c686f873536856bf9103efb1115ca865709bbeb7bd940895e">+4/-4</a> </td> </tr> <tr> <td><strong>DeploymentListItem.tsx</strong><dd><code>Update refetch query for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-2a548c457ff2ab8fc1bee326a6a3b5eae9d0d6eb18f5ae95bbdb437c3f6b0a73">+2/-2</a> </td> </tr> <tr> <td><strong>index.tsx</strong><dd><code>Update mutations to refetch organization data</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-b4185be97a505e25badcdefe31ea86fa9d69f72264c4bb35eae17fba936a3d47">+4/-3</a> </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>2 files</summary><table> <tr> <td><strong>mocks.ts</strong><dd><code>Update mock data for organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-d1ef12c0f15123bb4e23a0c513fc3d9b5c16af421c71c2909fde3717e09a9d89">+10/-27</a> </td> </tr> <tr> <td><strong>testUtils.tsx</strong><dd><code>Add new test utilities for GraphQL mocking</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-78f29250407edf853a353b48242d3cee59aa5724f38a60bb23bebdfc1ea2f9b5">+50/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>useNotFoundRedirect.ts</strong><dd><code>Update comment to reflect organization structure</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-262687fd80c4510f966a57885b1cc42a6297fd89ab49f6ff49b0df59670027f1">+1/-3</a> </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>77 files</summary><table> <tr> <td><strong>env.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-a1581a28a990763a0fada80d8a3030b70a702d744e98303887f390ac5ae24139">+1/-1</a> </td> </tr> <tr> <td><strong>ContactUs.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-7137edfa9862e14ab2ca4660c679fb62f83990e161267d0dd7deb2977d117ea3">+0/-102</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-7968eed227f9c5da437b28062300b7076b1c124a3e3a335b29d91610c321954b">+0/-2</a> </td> </tr> <tr> <td><strong>InviteNotification.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-9209cf2ec7253c2a3ea03496f2e213b9f6ebf569264394ccd4c5cf5deef1f0b5">+0/-200</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-d4130a4e4d35e0d48479ae89c72650e23cb7a0389224f932efe59722e3a47d93">+0/-1</a> </td> </tr> <tr> <td><strong>TimePicker.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-784f69003ebbc9e39837b920007cef14125a5fc48bb9114226820bcb2b0827b0">+1/-1</a> </td> </tr> <tr> <td><strong>AuthenticatedLayout.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-2d69ccffd267658f76d77a864cdece93fc222e08f6025955795fc6f4697f60e7">+0/-3</a> </td> </tr> <tr> <td><strong>OrgsComboBox.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-0736dac185f4ed134d5b53be292c9a2ee4f6df65e965b801a2dbbc8a184b3687">+2/-9</a> </td> </tr> <tr> <td><strong>PinnedMainNav.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-0fbc67c16a16e263b51e46ada3fbaccc041074f31f541bf663ae3b4b5f2a2a17">+2/-2</a> </td> </tr> <tr> <td><strong>DisplayNameSetting.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-a1daec18d5c3196aee5b2c5303db5654724f8d37cfa427594951a4d02fbe32db">+1/-1</a> </td> </tr> <tr> <td><strong>EmailSetting.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-98bdf4ebec67ab2b4cd475c9df16a39a66505da961a8448eb5e41a33544dcb38">+1/-1</a> </td> </tr> <tr> <td><strong>PATSettings.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-880f5f139ed8c495239dbffee77691f761a004dbc5ce8456a95a259f79fb4136">+1/-1</a> </td> </tr> <tr> <td><strong>PasswordSettings.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-3b25d2f3c57a61224551f9eafaf53f22a70c5767b22ff5b7e2ae85b9c5705dfe">+1/-1</a> </td> </tr> <tr> <td><strong>CreateOrgFormDialog.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-9a1ed9e851328393b81356d80ade3509016aa55c254ed1f4deb692b0bd96f02e">+1/-1</a> </td> </tr> <tr> <td><strong>TransferProjectDialog.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-d4ebdb8af76a7c9e73606708718c3448445545259ad553d73b6d322408e3eb8c">+1/-1</a> </td> </tr> <tr> <td><strong>NotificationsTray.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-727f6debec6a102557407e55c56363e0c75486e30a732158f85c81ada892f77c">+2/-4</a> </td> </tr> <tr> <td><strong>ErrorToast.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-4a05f3b37769de69682260045f29c254b3ad6ef05f059e2b0f77cf9bd68e9bdf">+0/-85</a> </td> </tr> <tr> <td><strong>ErrorToast.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-0095b510fd0557ef1d286cebd9fa102d24e1b0ff4d67148575d158e938304656">+0/-170</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-c16e96ee3476ef73bbc643d7c2399ad9ae8d0cff77a8e554a79c78eea26252ab">+0/-1</a> </td> </tr> <tr> <td><strong>useFinishOrgCreation.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-3b8bf7608ab36d8ab0df895e400f0d2d9e29fad2055b40b33d8d9912a27c99c3">+1/-1</a> </td> </tr> <tr> <td><strong>useRestoreApplicationDatabasePiTR.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-dd5774f502b63d2d443069bedb4c9531a77794a95aaa5c07287093695a4dc60a">+1/-1</a> </td> </tr> <tr> <td><strong>DeleteAssistantModal.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-81fc3c54dbde20f2535b00a52fc28e11ffd80fbcc90c0c34b1b82ff937cce215">+1/-1</a> </td> </tr> <tr> <td><strong>DisableAIServiceConfirmationDialog.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-ae59e5250d9ec095cf3b141efa9734f239aff11c959de9795a94eddd426b1804">+1/-1</a> </td> </tr> <tr> <td><strong>CreateUserForm.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-4e59f7d98f7ab979d2273d8685649f1c39165b2e33b47887645f0dda07edf306">+1/-1</a> </td> </tr> <tr> <td><strong>PointInTimeBackupInfo.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-3980415ca79bf039abb469281fff9b1dc1de0a1ef52b4044d8c6f529538b6edf">+1/-1</a> </td> </tr> <tr> <td><strong>AppLoader.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-fdef910b2c808595c77cb3c0ae573db3ff57cdb4a8161db2e36e86ec548b9b6f">+10/-20</a> </td> </tr> <tr> <td><strong>ApplicationErrored.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-77c5a4128ffd614f299c867e5e3508430946f8f40d4ef5825f57874371fb1101">+0/-272</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-dbaba110b63f272983b09a1a453c0b69577136e7f0f2ff49c4cee6cf78f4325e">+0/-1</a> </td> </tr> <tr> <td><strong>ApplicationPaused.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-14afdf5ac20f058c26563a6992a3751f11cf173eec27206001262b5d1b3b979f">+2/-2</a> </td> </tr> <tr> <td><strong>DeleteServiceModal.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-509d84f75908da0f25dce5f49a6103f3a938c9dd7106b66739ca3758bb83686f">+1/-1</a> </td> </tr> <tr> <td><strong>useCheckProvisioning.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-e1758bb8d3381f814d6619dc33eee8b36e39d2fcb6486d5c8cc3c46bbe62c555">+2/-2</a> </td> </tr> <tr> <td><strong>useIsCurrentUserOwner.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-3941cc4f23c66f12e94850e88e05ca142a627ab2d9ec797ff757dab679c58c0f">+2/-2</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-5a21e22b6c3f0b8f3abd13a4f78cd918662785eee6253480fa0116d11e9c6957">+0/-1</a> </td> </tr> <tr> <td><strong>useNavigationVisible.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-3a395e5461f6112ecf12f399ef008999133a1b3a9d9b267b2ea7f7d5d39d1fe0">+0/-63</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-4f8060ca9eb12226bfb857e06e67f5f3fb583622d878a243e300c9529275c032">+0/-2</a> </td> </tr> <tr> <td><strong>useProjectRoutes.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-ffb341175a52f91f88ce6906c93ff747944ffd3ed9ff9ed27f0894e88e778b66">+0/-160</a> </td> </tr> <tr> <td><strong>RolePermissionEditorForm.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-c7cc670c499aaa76537a1ac3848721988fa0196d4cca8f6b5376b4a14f01341d">+1/-1</a> </td> </tr> <tr> <td><strong>DatabasePiTRSettings.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-85d1f82a571b56469eab40dcc164fdd1e107fba79611ddd5cca7c191fe5117b4">+1/-1</a> </td> </tr> <tr> <td><strong>DatabaseServiceVersionSettings.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-a982b817513fc173371f7468ad642f99ee0c914e5990a48992fc1fa5e230765f">+1/-5</a> </td> </tr> <tr> <td><strong>OverviewDeployments.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-a9440d76cf165e4df8e9db020ee2ab3896281633dbe5ba3691e775d57188bc80">+2/-2</a> </td> </tr> <tr> <td><strong>ResourcesForm.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-8828db70c080be6fc19f88059b08587584f1c23c9159092d6b186ca82a1943aa">+1/-5</a> </td> </tr> <tr> <td><strong>ServiceForm.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-a02746694d45a84390d09b49a1b3eec85c25a8bd9a70b4834ee5af1ba82cb88e">+1/-1</a> </td> </tr> <tr> <td><strong>execPromiseWithErrorToast.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-adc371e89102ef58f14269197d4ce970117519df44ad77174ed6c32128a67079">+1/-1</a> </td> </tr> <tr> <td><strong>getAllWorkspacesAndProjects.gql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-d932cf46bd3efabd2e4240961ce868bfe056319507e0f0738476d2300520df46">+0/-5</a> </td> </tr> <tr> <td><strong>getAppPlansAndGlobalPlans.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-390440124963f8917a01146b85220aaa57a1979f1d0efa5d460b8979121be089">+0/-39</a> </td> </tr> <tr> <td><strong>getApplicationPlan.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-cd0b842260639b906128451c479685925192415ae366c3a584f897022f715ccb">+0/-14</a> </td> </tr> <tr> <td><strong>getWorkspaceAndProject.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-6b457c9426ff027a373b6366fa518466e1bbe31aedd19ae0d5a5ac000defebff">+0/-5</a> </td> </tr> <tr> <td><strong>getWorkspacesAppPlansAndGlobalPlans.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-98bf824b6937b6b6ec16d4c75194876ecdb2ad9e9a4d5bb3681458214007fd02">+0/-39</a> </td> </tr> <tr> <td><strong>insertApp.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-1b04d1a9b24bd1348f12f9f89330e38aa4e64fa9d34f3635a02f23c5bbc767d1">+0/-12</a> </td> </tr> <tr> <td><strong>prefetchNewApp.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-da7b539d835be3df6211788845bfccf4a45259516af11bcae8840f7ac6c2eb9d">+0/-12</a> </td> </tr> <tr> <td><strong>organization.gql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-7a8445445910a3f718136846bcdd03a504adaa0ece372e1cee99855abc26f1a0">+27/-0</a> </td> </tr> <tr> <td><strong>workspace.gql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-503e0160f94a01ed2ac4026bb30e5c3524d54eadba1edb986a8b5e5518112577">+0/-18</a> </td> </tr> <tr> <td><strong>getAllOrganizationsAndProjects.gql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-11d0c4e299315719cac8553bfe6a245fcab0d592611f262b8975066b968799a5">+5/-0</a> </td> </tr> <tr> <td><strong>deletePaymentMethod.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-ed0ca3304c58b0867a3dacc4262b9f3dbe1720f6bbbc4f6b70c630231d3fa842">+0/-5</a> </td> </tr> <tr> <td><strong>getPaymentMethods.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-05e6800ddfae03eec9b811bedb49e519a9683009eef7db1276d483d8810016b2">+0/-27</a> </td> </tr> <tr> <td><strong>insertPaymentMethod.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-580f9ca3fbdc0b48c66b5c358045a24e890c5d53e6ed2ae9818d7775f2564269">+0/-16</a> </td> </tr> <tr> <td><strong>setNewDefaultPaymentMethod.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-fa1366610e5485ac4e423267434c0d9147dc76db7f0842ac2f9d5c32f57e8e22">+0/-17</a> </td> </tr> <tr> <td><strong>deleteWorkspaceMemberInvite.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-85b6d2a6825ba54b85b5cb065705eeb0d65a488fbea853cd46e60208f2d17146">+0/-5</a> </td> </tr> <tr> <td><strong>getWorkspaceMemberInvitesToManage.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-8e97084848d9462ae3a1751d5e5468c5a9772df56d790b4c29a80c89776070a0">+0/-14</a> </td> </tr> <tr> <td><strong>insertWorkspaceMemberInvite.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-7a86261ddc3fcb4863c2fdf607eb73292f0c6175f4fe74303b5f3325279852da">+0/-7</a> </td> </tr> <tr> <td><strong>updateWorkspaceMemberInvite.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-b36ab22e1cc92da61d8499dffa16a96e54e353f839b026acc9a08d29b2ebba1e">+0/-11</a> </td> </tr> <tr> <td><strong>deleteWorkspaceMember.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-7a1add8ebc3e12adf78aa481062a207af509d82170a383d0995eb46a1151e4de">+0/-5</a> </td> </tr> <tr> <td><strong>getWorkspaceMembers.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-584c1f1108054fd783fd7f12a9a746a1c69345d0a6c1d2bfb6f6cf57ce423065">+0/-31</a> </td> </tr> <tr> <td><strong>updateWorkspaceMember.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-ad799e00a2ca484fbe7aa9b1e1f6e0519a989ebf1478506f1c4a516052bd70a6">+0/-8</a> </td> </tr> <tr> <td><strong>deleteWorkspace.gql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-9c3cea5f88a37ea264617a5cc4e992f2b49c3817a31786a238fef9ec4cf6ad95">+0/-5</a> </td> </tr> <tr> <td><strong>insertWorkspace.gql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-ffcac8e9d094021d7ec386ced82f1a36b366e86b39acaf389c570bc21b92be1c">+0/-6</a> </td> </tr> <tr> <td><strong>updateWorkspace.gql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-d8cf2dd0d7e221dc8d5f787a1db06492cb56919725fc2211f47c399ddc1b0f19">+0/-16</a> </td> </tr> <tr> <td><strong>[...slug].tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-56e5a4a71eca9397303199bc4f5595a08ec3ce62a2499f8c079d53c71e9cd8f1">+0/-158</a> </td> </tr> <tr> <td><strong>plansQuery.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-22d9c2d45021b1b76fc284ef1baa41474357ea0ef8c2cdedd06d7bcac3e32629">+0/-16</a> </td> </tr> <tr> <td><strong>prefetchNewAppQuery.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-0a3a444a14b5f5495ef86c90f200a3a672732770e90d4b7206468e2ac265d9fe">+0/-1</a> </td> </tr> <tr> <td><strong>mocks.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-39b16c295568f731fa43aa5a9d642b75fc70f4c0a8e281d701c59da01ec2121e">+0/-124</a> </td> </tr> <tr> <td><strong>testUtils.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-6ebbd73e167641a1706f1b8d30b00569336d10f3c2ab7626d81e639015383e5e">+0/-164</a> </td> </tr> <tr> <td><strong>application.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-380f35753fb3e224792c12d28bc7505ea961ea3f7efd578d1647f76af15afe9f">+0/-2</a> </td> </tr> <tr> <td><strong>graphite.graphql.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-0b7f0e87bb1506853e3ff0227d39085c67994427b818b1b05bb3df5a94539ffb">+5769/-25931</a></td> </tr> <tr> <td><strong>execPromiseWithErrorToast.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-1470fd6a1f6e5557aae2940678106477b11e8a9c8ebf37fc2fa38c0d24a9118e">+0/-62</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-992ff318b1e702f9ad368ce2e529f0ea57cc6711edf892815a0ed246173001b5">+0/-1</a> </td> </tr> <tr> <td><strong>tailwind.config.js</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3241/files#diff-0421515d64f36bf18988a5e62f6b406277d9a63b6991a8b3f4c9e976836449c8">+8/-9</a> </td> </tr> </table></details></td></tr></tr></tbody></table> ___ > <details> <summary> Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about PR-Agent usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>
This commit is contained in:
1
.github/workflows/ci.yaml
vendored
1
.github/workflows/ci.yaml
vendored
@@ -19,7 +19,6 @@ env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
||||
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
|
||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_SLUG: ${{ vars.NHOST_TEST_ORGANIZATION_SLUG }}
|
||||
|
||||
@@ -149,8 +149,11 @@ Next, you need to create a project. Create a `.env.test` file with the following
|
||||
NHOST_TEST_DASHBOARD_URL=<test_dashboard_url>
|
||||
NHOST_TEST_USER_EMAIL=<test_user_email>
|
||||
NHOST_TEST_USER_PASSWORD=<test_user_password>
|
||||
NHOST_TEST_WORKSPACE_NAME=<test_workspace_name>
|
||||
NHOST_TEST_ORGANIZATION_NAME=<test_organization_name>
|
||||
NHOST_TEST_ORGANIZATION_SLUG=<test_organization_slug>
|
||||
NHOST_TEST_PERSONAL_ORG_SLUG=<test_personal_org_slug>
|
||||
NHOST_TEST_PROJECT_NAME=<test_project_name>
|
||||
NHOST_TEST_PROJECT_SUBDOMAIN=<test_project_subdomain>
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET=<test_project_admin_secret>
|
||||
```
|
||||
|
||||
@@ -159,11 +162,14 @@ NHOST_TEST_PROJECT_ADMIN_SECRET=<test_project_admin_secret>
|
||||
- `NHOST_TEST_DASHBOARD_URL`: The URL to run the tests against (e.g: http://localhost:3000 or https://staging.app.nhost.io)
|
||||
- `NHOST_TEST_USER_EMAIL`: Email address of the test user that owns the test project
|
||||
- `NHOST_TEST_USER_PASSWORD`: Password of the test user that owns the test project
|
||||
- `NHOST_TEST_WORKSPACE_NAME`: Name of the workspace that contains the test project
|
||||
- `NHOST_TEST_ORGANIZATION_NAME`: Name of the organization that contains the test project
|
||||
- `NHOST_TEST_ORGANIZATION_SLUG`: Slug of the organization that contains the test project
|
||||
- `NHOST_TEST_PERSONAL_ORG_SLUG`: Slug of the personal organization that contains the test project
|
||||
- `NHOST_TEST_PROJECT_NAME`: Name of the test project
|
||||
- `NHOST_TEST_PROJECT_SUBDOMAIN`: Subdomain of the test project
|
||||
- `NHOST_TEST_PROJECT_ADMIN_SECRET`: Admin secret of the test project
|
||||
|
||||
Make sure to copy the workspace and project information from the [Nhost Dashboard](https://app.nhost.io/).
|
||||
Make sure to copy the organization and project information from the [Nhost Dashboard](https://app.nhost.io/).
|
||||
|
||||
End-to-end tests are written using [Playwright](https://playwright.dev/). To run the tests, run the following command:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL;
|
||||
|
||||
/**
|
||||
* Name of the workspace to test against.
|
||||
* Name of the organization to test against.
|
||||
*/
|
||||
export const TEST_ORGANIZATION_NAME = process.env.NHOST_TEST_ORGANIZATION_NAME;
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface ContactUsProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
|
||||
isTeam?: boolean;
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
export default function FeedbackForm({
|
||||
className,
|
||||
isTeam,
|
||||
isOwner,
|
||||
...props
|
||||
}: ContactUsProps) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'grid max-w-md grid-flow-row gap-2 px-5 py-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Text variant="h3" component="h2">
|
||||
Contact us
|
||||
</Text>
|
||||
|
||||
{isTeam && isOwner && (
|
||||
<Text>
|
||||
If this is a new Team project, or you need to manage members, reach
|
||||
out to us on discord or via email at{' '}
|
||||
<Link
|
||||
href="mailto:support@nhost.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
support@nhost.io
|
||||
</Link>{' '}
|
||||
so we can have your dedicated channel set up.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{isTeam && !isOwner && (
|
||||
<Text>
|
||||
As part of a team plan you can reach out to us on the private channel
|
||||
for this workspace. If you haven't been added to the channel, ask
|
||||
the workspace owner to add you.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text>
|
||||
To report issues with Nhost, please open a GitHub issue in the{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/nhost/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
nhost/nhost
|
||||
</Link>{' '}
|
||||
repository.
|
||||
</Text>
|
||||
<Text>
|
||||
For issues related to the CLI, please visit the{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/cli/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
nhost/cli
|
||||
</Link>{' '}
|
||||
repository.
|
||||
</Text>
|
||||
<Text>
|
||||
If you need assistance or have any questions, feel free to join us on{' '}
|
||||
<Link
|
||||
href="https://discord.com/invite/9V7Qb2U"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
Discord
|
||||
</Link>
|
||||
. Alternatively, if you prefer, you can also open a{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/nhost/discussions/new/choose"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
GitHub discussion
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
<Text>We're here to help, so don't hesitate to reach out!</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './ContactUs';
|
||||
export { default as ContactUs } from './ContactUs';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isTZDate } from '@/components/common/TimePicker/time-picker-utils';
|
||||
import { render, screen, waitFor } from '@/tests/orgs/testUtils';
|
||||
import { render, screen, waitFor } from '@/tests/testUtils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { isBefore, startOfDay } from 'date-fns-v4';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
GetWorkspaceMemberInvitesToManageDocument,
|
||||
useGetWorkspaceMemberInvitesToManageQuery,
|
||||
} from '@/generated/graphql';
|
||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { alpha } from '@mui/system';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function InviteNotification() {
|
||||
const user = useUserData();
|
||||
|
||||
const isPlatform = useIsPlatform();
|
||||
const client = useApolloClient();
|
||||
const router = useRouter();
|
||||
const { submitState, setSubmitState } = useSubmitState();
|
||||
const { submitState: ignoreState, setSubmitState: setIgnoreState } =
|
||||
useSubmitState();
|
||||
|
||||
// @FIX: We probably don't want to poll every ten seconds for possible invites. (We can change later depending on how it works in production.) Maybe just on the workspace page?
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: refetchInvitations,
|
||||
startPolling,
|
||||
} = useGetWorkspaceMemberInvitesToManageQuery({
|
||||
variables: {
|
||||
userId: user?.id,
|
||||
},
|
||||
skip: !isPlatform || !user,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(15000);
|
||||
}, [startPolling]);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// TODO: Throw error instead and wrap this component in an ErrorBoundary
|
||||
// that would handle the error
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data || data.workspaceMemberInvites.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleInviteAccept = async (
|
||||
_event: React.SyntheticEvent<HTMLButtonElement>,
|
||||
invite: (typeof data.workspaceMemberInvites)[number],
|
||||
) => {
|
||||
setSubmitState({
|
||||
error: null,
|
||||
loading: true,
|
||||
});
|
||||
const { res, error: acceptError } = await nhost.functions.call(
|
||||
'/accept-workspace-invite',
|
||||
{
|
||||
workspaceMemberInviteId: invite.id,
|
||||
isAccepted: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
triggerToast('An error occurred when trying to accept the invitation.');
|
||||
|
||||
return setSubmitState({
|
||||
error: new Error(acceptError.message),
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
await client.refetchQueries({
|
||||
include: [
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
GetWorkspaceMemberInvitesToManageDocument,
|
||||
],
|
||||
});
|
||||
await router.push(`/${invite.workspace.slug}`);
|
||||
await refetchInvitations();
|
||||
triggerToast('Workspace invite accepted');
|
||||
return setSubmitState({
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
};
|
||||
|
||||
async function handleIgnoreInvitation(
|
||||
inviteId: (typeof data.workspaceMemberInvites)[number]['id'],
|
||||
) {
|
||||
setIgnoreState({
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const { error: ignoreError } = await nhost.functions.call(
|
||||
'/accept-workspace-invite',
|
||||
{
|
||||
workspaceMemberInviteId: inviteId,
|
||||
isAccepted: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (ignoreError) {
|
||||
triggerToast('An error occurred when trying to ignore the invitation.');
|
||||
|
||||
setIgnoreState({
|
||||
loading: false,
|
||||
error: new Error(ignoreError.message),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// just refetch all data
|
||||
await client.refetchQueries({
|
||||
include: [
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
GetWorkspaceMemberInvitesToManageDocument,
|
||||
],
|
||||
});
|
||||
|
||||
setIgnoreState({
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="absolute right-10 z-50 mt-14 w-workspaceSidebar rounded-lg px-6 py-6 text-left"
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.700',
|
||||
borderWidth: (theme) => (theme.palette.mode === 'dark' ? 1 : 0),
|
||||
borderColor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? theme.palette.grey[400] : 'none',
|
||||
}}
|
||||
>
|
||||
{data?.workspaceMemberInvites?.map(
|
||||
(invite: (typeof data.workspaceMemberInvites)[number]) => (
|
||||
<div key={invite.id} className="grid grid-flow-row gap-4 text-center">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2" sx={{ color: 'common.white' }}>
|
||||
You have been invited to
|
||||
</Text>
|
||||
<Text variant="h3" component="p" sx={{ color: 'common.white' }}>
|
||||
{invite.workspace.name}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
onClick={(e: React.SyntheticEvent<HTMLButtonElement>) =>
|
||||
handleInviteAccept(e, invite)
|
||||
}
|
||||
loading={submitState.loading}
|
||||
>
|
||||
Accept Invite
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
sx={{
|
||||
color: 'common.white',
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) =>
|
||||
alpha(theme.palette.common.white, 0.05),
|
||||
},
|
||||
'&:focus': {
|
||||
backgroundColor: (theme) =>
|
||||
alpha(theme.palette.common.white, 0.1),
|
||||
},
|
||||
}}
|
||||
onClick={() => handleIgnoreInvitation(invite.id)}
|
||||
loading={ignoreState.loading}
|
||||
>
|
||||
Ignore Invite
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InviteNotification } from './InviteNotification';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@/tests/orgs/testUtils';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { guessTimezone } from '@/utils/timezoneUtils';
|
||||
import { TZDate } from '@date-fns/tz';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InviteNotification } from '@/components/common/InviteNotification';
|
||||
import type { BaseLayoutProps } from '@/components/layout/BaseLayout';
|
||||
import { BaseLayout } from '@/components/layout/BaseLayout';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
@@ -146,8 +145,6 @@ export default function AuthenticatedLayout({
|
||||
{children}
|
||||
</div>
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
<InviteNotification />
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -17,8 +17,7 @@ import ProjectSettingsPagesComboBox from './ProjectSettingsPagesComboBox';
|
||||
export default function BreadcrumbNav() {
|
||||
const { query, asPath, route } = useRouter();
|
||||
|
||||
// Extract orgSlug and appSubdomain from router.query
|
||||
const { appSubdomain, workspaceSlug } = query;
|
||||
const { appSubdomain } = query;
|
||||
|
||||
// Extract path segments from the URL
|
||||
const pathSegments = useMemo(() => asPath.split('/'), [asPath]);
|
||||
@@ -27,8 +26,7 @@ export default function BreadcrumbNav() {
|
||||
const projectPage = pathSegments[3] || null;
|
||||
const isSettingsPage = pathSegments[5] === 'settings';
|
||||
|
||||
const showBreadcrumbs =
|
||||
!workspaceSlug && !['/', '/orgs/verify'].includes(route);
|
||||
const showBreadcrumbs = !['/', '/orgs/verify'].includes(route);
|
||||
|
||||
return (
|
||||
<Breadcrumb className="mt-2 flex w-full flex-row flex-nowrap overflow-x-auto lg:mt-0 lg:overflow-visible">
|
||||
|
||||
@@ -25,7 +25,6 @@ type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
plan: string;
|
||||
type: 'organization' | 'workspace';
|
||||
};
|
||||
|
||||
export default function OrgsComboBox() {
|
||||
@@ -51,7 +50,6 @@ export default function OrgsComboBox() {
|
||||
label: selectedItemFromUrl.name,
|
||||
value: selectedItemFromUrl.slug,
|
||||
plan: selectedOrgFromUrl ? selectedOrgFromUrl.plan.name : 'Legacy',
|
||||
type: selectedOrgFromUrl ? 'organization' : 'workspace',
|
||||
});
|
||||
}
|
||||
}, [selectedOrgFromUrl]);
|
||||
@@ -60,7 +58,6 @@ export default function OrgsComboBox() {
|
||||
label: org.name,
|
||||
value: org.slug,
|
||||
plan: org.plan.name,
|
||||
type: 'organization',
|
||||
}));
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -100,7 +97,7 @@ export default function OrgsComboBox() {
|
||||
{renderBadge(selectedItem.plan)}
|
||||
</div>
|
||||
) : (
|
||||
'Select organization / workspace'
|
||||
'Select organization'
|
||||
)}
|
||||
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
@@ -124,11 +121,7 @@ export default function OrgsComboBox() {
|
||||
// persist last slug in local storage
|
||||
setLastSlug(option.value);
|
||||
|
||||
if (option.type === 'organization') {
|
||||
push(`/orgs/${option.value}/projects`);
|
||||
} else {
|
||||
push(`/${option.value}`);
|
||||
}
|
||||
push(`/orgs/${option.value}/projects`);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTreeNavState } from './TreeNavStateContext';
|
||||
export default function PinnedMainNav() {
|
||||
const {
|
||||
asPath,
|
||||
query: { workspaceSlug, orgSlug },
|
||||
query: { orgSlug },
|
||||
} = useRouter();
|
||||
|
||||
const scrollContainerRef = useRef();
|
||||
@@ -44,7 +44,7 @@ export default function PinnedMainNav() {
|
||||
};
|
||||
}, [asPath]);
|
||||
|
||||
if (!orgSlug && !workspaceSlug) {
|
||||
if (!orgSlug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +1,26 @@
|
||||
import { ContactUs } from '@/components/common/ContactUs';
|
||||
import { NavLink } from '@/components/common/NavLink';
|
||||
import { ThemeSwitcher } from '@/components/common/ThemeSwitcher';
|
||||
import { Nav } from '@/components/presentational/Nav';
|
||||
import type { ButtonProps } from '@/components/ui/v2/Button';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Drawer } from '@/components/ui/v2/Drawer';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { MenuIcon } from '@/components/ui/v2/icons/MenuIcon';
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useNavigationVisible } from '@/features/orgs/projects/common/hooks/useNavigationVisible';
|
||||
import { useProjectRoutes } from '@/features/orgs/projects/common/hooks/useProjectRoutes';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useSignOut } from '@nhost/nextjs';
|
||||
import getConfig from 'next/config';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactNode } from 'react';
|
||||
import { cloneElement, Fragment, isValidElement, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface MobileNavProps extends ButtonProps {}
|
||||
|
||||
interface MobileNavLinkProps extends ListItemButtonProps {
|
||||
/**
|
||||
* Link to navigate to.
|
||||
*/
|
||||
href: string;
|
||||
/**
|
||||
* Determines whether or not the link should be active if it's href exactly
|
||||
* matches the current route.
|
||||
*/
|
||||
exact?: boolean;
|
||||
/**
|
||||
* Icon to display next to the text.
|
||||
*/
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
function MobileNavLink({
|
||||
className,
|
||||
exact = true,
|
||||
href,
|
||||
icon,
|
||||
...props
|
||||
}: MobileNavLinkProps) {
|
||||
const router = useRouter();
|
||||
const baseUrl = `/${router.query.workspaceSlug}/${router.query.appSlug}`;
|
||||
const finalUrl = href && href !== '/' ? `${baseUrl}${href}` : baseUrl;
|
||||
|
||||
const active = exact
|
||||
? router.asPath === finalUrl
|
||||
: router.asPath.startsWith(finalUrl);
|
||||
|
||||
return (
|
||||
<ListItem.Root
|
||||
className={twMerge('grid grid-flow-row gap-2 py-2', className)}
|
||||
>
|
||||
<ListItem.Button
|
||||
className="w-full"
|
||||
component={NavLink}
|
||||
href={finalUrl}
|
||||
selected={active}
|
||||
{...props}
|
||||
>
|
||||
<ListItem.Icon>
|
||||
{isValidElement(icon)
|
||||
? cloneElement(icon, { ...icon.props, className: 'w-4.5 h-4.5' })
|
||||
: null}
|
||||
</ListItem.Icon>
|
||||
|
||||
<ListItem.Text>{props.children}</ListItem.Text>
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
);
|
||||
}
|
||||
export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { allRoutes } = useProjectRoutes();
|
||||
const shouldDisplayNav = useNavigationVisible();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const { signOut } = useSignOut();
|
||||
const apolloClient = useApolloClient();
|
||||
@@ -113,76 +51,23 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
className: 'w-full px-4 pt-18 pb-12 grid grid-flow-row gap-6',
|
||||
}}
|
||||
>
|
||||
{shouldDisplayNav && (
|
||||
<section>
|
||||
<Nav
|
||||
flow="row"
|
||||
className="w-full"
|
||||
aria-label="Mobile navigation"
|
||||
listProps={{ className: 'gap-2' }}
|
||||
>
|
||||
<List>
|
||||
{allRoutes.map(
|
||||
({ relativePath, label, icon, exact, disabled }, index) => (
|
||||
<Fragment key={relativePath}>
|
||||
<MobileNavLink
|
||||
href={relativePath}
|
||||
className="w-full"
|
||||
exact={exact}
|
||||
icon={icon}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
</MobileNavLink>
|
||||
|
||||
{index < allRoutes.length - 1 && (
|
||||
<Divider component="li" />
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
)}
|
||||
</List>
|
||||
</Nav>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section
|
||||
className={twMerge(
|
||||
'grid grid-flow-row gap-3',
|
||||
!shouldDisplayNav && 'mt-2',
|
||||
)}
|
||||
>
|
||||
<section className="mt-2 grid grid-flow-row gap-3">
|
||||
<Text variant="h2" className="text-xl font-semibold">
|
||||
Resources
|
||||
</Text>
|
||||
|
||||
<List className="grid grid-flow-row gap-2">
|
||||
{isPlatform && (
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
className="justify-initial w-full"
|
||||
hideChevron
|
||||
asChild
|
||||
<ListItem.Root>
|
||||
<ListItem.Button
|
||||
component={NavLink}
|
||||
href="/support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ListItem.Root>
|
||||
<ListItem.Button
|
||||
component="span"
|
||||
className="w-full"
|
||||
role={undefined}
|
||||
>
|
||||
<ListItem.Text>Contact us</ListItem.Text>
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<ContactUs className="max-w-md" />
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
<ListItem.Text>Contact us</ListItem.Text>
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
)}
|
||||
|
||||
<Divider component="li" />
|
||||
|
||||
@@ -2,84 +2,41 @@ import { render, screen } from '@/tests/testUtils';
|
||||
import { test } from 'vitest';
|
||||
import ErrorToast from './ErrorToast';
|
||||
|
||||
const oneMemberByWorkspaceError = {
|
||||
const runUpdateError = {
|
||||
name: 'ApolloError',
|
||||
graphQLErrors: [
|
||||
{
|
||||
message: 'database query error',
|
||||
extensions: {
|
||||
path: '$.selectionSet.insertApp.args.object',
|
||||
code: 'unexpected',
|
||||
internal: {
|
||||
arguments: [],
|
||||
error: {
|
||||
description: null,
|
||||
exec_status: 'FatalError',
|
||||
hint: null,
|
||||
message:
|
||||
'Only one workspace member is allowed for individual plans',
|
||||
status_code: 'P0001',
|
||||
},
|
||||
prepared: false,
|
||||
statement: '.....',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
protocolErrors: [],
|
||||
clientErrors: [],
|
||||
networkError: null,
|
||||
message: 'database query error',
|
||||
};
|
||||
|
||||
const changeNodeInvalidVersionError = {
|
||||
name: 'ApolloError',
|
||||
graphQLErrors: [
|
||||
{
|
||||
message:
|
||||
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
|
||||
path: ['replaceConfigRawJSON'],
|
||||
message: 'The port value "302300" is out of range',
|
||||
path: ['replaceRunServiceConfig', 'config', 'ports', 0, 'port'],
|
||||
},
|
||||
],
|
||||
protocolErrors: [],
|
||||
clientErrors: [],
|
||||
networkError: null,
|
||||
message:
|
||||
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
|
||||
'problem trying to parse string: strconv.ParseInt: parsing "302300": value out of range',
|
||||
cause: {
|
||||
message:
|
||||
'problem trying to parse string: strconv.ParseInt: parsing "302300": value out of range',
|
||||
path: ['replaceRunServiceConfig', 'config', 'ports', 0, 'port'],
|
||||
},
|
||||
};
|
||||
|
||||
test('should render the error message when creating a project with an individual plan in a workspace with multiple users', () => {
|
||||
const errorMessage =
|
||||
'An error occurred while creating the project. Please try again.';
|
||||
test('should render the available Apollo error message but not the fallback message', () => {
|
||||
const fallbackErrorMessage =
|
||||
'An error occurred while updating the service. Please try again.';
|
||||
render(
|
||||
<ErrorToast
|
||||
isVisible
|
||||
errorMessage={errorMessage}
|
||||
error={oneMemberByWorkspaceError}
|
||||
errorMessage={fallbackErrorMessage}
|
||||
error={runUpdateError}
|
||||
close={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(fallbackErrorMessage)).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Only one workspace member is allowed for individual plans/i,
|
||||
),
|
||||
screen.getByText(/The port value "302300" is out of range/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the error message when changing the node version to an invalid value in configuration editor', () => {
|
||||
const errorMessage =
|
||||
'An error occurred while saving configuration. Please try again.';
|
||||
render(
|
||||
<ErrorToast
|
||||
isVisible
|
||||
errorMessage={errorMessage}
|
||||
error={changeNodeInvalidVersionError}
|
||||
close={() => {}}
|
||||
/>,
|
||||
);
|
||||
const regex =
|
||||
/failed to resolve config: failed to validate config: config is not valid: #Config\.functions\.node\.version: 2 errors in empty disjunction: \(and 2 more errors\)/i;
|
||||
|
||||
expect(screen.getByText(regex)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -4,11 +4,8 @@ import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
useDeleteUserAccountMutation,
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useDeleteUserAccountMutation } from '@/utils/__generated__/graphql';
|
||||
import { useSignOut, useUserData } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
@@ -24,15 +21,6 @@ function ConfirmDeleteAccountModal({
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
|
||||
const user = useUserData();
|
||||
|
||||
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const userHasProjects =
|
||||
!loading && data?.workspaces.some((workspace) => workspace.projects.length);
|
||||
|
||||
const userData = useUserData();
|
||||
|
||||
const [deleteUserAccount] = useDeleteUserAccountMutation({
|
||||
@@ -64,17 +52,6 @@ function ConfirmDeleteAccountModal({
|
||||
Delete Account?
|
||||
</Text>
|
||||
|
||||
{userHasProjects && (
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="font-bold"
|
||||
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
|
||||
>
|
||||
You still have active projects. Please delete your projects before
|
||||
proceeding with the account deletion.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box className="my-4">
|
||||
<Checkbox
|
||||
id="accept-1"
|
||||
@@ -90,7 +67,6 @@ function ConfirmDeleteAccountModal({
|
||||
<Button
|
||||
color="error"
|
||||
onClick={onClickConfirm}
|
||||
disabled={userHasProjects}
|
||||
loading={loadingRemove}
|
||||
>
|
||||
Delete
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useUpdateUserDisplayNameMutation } from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useNhostClient, useUserData } from '@nhost/nextjs';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
@@ -15,12 +15,12 @@ import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { CreatePATForm } from '@/features/account/settings/components/CreatePATForm';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetPersonalAccessTokensDocument,
|
||||
useDeletePersonalAccessTokenMutation,
|
||||
useGetPersonalAccessTokensQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { Fragment } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useChangePassword } from '@nhost/nextjs';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
@@ -26,13 +26,13 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
|
||||
import { StripeEmbeddedForm } from '@/features/orgs/components/StripeEmbeddedForm';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { planDescriptions } from '@/features/orgs/projects/common/utils/planDescriptions';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
useCreateOrganizationRequestMutation,
|
||||
usePrefetchNewAppQuery,
|
||||
type PrefetchNewAppPlansFragment,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { DialogDescription } from '@radix-ui/react-dialog';
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@/tests/orgs/testUtils';
|
||||
} from '@/tests/testUtils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { mockMatchMediaValue } from '@/tests/mocks';
|
||||
import { mockMatchMediaValue, mockSession } from '@/tests/mocks';
|
||||
import { getOrganizations } from '@/tests/msw/mocks/graphql/getOrganizationQuery';
|
||||
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { mockSession } from '@/tests/orgs/mocks';
|
||||
import { queryClient, render, waitFor } from '@/tests/orgs/testUtils';
|
||||
import { queryClient, render, waitFor } from '@/tests/testUtils';
|
||||
import { CheckoutStatus } from '@/utils/__generated__/graphql';
|
||||
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { test } from 'vitest';
|
||||
import ErrorToast from './ErrorToast';
|
||||
|
||||
const oneMemberByWorkspaceError = {
|
||||
name: 'ApolloError',
|
||||
graphQLErrors: [
|
||||
{
|
||||
message: 'database query error',
|
||||
extensions: {
|
||||
path: '$.selectionSet.insertApp.args.object',
|
||||
code: 'unexpected',
|
||||
internal: {
|
||||
arguments: [],
|
||||
error: {
|
||||
description: null,
|
||||
exec_status: 'FatalError',
|
||||
hint: null,
|
||||
message:
|
||||
'Only one workspace member is allowed for individual plans',
|
||||
status_code: 'P0001',
|
||||
},
|
||||
prepared: false,
|
||||
statement: '.....',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
protocolErrors: [],
|
||||
clientErrors: [],
|
||||
networkError: null,
|
||||
message: 'database query error',
|
||||
};
|
||||
|
||||
const changeNodeInvalidVersionError = {
|
||||
name: 'ApolloError',
|
||||
graphQLErrors: [
|
||||
{
|
||||
message:
|
||||
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
|
||||
path: ['replaceConfigRawJSON'],
|
||||
},
|
||||
],
|
||||
protocolErrors: [],
|
||||
clientErrors: [],
|
||||
networkError: null,
|
||||
message:
|
||||
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
|
||||
};
|
||||
|
||||
test('should render the error message when creating a project with an individual plan in a workspace with multiple users', () => {
|
||||
const errorMessage =
|
||||
'An error occurred while creating the project. Please try again.';
|
||||
render(
|
||||
<ErrorToast
|
||||
isVisible
|
||||
errorMessage={errorMessage}
|
||||
error={oneMemberByWorkspaceError}
|
||||
close={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Only one workspace member is allowed for individual plans/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the error message when changing the node version to an invalid value in configuration editor', () => {
|
||||
const errorMessage =
|
||||
'An error occurred while saving configuration. Please try again.';
|
||||
render(
|
||||
<ErrorToast
|
||||
isVisible
|
||||
errorMessage={errorMessage}
|
||||
error={changeNodeInvalidVersionError}
|
||||
close={() => {}}
|
||||
/>,
|
||||
);
|
||||
const regex =
|
||||
/failed to resolve config: failed to validate config: config is not valid: #Config\.functions\.node\.version: 2 errors in empty disjunction: \(and 2 more errors\)/i;
|
||||
|
||||
expect(screen.getByText(regex)).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,170 +0,0 @@
|
||||
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
|
||||
import { ChevronUpIcon } from '@/components/ui/v2/icons/ChevronUpIcon';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { getToastBackgroundColor } from '@/utils/constants/settings';
|
||||
import { copy } from '@/utils/copy';
|
||||
import type { ApolloError } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ErrorDetails {
|
||||
info: {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
url?: string;
|
||||
};
|
||||
error: any;
|
||||
}
|
||||
|
||||
const getInternalErrorMessage = (
|
||||
error: Error | ApolloError | undefined,
|
||||
): string | null => {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error.name === 'ApolloError') {
|
||||
// @ts-ignore
|
||||
const graphqlError = error.graphQLErrors?.[0];
|
||||
const graphqlExtensionsError = graphqlError?.extensions?.internal
|
||||
?.error as { message: string };
|
||||
return graphqlExtensionsError?.message || graphqlError.message || null;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const errorToObject = (error: ApolloError | Error) => {
|
||||
if (error.name === 'ApolloError') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export default function ErrorToast({
|
||||
isVisible,
|
||||
errorMessage,
|
||||
error,
|
||||
close,
|
||||
}: {
|
||||
isVisible: boolean;
|
||||
errorMessage: string;
|
||||
error: ApolloError | Error;
|
||||
close: () => void;
|
||||
}) {
|
||||
const userData = useUserData();
|
||||
const { asPath } = useRouter();
|
||||
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const { project } = useProject();
|
||||
|
||||
const errorDetails: ErrorDetails = {
|
||||
info: {
|
||||
projectId: project?.id,
|
||||
userId: userData?.id || 'local',
|
||||
url: asPath,
|
||||
},
|
||||
error: errorToObject(error),
|
||||
};
|
||||
|
||||
const msg = getInternalErrorMessage(error) || errorMessage;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
style={{
|
||||
backgroundColor: getToastBackgroundColor(),
|
||||
}}
|
||||
className="flex w-full max-w-xl flex-col space-y-4 rounded-lg p-4 text-white"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
y: 100,
|
||||
}}
|
||||
transition={{
|
||||
bounce: 0.1,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4">
|
||||
<button
|
||||
className="flex-shrink-0"
|
||||
onClick={close}
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
>
|
||||
<XIcon className="h-4 w-4 text-white" />
|
||||
</button>
|
||||
<span className="flex-grow overflow-hidden break-words">
|
||||
{msg ?? 'An unkown error has occured, please try again later!'}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInfo(!showInfo)}
|
||||
className="flex flex-shrink-0 flex-row items-center justify-center space-x-2 text-white"
|
||||
aria-label="Show error details"
|
||||
>
|
||||
<span>Info</span>
|
||||
{showInfo ? (
|
||||
<ChevronUpIcon className="h-3 w-3 text-white" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-3 w-3 text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showInfo && (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="relative flex flex-col">
|
||||
<div className="relative flex max-h-[400px] w-full max-w-xl flex-row justify-between overflow-x-auto rounded-lg bg-black p-4">
|
||||
<pre>{JSON.stringify(errorDetails, null, 2)}</pre>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Copy error details"
|
||||
className="absolute right-2 top-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
copy(
|
||||
JSON.stringify(errorDetails, null, 2),
|
||||
'Error details',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as ErrorToast } from './ErrorToast';
|
||||
@@ -1,9 +1,9 @@
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
CheckoutStatus,
|
||||
type PostOrganizationRequestMutation,
|
||||
usePostOrganizationRequestMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { useAuthenticationStatus } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useRestoreApplicationDatabasePiTrMutation } from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
|
||||
function useRestoreApplicationDatabasePiTR() {
|
||||
const [restoreApplicationDatabaseMutation, { loading }] =
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { type Assistant } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/assistants';
|
||||
import { useDeleteAssistantMutation } from '@/utils/__generated__/graphite.graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useUpdateConfigMutation } from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@/tests/msw/mocks/graphql/getPostgresSettings';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen } from '@/tests/orgs/testUtils';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
import BackupsContent from './BackupsContent';
|
||||
|
||||
@@ -4,12 +4,7 @@ import {
|
||||
mockMatchMediaValue,
|
||||
} from '@/tests/mocks';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import {
|
||||
mockPointerEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@/tests/orgs/testUtils';
|
||||
import { mockPointerEvent, render, screen, waitFor } from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@/tests/msw/mocks/graphql/getPostgresSettings';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen } from '@/tests/orgs/testUtils';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
import PointInTimeTabsContent from './PointInTimeTabsContent';
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '@/tests/msw/mocks/graphql/getPostgresSettings';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen } from '@/tests/orgs/testUtils';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
import ScheduledBackupTabContent from './ScheduledBackupTabContent';
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
mockMatchMediaValue,
|
||||
} from '@/tests/mocks';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen, waitFor } from '@/tests/orgs/testUtils';
|
||||
import { render, screen, waitFor } from '@/tests/testUtils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { test, vi } from 'vitest';
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { ContactUs } from '@/components/common/ContactUs';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useInterval } from '@/hooks/useInterval';
|
||||
@@ -97,22 +95,14 @@ export default function AppLoader({
|
||||
<ActivityIndicator className="mx-auto" />
|
||||
|
||||
{timeElapsed > 180 && (
|
||||
<Dropdown.Root className="mx-auto flex flex-col">
|
||||
<Dropdown.Trigger
|
||||
className="mx-auto flex font-medium"
|
||||
hideChevron
|
||||
asChild
|
||||
>
|
||||
<Button variant="borderless">Contact Support</Button>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<ContactUs />
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
<Link
|
||||
className="font-semibold underline underline-offset-2"
|
||||
href="/support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Contact Support
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import { ContactUs } from '@/components/common/ContactUs';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { Modal } from '@/components/ui/v1/Modal';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ApplicationInfo } from '@/features/orgs/projects/common/components/ApplicationInfo';
|
||||
import { ApplicationLive } from '@/features/orgs/projects/common/components/ApplicationLive';
|
||||
import { RemoveApplicationModal } from '@/features/orgs/projects/common/components/RemoveApplicationModal';
|
||||
import { StagingMetadata } from '@/features/orgs/projects/common/components/StagingMetadata';
|
||||
import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import { getPreviousApplicationState } from '@/features/orgs/projects/common/utils/getPreviousApplicationState';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import type { ApplicationState } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import {
|
||||
useDeleteApplicationMutation,
|
||||
useGetApplicationStateQuery,
|
||||
useInsertApplicationMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ApplicationErrored() {
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const { org } = useCurrentOrg();
|
||||
|
||||
const [changingApplicationStateLoading, setChangingApplicationStateLoading] =
|
||||
useState(false);
|
||||
|
||||
const [deleteApplication] = useDeleteApplicationMutation();
|
||||
const [updateApplication] = useUpdateApplicationMutation();
|
||||
|
||||
// If we reach this component we already have an application state in the ERRORED
|
||||
// state, but we want to query again to double-check that we have the latest state
|
||||
// of the application. @GC.
|
||||
const { data, loading, error } = useGetApplicationStateQuery({
|
||||
variables: { appId: project?.id },
|
||||
skip: !project,
|
||||
});
|
||||
|
||||
const previousState = data?.app?.appStates
|
||||
? getPreviousApplicationState(data.app.appStates)
|
||||
: null;
|
||||
|
||||
const [showRecreateModal, setShowRecreateModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [insertApp] = useInsertApplicationMutation();
|
||||
const currentDate = new Date().getTime();
|
||||
const user = useUserData();
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
|
||||
const appCreatedAt = new Date(project?.createdAt).getTime();
|
||||
|
||||
const FIVE_DAYS_IN_MILLISECONDS = 60 * 24 * 60 * 5 * 1000;
|
||||
const HALF_DAY_IN_MILLISECONDS = 60 * 12 * 60 * 1000;
|
||||
|
||||
async function recreateApplication() {
|
||||
try {
|
||||
await deleteApplication({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`${project?.name} deleted`);
|
||||
} catch (e) {
|
||||
triggerToast(`Error deleting ${project?.name}`);
|
||||
discordAnnounce(`Error deleting app: ${project?.name} (${user.email})`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await insertApp({
|
||||
variables: {
|
||||
app: {
|
||||
name: project.name,
|
||||
slug: project.slug,
|
||||
organizationID: org.id,
|
||||
regionId: project.region.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
discordAnnounce(`Recreating: ${project?.name} (${user.email})`);
|
||||
triggerToast(`Recreating ${project?.name} `);
|
||||
await refetchProject();
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to recreate: ${project?.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTriggerUnpausing() {
|
||||
setChangingApplicationStateLoading(true);
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
app: {
|
||||
desiredState: ApplicationStatus.Live,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`${project?.name} set to awake.`);
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to awake ${project?.name}`);
|
||||
discordAnnounce(
|
||||
`Error trying to awake app: ${project?.name} (${user.email})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTryAgain() {
|
||||
setChangingApplicationStateLoading(true);
|
||||
|
||||
// If the application is older than seven days, and has fallen into failed setup, we want
|
||||
// to make sure the user knows that attempting to recreate the application will remove
|
||||
// all of its data.
|
||||
if (currentDate - appCreatedAt > HALF_DAY_IN_MILLISECONDS) {
|
||||
setChangingApplicationStateLoading(false);
|
||||
setShowRecreateModal(true);
|
||||
// Since the modal for removing an application already handles the deleting of the app,
|
||||
// we don't want to delete it on the recreation part but just insert the same app with the same
|
||||
// data.
|
||||
return;
|
||||
}
|
||||
await recreateApplication();
|
||||
}
|
||||
|
||||
if (loading || previousState === null) {
|
||||
return (
|
||||
<Container className="mx-auto mt-12 max-w-sm text-center">
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading application state..."
|
||||
className="mx-auto inline-grid"
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
previousState === ApplicationStatus.Updating ||
|
||||
previousState === ApplicationStatus.Empty
|
||||
) {
|
||||
return (
|
||||
<ApplicationLive errorMessage="Error deploying the project most likely due to invalid configuration. Please review your project's configuration and logs for more information." />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={showRecreateModal}
|
||||
close={() => setShowRecreateModal(false)}
|
||||
>
|
||||
<RemoveApplicationModal
|
||||
// We accept a handler in this model to override the function of then modal,
|
||||
// which instead of deleting just an application, it deletes and recreates.
|
||||
handler={recreateApplication}
|
||||
close={() => setShowRecreateModal(false)}
|
||||
title={`Recreate project ${project.name}?`}
|
||||
description={`The project ${project?.name} will be removed and then re-created. All data will be lost and there will be no way to
|
||||
recover the app once it has been deleted.`}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
showModal={showDeleteModal}
|
||||
close={() => setShowDeleteModal(false)}
|
||||
>
|
||||
<RemoveApplicationModal
|
||||
close={() => setShowDeleteModal(false)}
|
||||
title={`Remove project ${project.name}?`}
|
||||
description={`The project ${project?.name} will be removed. All data will be lost and there will be no way to
|
||||
recover the app once it has been deleted.`}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Container className="mx-auto mt-12 max-w-sm text-center">
|
||||
<div className="mx-auto flex w-centImage flex-col text-center">
|
||||
<Image
|
||||
src="/assets/ProvisioningFailed.svg"
|
||||
alt="Danger sign"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
</div>
|
||||
<Text variant="h3" component="h1" className="mt-4">
|
||||
Project Setup Failed while {getApplicationStatusString(previousState)}
|
||||
</Text>
|
||||
|
||||
<Text className="mt-1 font-normal">
|
||||
Something on our end went wrong and we could not finish setup. If this
|
||||
keeps happening, contact support.
|
||||
</Text>
|
||||
|
||||
<div className="mx-auto mt-6 grid grid-flow-row gap-2">
|
||||
{(previousState === ApplicationStatus.Provisioning ||
|
||||
previousState === ApplicationStatus.Unpausing) &&
|
||||
currentDate - appCreatedAt < FIVE_DAYS_IN_MILLISECONDS ? (
|
||||
<Button
|
||||
className="mx-auto w-full max-w-[240px]"
|
||||
loading={changingApplicationStateLoading}
|
||||
onClick={() => {
|
||||
const previousApplicationState = getPreviousApplicationState(
|
||||
data.app.appStates as ApplicationState[],
|
||||
);
|
||||
|
||||
switch (previousApplicationState) {
|
||||
case ApplicationStatus.Provisioning:
|
||||
handleTryAgain();
|
||||
break;
|
||||
case ApplicationStatus.Unpausing:
|
||||
handleTriggerUnpausing();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized previous project state.`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
className="w-full max-w-[240px]"
|
||||
hideChevron
|
||||
asChild
|
||||
>
|
||||
<Button variant="borderless">Contact Support</Button>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<ContactUs />
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="error"
|
||||
className="mx-auto w-full max-w-[240px]"
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<StagingMetadata>
|
||||
<ApplicationInfo />
|
||||
</StagingMetadata>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as ApplicationErrored } from './ApplicationErrored';
|
||||
@@ -4,10 +4,14 @@ import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useBillingDeleteAppMutation } from '@/generated/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetOrganizationsDocument,
|
||||
useBillingDeleteAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
@@ -15,8 +19,13 @@ export default function ApplicationInfo() {
|
||||
const router = useRouter();
|
||||
const { project } = useProject();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
const userData = useUserData();
|
||||
|
||||
const [deleteApplication] = useBillingDeleteAppMutation();
|
||||
const [deleteApplication] = useBillingDeleteAppMutation({
|
||||
refetchQueries: [
|
||||
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
|
||||
],
|
||||
});
|
||||
|
||||
async function handleClickRemove() {
|
||||
await execPromiseWithErrorToast(
|
||||
|
||||
@@ -9,10 +9,6 @@ import { StagingMetadata } from '@/features/orgs/projects/common/components/Stag
|
||||
import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useUnpauseApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ApplicationPaused() {
|
||||
@@ -23,9 +19,6 @@ export default function ApplicationPaused() {
|
||||
useState(false);
|
||||
|
||||
const [showDeletingModal, setShowDeletingModal] = useState(false);
|
||||
useUnpauseApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -8,7 +8,11 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useUnpauseApplicationMutation } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
GetOrganizationsDocument,
|
||||
useUnpauseApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -25,12 +29,19 @@ export default function ApplicationPausedBanner({
|
||||
const { state } = useAppState();
|
||||
const { freeAndLiveProjectsNumberExceeded } = useAppPausedReason();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
const userData = useUserData();
|
||||
|
||||
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
||||
useUnpauseApplicationMutation({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GetOrganizationsDocument,
|
||||
variables: { userId: userData.id },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const handleTriggerUnpausing = useCallback(async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ContactUs } from '@/components/common/ContactUs';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { Modal } from '@/components/ui/v1/Modal';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ApplicationInfo } from '@/features/orgs/projects/common/components/ApplicationInfo';
|
||||
import { RemoveApplicationModal } from '@/features/orgs/projects/common/components/RemoveApplicationModal';
|
||||
@@ -13,10 +13,14 @@ import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ApplicationUnknown() {
|
||||
const { project } = useProject();
|
||||
const { project, loading } = useProject();
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
|
||||
if (!project || loading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -47,28 +51,20 @@ export default function ApplicationUnknown() {
|
||||
|
||||
<Text className="mt-1 font-normal">
|
||||
Something on our end went wrong and we could not finish setup. If
|
||||
this keeps happening, contact support.
|
||||
this keeps happening,{' '}
|
||||
<Link
|
||||
className="font-semibold underline underline-offset-2"
|
||||
href="/support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
contact support
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto grid grid-flow-row gap-2">
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
hideChevron
|
||||
asChild
|
||||
className="w-full max-w-[240px]"
|
||||
>
|
||||
<Button variant="borderless">Contact Support</Button>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<ContactUs />
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
variant="borderless"
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { type RunService } from '@/features/orgs/projects/common/hooks/useRunServices';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useDeleteRunServiceConfigMutation,
|
||||
useDeleteRunServiceMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import { useBillingDeleteAppMutation } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
GetOrganizationsDocument,
|
||||
useBillingDeleteAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import router from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -45,8 +49,13 @@ export default function RemoveApplicationModal({
|
||||
}: RemoveApplicationModalProps) {
|
||||
const { project } = useProject();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
const userData = useUserData();
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
const [deleteApplication] = useBillingDeleteAppMutation();
|
||||
const [deleteApplication] = useBillingDeleteAppMutation({
|
||||
refetchQueries: [
|
||||
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
|
||||
],
|
||||
});
|
||||
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [remove2, setRemove2] = useState(false);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useGetApplicationStateQuery,
|
||||
useGetOrganizationsLazyQuery,
|
||||
} from '@/generated/graphql';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
type ApplicationStateMetadata = {
|
||||
@@ -24,20 +25,23 @@ export default function useCheckProvisioning() {
|
||||
const [currentApplicationState, setCurrentApplicationState] =
|
||||
useState<ApplicationStateMetadata>({ state: ApplicationStatus.Empty });
|
||||
const isPlatform = useIsPlatform();
|
||||
const userData = useUserData();
|
||||
|
||||
const { data, startPolling, stopPolling, client } =
|
||||
useGetApplicationStateQuery({
|
||||
variables: { appId: project?.id },
|
||||
skip: !isPlatform || !project?.id,
|
||||
});
|
||||
const [getOrgs] = useGetOrganizationsLazyQuery();
|
||||
|
||||
const { data, startPolling, stopPolling } = useGetApplicationStateQuery({
|
||||
variables: { appId: project?.id },
|
||||
skip: !isPlatform || !project?.id,
|
||||
});
|
||||
|
||||
async function updateOwnCache() {
|
||||
await client.refetchQueries({
|
||||
include: [GetAllWorkspacesAndProjectsDocument],
|
||||
});
|
||||
await getOrgs({ variables: { userId: userData.id } });
|
||||
}
|
||||
|
||||
const memoizedUpdateCache = useCallback(updateOwnCache, [client]);
|
||||
const memoizedUpdateCache = useCallback(updateOwnCache, [
|
||||
userData?.id,
|
||||
getOrgs,
|
||||
]);
|
||||
|
||||
const currentApplicationId = project?.id;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Organization_Members_Role_Enum } from '@/utils/__generated__/graphql';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
|
||||
/**
|
||||
* Returns true if the current user is the owner of the current workspace.
|
||||
* Returns true if the current user is the owner of the current organization.
|
||||
*
|
||||
* @returns True if the current user is the owner of the current workspace.
|
||||
* @returns True if the current user is the owner of the current organization.
|
||||
*/
|
||||
export default function useIsCurrentUserOwner() {
|
||||
const { org, loading: loadingOrg } = useCurrentOrg();
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default as useNavigationVisible } from './useNavigationVisible';
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { usePreviousApplicationStates } from '@/features/orgs/projects/common/hooks/usePreviousApplicationStates';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
/**
|
||||
* This hook will check the route, the environment, and the history of the states of the app to correctly render the navigation header.
|
||||
*/
|
||||
export default function useNavigationVisible() {
|
||||
const { project } = useProject();
|
||||
const { state } = useAppState();
|
||||
const previousApplicationState = usePreviousApplicationStates();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
if (router.route === '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (router.route === '/new' || router.route === '/404') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (router.query.workspaceSlug && !router.query.appSlug) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (project.appStates?.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (project.desiredState === ApplicationStatus.Migrating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
state === ApplicationStatus.Migrating &&
|
||||
project.desiredState === ApplicationStatus.Live
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
state === ApplicationStatus.Live ||
|
||||
state === ApplicationStatus.Updating
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
state === ApplicationStatus.Errored &&
|
||||
previousApplicationState === ApplicationStatus.Updating
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Redirects to 404 page if either currentWorkspace/currentProject resolves to undefined
|
||||
* Redirects to 404 page if currentProject resolves to undefined
|
||||
* or if the current pathname is not a valid organization/project.
|
||||
* Not applicable if running dashboard with local Nhost backend.
|
||||
*/
|
||||
@@ -14,7 +14,6 @@ export default function useNotFoundRedirect() {
|
||||
const {
|
||||
query: {
|
||||
orgSlug: urlOrgSlug,
|
||||
workspaceSlug: urlWorkspaceSlug,
|
||||
appSubdomain: urlAppSubdomain,
|
||||
updating,
|
||||
appSlug: urlAppSlug,
|
||||
@@ -68,7 +67,6 @@ export default function useNotFoundRedirect() {
|
||||
orgLoading,
|
||||
currentOrgSlug,
|
||||
projectSubdomain,
|
||||
urlWorkspaceSlug,
|
||||
urlOrgSlug,
|
||||
isPlatform,
|
||||
]);
|
||||
|
||||
@@ -5,10 +5,11 @@ import type {
|
||||
GetApplicationStateQueryVariables,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useGetApplicationStateQuery,
|
||||
useGetOrganizationsLazyQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import type { QueryHookOptions } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export interface UseProjectRedirectWhenReadyOptions
|
||||
@@ -21,7 +22,10 @@ export default function useProjectRedirectWhenReady(
|
||||
options: UseProjectRedirectWhenReadyOptions = {},
|
||||
) {
|
||||
const { project } = useProject();
|
||||
const { data, client, startPolling, ...rest } = useGetApplicationStateQuery({
|
||||
const userData = useUserData();
|
||||
const [getOrgs] = useGetOrganizationsLazyQuery();
|
||||
|
||||
const { data, startPolling, ...rest } = useGetApplicationStateQuery({
|
||||
...options,
|
||||
variables: { ...options.variables, appId: project?.id },
|
||||
skip: !project?.id,
|
||||
@@ -33,11 +37,8 @@ export default function useProjectRedirectWhenReady(
|
||||
|
||||
useEffect(() => {
|
||||
async function updateOwnCache() {
|
||||
await client.refetchQueries({
|
||||
include: [GetAllWorkspacesAndProjectsDocument],
|
||||
});
|
||||
await getOrgs({ variables: { userId: userData.id } });
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
@@ -55,12 +56,9 @@ export default function useProjectRedirectWhenReady(
|
||||
lastState.stateId === ApplicationStatus.Live ||
|
||||
lastState.stateId === ApplicationStatus.Errored
|
||||
) {
|
||||
// Will update the cache and update with the new application state
|
||||
// which will trigger the correct application component
|
||||
// under `src\components\applications\App.tsx`
|
||||
updateOwnCache();
|
||||
}
|
||||
}, [data, client]);
|
||||
}, [data, getOrgs, userData?.id]);
|
||||
|
||||
return { data, client, ...rest };
|
||||
return { data, ...rest };
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './useProjectRoutes';
|
||||
export { default as useProjectRoutes } from './useProjectRoutes';
|
||||
@@ -1,160 +0,0 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
|
||||
import { CloudIcon } from '@/components/ui/v2/icons/CloudIcon';
|
||||
import { CogIcon } from '@/components/ui/v2/icons/CogIcon';
|
||||
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
|
||||
import { FileTextIcon } from '@/components/ui/v2/icons/FileTextIcon';
|
||||
import { GaugeIcon } from '@/components/ui/v2/icons/GaugeIcon';
|
||||
import { GraphQLIcon } from '@/components/ui/v2/icons/GraphQLIcon';
|
||||
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
|
||||
import { HomeIcon } from '@/components/ui/v2/icons/HomeIcon';
|
||||
import { RocketIcon } from '@/components/ui/v2/icons/RocketIcon';
|
||||
import { ServicesIcon } from '@/components/ui/v2/icons/ServicesIcon';
|
||||
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
|
||||
import type { SvgIconProps } from '@/components/ui/v2/icons/SvgIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
export interface ProjectRoute {
|
||||
/**
|
||||
* Path relative to the workspace and project root.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* '/sample-path' => '/<workspace-slug>/<project-slug>/sample-path'
|
||||
* ```
|
||||
*/
|
||||
relativePath: string;
|
||||
/**
|
||||
* Main path of the route relative to the workspace and project root.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* '/sample-path' => '/<workspace-slug>/<project-slug>/sample-path/sample-sub-path'
|
||||
* ```
|
||||
*/
|
||||
relativeMainPath?: string;
|
||||
/**
|
||||
* Label of the route.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Determines whether the route should be active even if the href is not
|
||||
* exactly the same as the current path, but starts with it.
|
||||
*/
|
||||
exact?: boolean;
|
||||
/**
|
||||
* Icon to display for the route.
|
||||
*/
|
||||
icon?: ReactElement<SvgIconProps>;
|
||||
/**
|
||||
* Determines whether the route should be disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function useProjectRoutes() {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project, loading } = useProject();
|
||||
const nhostRoutes: ProjectRoute[] = [
|
||||
{
|
||||
relativePath: '/deployments',
|
||||
exact: false,
|
||||
label: 'Deployments',
|
||||
icon: <RocketIcon />,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
{
|
||||
relativePath: '/backups',
|
||||
exact: false,
|
||||
label: 'Backups',
|
||||
icon: <CloudIcon />,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
{
|
||||
relativePath: '/logs',
|
||||
exact: false,
|
||||
label: 'Logs',
|
||||
icon: <FileTextIcon />,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
{
|
||||
relativePath: '/metrics',
|
||||
exact: false,
|
||||
label: 'Metrics',
|
||||
icon: <GaugeIcon />,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
];
|
||||
|
||||
const allRoutes: ProjectRoute[] = [
|
||||
{
|
||||
relativePath: '/',
|
||||
exact: true,
|
||||
label: 'Overview',
|
||||
icon: <HomeIcon />,
|
||||
},
|
||||
{
|
||||
relativePath: '/database/browser/default',
|
||||
exact: false,
|
||||
label: 'Database',
|
||||
icon: <DatabaseIcon />,
|
||||
},
|
||||
{
|
||||
relativePath: '/graphql',
|
||||
exact: true,
|
||||
label: 'GraphQL',
|
||||
icon: <GraphQLIcon />,
|
||||
},
|
||||
{
|
||||
relativePath: '/hasura',
|
||||
exact: true,
|
||||
label: 'Hasura',
|
||||
icon: <HasuraIcon />,
|
||||
disabled: !project?.config?.hasura.settings?.enableConsole,
|
||||
},
|
||||
{
|
||||
relativePath: '/users',
|
||||
exact: false,
|
||||
label: 'Auth',
|
||||
icon: <UserIcon />,
|
||||
},
|
||||
{
|
||||
relativePath: '/storage',
|
||||
exact: false,
|
||||
label: 'Storage',
|
||||
icon: <StorageIcon />,
|
||||
},
|
||||
{
|
||||
relativePath: '/services',
|
||||
exact: false,
|
||||
label: 'Run',
|
||||
icon: <ServicesIcon />,
|
||||
},
|
||||
{
|
||||
relativeMainPath: '/ai',
|
||||
relativePath: '/ai/auto-embeddings',
|
||||
exact: false,
|
||||
label: 'AI',
|
||||
icon: <AIIcon />,
|
||||
},
|
||||
...nhostRoutes,
|
||||
{
|
||||
relativeMainPath: '/settings',
|
||||
relativePath: '/settings/general',
|
||||
exact: false,
|
||||
label: 'Settings',
|
||||
icon: <CogIcon />,
|
||||
disabled: maintenanceActive,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
nhostRoutes,
|
||||
allRoutes,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import type {
|
||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { convertToHasuraPermissions } from '@/features/orgs/projects/database/dataGrid/utils/convertToHasuraPermissions';
|
||||
import { convertToRuleGroup } from '@/features/orgs/projects/database/dataGrid/utils/convertToRuleGroup';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetPostgresSettingsDocument,
|
||||
GetWorkspaceAndProjectDocument,
|
||||
useUpdateDatabaseVersionMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
@@ -42,10 +41,7 @@ export default function DatabaseMigrateVersionConfirmationDialog({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { project } = useProject();
|
||||
const [updatePostgresMajor] = useUpdateDatabaseVersionMutation({
|
||||
refetchQueries: [
|
||||
GetPostgresSettingsDocument,
|
||||
GetWorkspaceAndProjectDocument,
|
||||
],
|
||||
refetchQueries: [GetPostgresSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mockMatchMediaValue } from '@/tests/mocks';
|
||||
import { render, screen } from '@/tests/orgs/testUtils';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi } from 'vitest';
|
||||
import DatabasePiTRSettings from './DatabasePiTRSettings';
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
|
||||
import {
|
||||
GetPostgresSettingsDocument,
|
||||
GetWorkspaceAndProjectDocument,
|
||||
Software_Type_Enum,
|
||||
useGetSoftwareVersionsQuery,
|
||||
useUpdateConfigMutation,
|
||||
@@ -71,10 +70,7 @@ export default function DatabaseServiceVersionSettings() {
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [
|
||||
GetPostgresSettingsDocument,
|
||||
GetWorkspaceAndProjectDocument,
|
||||
],
|
||||
refetchQueries: [GetPostgresSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
GetOrganizationsDocument,
|
||||
useInsertDeploymentMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -49,6 +50,7 @@ export default function DeploymentListItem({
|
||||
}: DeploymentListItemProps) {
|
||||
const { project } = useProject();
|
||||
const { org } = useCurrentOrg();
|
||||
const userData = useUserData();
|
||||
|
||||
const relativeDateOfDeployment = deployment.deploymentStartedAt
|
||||
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
|
||||
@@ -57,7 +59,9 @@ export default function DeploymentListItem({
|
||||
: '';
|
||||
|
||||
const [insertDeployment, { loading }] = useInsertDeploymentMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
refetchQueries: [
|
||||
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
|
||||
],
|
||||
});
|
||||
const { commitMessage } = deployment;
|
||||
|
||||
|
||||
@@ -5,16 +5,15 @@ import { InlineCode } from '@/components/presentational/InlineCode';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
GetOrganizationsDocument,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
|
||||
export interface BaseDirectoryFormValues {
|
||||
/**
|
||||
@@ -27,7 +26,7 @@ export default function BaseDirectorySettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const [updateApp] = useUpdateApplicationMutation();
|
||||
const client = useApolloClient();
|
||||
const userData = useUserData();
|
||||
|
||||
const form = useForm<BaseDirectoryFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
@@ -52,6 +51,9 @@ export default function BaseDirectorySettings() {
|
||||
...values,
|
||||
},
|
||||
},
|
||||
refetchQueries: [
|
||||
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
|
||||
],
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
@@ -66,16 +68,6 @@ export default function BaseDirectorySettings() {
|
||||
"An error occurred while trying to update the project's base directory.",
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await client.refetchQueries({
|
||||
include: [GetAllWorkspacesAndProjectsDocument],
|
||||
});
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
error.message || 'Error while trying to update application cache',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,16 +4,15 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
GetOrganizationsDocument,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
|
||||
export interface DeploymentBranchFormValues {
|
||||
/**
|
||||
@@ -26,7 +25,7 @@ export default function DeploymentBranchSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const [updateApp] = useUpdateApplicationMutation();
|
||||
const client = useApolloClient();
|
||||
const userData = useUserData();
|
||||
|
||||
const form = useForm<DeploymentBranchFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
@@ -53,6 +52,9 @@ export default function DeploymentBranchSettings() {
|
||||
...values,
|
||||
},
|
||||
},
|
||||
refetchQueries: [
|
||||
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
|
||||
],
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
@@ -67,16 +69,6 @@ export default function DeploymentBranchSettings() {
|
||||
'An error occurred while trying to create the permission variable.',
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await client.refetchQueries({
|
||||
include: [GetAllWorkspacesAndProjectsDocument],
|
||||
});
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
error.message || 'Error while trying to update application cache',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mockApplication, mockOrganization } from '@/tests/mocks';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { mockApplication, mockOrganization } from '@/tests/orgs/mocks';
|
||||
import { queryClient, render, screen } from '@/tests/orgs/testUtils';
|
||||
import { queryClient, render, screen } from '@/tests/testUtils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { mockMatchMediaValue, mockRouter } from '@/tests/mocks';
|
||||
import {
|
||||
getProPlanOnlyQuery,
|
||||
getWorkspaceAndProjectQuery,
|
||||
} from '@/tests/msw/mocks/graphql/plansQuery';
|
||||
import { getProPlanOnlyQuery } from '@/tests/msw/mocks/graphql/plansQuery';
|
||||
import {
|
||||
resourcesAvailableQuery,
|
||||
resourcesUnavailableQuery,
|
||||
@@ -40,7 +37,6 @@ const server = setupServer(
|
||||
tokenQuery,
|
||||
resourcesAvailableQuery,
|
||||
getProPlanOnlyQuery,
|
||||
getWorkspaceAndProjectQuery,
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useInsertRunServiceConfigMutation,
|
||||
useReplaceRunServiceConfigMutation,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ErrorToast } from '@/features/orgs/components/ui/v2/ErrorToast';
|
||||
import { ErrorToast } from '@/components/ui/v2/ErrorToast';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
query GetAllWorkspacesAndProjects {
|
||||
workspaces(order_by: { name: asc }) {
|
||||
...Workspace
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
fragment getAppPlanAndGlobalPlansApp on apps {
|
||||
id
|
||||
subdomain
|
||||
workspace {
|
||||
id
|
||||
paymentMethods {
|
||||
id
|
||||
}
|
||||
}
|
||||
legacyPlan {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fragment getAppPlanAndGlobalPlansPlan on plans {
|
||||
id
|
||||
name
|
||||
isFree
|
||||
price
|
||||
featureMaxDbSize
|
||||
}
|
||||
|
||||
query getAppPlanAndGlobalPlans(
|
||||
$workspaceSlug: String!
|
||||
$appSubdomain: String!
|
||||
) {
|
||||
apps(
|
||||
where: {
|
||||
workspace: { slug: { _eq: $workspaceSlug } }
|
||||
slug: { _eq: $appSubdomain }
|
||||
}
|
||||
) {
|
||||
...getAppPlanAndGlobalPlansApp
|
||||
}
|
||||
plans {
|
||||
...getAppPlanAndGlobalPlansPlan
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
query getApplicationPlan($workspace: String!, $slug: String!) {
|
||||
apps(
|
||||
where: { workspace: { slug: { _eq: $workspace } }, slug: { _eq: $slug } }
|
||||
) {
|
||||
id
|
||||
subdomain
|
||||
legacyPlan {
|
||||
name
|
||||
price
|
||||
upatedAt
|
||||
featureMaxDbSize
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
query GetWorkspaceAndProject($workspaceSlug: String!, $projectSlug: String) {
|
||||
workspaces(where: { slug: { _eq: $workspaceSlug } }) {
|
||||
...Workspace
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
fragment getAppPlanAndGlobalPlansApp on apps {
|
||||
id
|
||||
subdomain
|
||||
workspace {
|
||||
id
|
||||
paymentMethods {
|
||||
id
|
||||
}
|
||||
}
|
||||
legacyPlan {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fragment getAppPlanAndGlobalPlansPlan on plans {
|
||||
id
|
||||
name
|
||||
isFree
|
||||
price
|
||||
featureMaxDbSize
|
||||
}
|
||||
|
||||
query getWorkspacesAppPlansAndGlobalPlans(
|
||||
$workspaceSlug: String!
|
||||
$slug: String!
|
||||
) {
|
||||
apps(
|
||||
where: {
|
||||
workspace: { slug: { _eq: $workspaceSlug } }
|
||||
slug: { _eq: $slug }
|
||||
}
|
||||
) {
|
||||
...getAppPlanAndGlobalPlansApp
|
||||
}
|
||||
plans {
|
||||
...getAppPlanAndGlobalPlansPlan
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
mutation insertApplication($app: apps_insert_input!) {
|
||||
insertApp(object: $app) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
workspace {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,6 @@ fragment PrefetchNewAppPlans on plans {
|
||||
featureMaxDbSize
|
||||
}
|
||||
|
||||
fragment PrefetchNewAppWorkspace on workspaces {
|
||||
id
|
||||
name
|
||||
slug
|
||||
paymentMethods {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
query PrefetchNewApp {
|
||||
regions(order_by: { city: asc }) {
|
||||
...PrefetchNewAppRegions
|
||||
@@ -38,7 +29,4 @@ query PrefetchNewApp {
|
||||
) {
|
||||
...PrefetchNewAppPlans
|
||||
}
|
||||
workspaces {
|
||||
...PrefetchNewAppWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
fragment Workspace on workspaces {
|
||||
id
|
||||
name
|
||||
slug
|
||||
creatorUserId
|
||||
workspaceMembers {
|
||||
id
|
||||
user {
|
||||
id
|
||||
email
|
||||
displayName
|
||||
}
|
||||
type
|
||||
}
|
||||
projects: apps(order_by: { name: asc }) {
|
||||
...Project
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,7 @@ query getOrganizations($userId: uuid!) {
|
||||
featureMaxDbSize
|
||||
}
|
||||
apps(order_by: { name: asc }) {
|
||||
id
|
||||
name
|
||||
subdomain
|
||||
slug
|
||||
...Project
|
||||
}
|
||||
members {
|
||||
id
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation deletePaymentMethod($paymentMethodId: uuid!) {
|
||||
deletePaymentMethod(id: $paymentMethodId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
fragment getPaymentMethods on paymentMethods {
|
||||
id
|
||||
createdAt
|
||||
cardBrand
|
||||
cardLast4
|
||||
cardExpMonth
|
||||
cardExpYear
|
||||
isDefault
|
||||
workspace {
|
||||
id
|
||||
apps {
|
||||
id
|
||||
legacyPlan {
|
||||
isFree
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query getPaymentMethods($workspaceId: uuid!) {
|
||||
paymentMethods(
|
||||
where: { workspaceId: { _eq: $workspaceId } }
|
||||
order_by: { createdAt: desc }
|
||||
) {
|
||||
...getPaymentMethods
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
# Update current workspace payment methods to default: false
|
||||
# Insert new payment method with default: true
|
||||
mutation insertNewPaymentMethod(
|
||||
$workspaceId: uuid!
|
||||
$paymentMethod: paymentMethods_insert_input!
|
||||
) {
|
||||
updatePaymentMethods(
|
||||
where: { workspaceId: { _eq: $workspaceId } }
|
||||
_set: { isDefault: false }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
insertPaymentMethod(object: $paymentMethod) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
mutation setNewDefaultPaymentMethod(
|
||||
$workspaceId: uuid!
|
||||
$paymentMethodId: uuid!
|
||||
) {
|
||||
setAllPaymentMethodToDefaultFalse: updatePaymentMethods(
|
||||
where: { workspaceId: { _eq: $workspaceId } }
|
||||
_set: { isDefault: false }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
updatePaymentMethods(
|
||||
where: { id: { _eq: $paymentMethodId } }
|
||||
_set: { isDefault: true }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation deleteWorkspaceMemberInvites($id: uuid!) {
|
||||
deleteWorkspaceMemberInvites(where: { id: { _eq: $id } }) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
query getWorkspaceMemberInvitesToManage($userId: uuid!) {
|
||||
workspaceMemberInvites(where: { userByEmail: { id: { _eq: $userId } } }) {
|
||||
id
|
||||
email
|
||||
userByEmail {
|
||||
id
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mutation insertWorkspaceMemberInvite(
|
||||
$workspaceMemberInvite: workspaceMemberInvites_insert_input!
|
||||
) {
|
||||
insertWorkspaceMemberInvite(object: $workspaceMemberInvite) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
mutation updateWorkspaceMemberInvite(
|
||||
$id: uuid!
|
||||
$workspaceMemberInvite: workspaceMemberInvites_set_input!
|
||||
) {
|
||||
updateWorkspaceMemberInvites(
|
||||
_set: $workspaceMemberInvite
|
||||
where: { id: { _eq: $id } }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation deleteWorkspaceMember($id: uuid!) {
|
||||
deleteWorkspaceMember(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
fragment getWorkspaceMembersWorkspaceMember on workspaceMembers {
|
||||
id
|
||||
type
|
||||
user {
|
||||
id
|
||||
displayName
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
fragment getWorkspaceMembersWorkspaceMemberInvite on workspaceMemberInvites {
|
||||
id
|
||||
email
|
||||
memberType
|
||||
}
|
||||
|
||||
query getWorkspaceMembers($workspaceId: uuid!) {
|
||||
workspace(id: $workspaceId) {
|
||||
id
|
||||
creatorUser {
|
||||
id
|
||||
}
|
||||
workspaceMembers(order_by: { createdAt: asc }) {
|
||||
...getWorkspaceMembersWorkspaceMember
|
||||
}
|
||||
workspaceMemberInvites(order_by: { createdAt: asc }) {
|
||||
...getWorkspaceMembersWorkspaceMemberInvite
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
mutation updateWorkspaceMember(
|
||||
$id: uuid!
|
||||
$workspaceMember: workspaceMembers_set_input!
|
||||
) {
|
||||
updateWorkspaceMember(_set: $workspaceMember, pk_columns: { id: $id }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation deleteWorkspace($id: uuid!) {
|
||||
deleteWorkspace(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
mutation insertWorkspace($workspace: workspaces_insert_input!) {
|
||||
insertWorkspace(object: $workspace) {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
mutation updateWorkspace($id: uuid!, $workspace: workspaces_set_input!) {
|
||||
updateWorkspace(pk_columns: { id: $id }, _set: $workspace) {
|
||||
id
|
||||
name
|
||||
email
|
||||
companyName
|
||||
addressLine1
|
||||
addressLine2
|
||||
addressPostalCode
|
||||
addressCity
|
||||
addressCountryCode
|
||||
slug
|
||||
taxIdType
|
||||
taxIdValue
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
type GetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { Divider } from '@mui/material';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ChangeEvent, ReactElement } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type Workspace = Omit<
|
||||
GetAllWorkspacesAndProjectsQuery['workspaces'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function SelectWorkspaceAndProject() {
|
||||
const user = useUserData();
|
||||
const router = useRouter();
|
||||
|
||||
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const workspaces: Workspace[] = data?.workspaces || [];
|
||||
|
||||
const projects = workspaces.flatMap((workspace) =>
|
||||
workspace.projects.map((project) => ({
|
||||
workspaceName: workspace.name,
|
||||
projectName: project.name,
|
||||
value: `${workspace.slug}/${project.slug}`,
|
||||
})),
|
||||
);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const handleFilterChange = useMemo(
|
||||
() =>
|
||||
debounce((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, 200),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||
|
||||
const goToProjectPage = async (project: {
|
||||
workspaceName: string;
|
||||
projectName: string;
|
||||
value: string;
|
||||
}) => {
|
||||
const { slug } = router.query;
|
||||
|
||||
await router.push({
|
||||
pathname: `/${project.value}/${
|
||||
Array.isArray(slug) ? slug.join('/') : slug
|
||||
}`,
|
||||
});
|
||||
};
|
||||
|
||||
const projectsToDisplay = filter
|
||||
? projects.filter((project) =>
|
||||
project.projectName.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
: projects;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading workspaces and projects..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="mx-auto grid max-w-[760px] grid-flow-row gap-4 py-6 sm:py-14">
|
||||
<Text variant="h2" component="h1" className="">
|
||||
Select a Project
|
||||
</Text>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex w-full">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
onChange={handleFilterChange}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
{projectsToDisplay.length === 0 ? (
|
||||
<Box className="h-import py-2">
|
||||
<Text variant="subtitle2">No results found.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<List className="h-import overflow-y-auto">
|
||||
{projectsToDisplay.map((project, index) => (
|
||||
<Fragment key={project.value}>
|
||||
<ListItem.Root
|
||||
className="grid grid-flow-col justify-start gap-2 py-2.5"
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="primary"
|
||||
onClick={() => goToProjectPage(project)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItem.Avatar>
|
||||
<span className="inline-block h-6 w-6 overflow-hidden rounded-md">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
</ListItem.Avatar>
|
||||
<ListItem.Text
|
||||
primary={project.projectName}
|
||||
secondary={`${project.workspaceName} / ${project.projectName}`}
|
||||
/>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < projects.length - 1 && <Divider component="li" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
SelectWorkspaceAndProject.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="Select a Project">{page}</AuthenticatedLayout>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetOrganizationsDocument,
|
||||
useBillingDeleteAppMutation,
|
||||
usePauseApplicationMutation,
|
||||
useUnpauseApplicationMutation,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, type ReactElement } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
@@ -52,6 +54,7 @@ export default function SettingsGeneralPage() {
|
||||
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
const userData = useUserData();
|
||||
const { project, loading, refetch: refetchProject } = useProject();
|
||||
const { state } = useAppState();
|
||||
|
||||
@@ -74,13 +77,17 @@ export default function SettingsGeneralPage() {
|
||||
const [pauseApplication, { loading: pauseApplicationLoading }] =
|
||||
usePauseApplicationMutation({
|
||||
variables: { appId: project?.id },
|
||||
refetchQueries: [
|
||||
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
|
||||
],
|
||||
});
|
||||
|
||||
const [unpauseApplication, { loading: unpauseApplicationLoading }] =
|
||||
useUnpauseApplicationMutation({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
},
|
||||
variables: { appId: project?.id },
|
||||
refetchQueries: [
|
||||
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
|
||||
],
|
||||
});
|
||||
|
||||
const form = useForm<ProjectNameValidationSchema>({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Select } from '@/components/ui/v2/Select';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
||||
import {
|
||||
useInsertOrgApplicationMutation,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
type GetOrganizationsQuery,
|
||||
type PrefetchNewAppRegionsFragment,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { getErrorMessage } from '@/utils/getErrorMessage';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -344,8 +344,8 @@ export default function NewProjectPage() {
|
||||
|
||||
const { regions } = data;
|
||||
|
||||
// get pre-selected workspace
|
||||
// use query param to get workspace or just pick first workspace
|
||||
// get pre-selected organization
|
||||
// use query param to get organization or just pick first organization
|
||||
const preSelectedOrg = currentOrg || orgs[0];
|
||||
const preSelectedRegion = regions.find((region) => region.active);
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import type { ChangeEvent, ReactElement } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface ProjectSelectorOption {
|
||||
type: 'workspace-project' | 'org-project';
|
||||
projectName: string;
|
||||
projectPathDescriptor: string;
|
||||
route: string;
|
||||
@@ -28,14 +27,13 @@ interface ProjectSelectorOption {
|
||||
plan: string;
|
||||
}
|
||||
|
||||
export default function SelectWorkspaceAndProject() {
|
||||
export default function SelectOrganizationAndProject() {
|
||||
const router = useRouter();
|
||||
const { openAlertDialog } = useDialog();
|
||||
const { orgs, loading: loadingOrgs } = useOrgs();
|
||||
|
||||
const orgProjects: ProjectSelectorOption[] = orgs.flatMap((org) =>
|
||||
org.apps.map((project) => ({
|
||||
type: 'org-project',
|
||||
projectName: project.name,
|
||||
projectPathDescriptor: `${org.name}/${project.name}`,
|
||||
route: `/orgs/${org.slug}/projects/${project.subdomain}/run`,
|
||||
@@ -192,13 +190,9 @@ export default function SelectWorkspaceAndProject() {
|
||||
className={cn(
|
||||
'hover:none ml-2 h-5 px-[6px] text-[10px]',
|
||||
project.isFree && 'bg-muted',
|
||||
project.type === 'workspace-project' &&
|
||||
'bg-orange-200 text-foreground hover:bg-orange-200 dark:bg-orange-500',
|
||||
)}
|
||||
>
|
||||
{project.type === 'workspace-project'
|
||||
? 'Legacy'
|
||||
: project.plan}
|
||||
{project.plan}
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
@@ -218,7 +212,9 @@ export default function SelectWorkspaceAndProject() {
|
||||
);
|
||||
}
|
||||
|
||||
SelectWorkspaceAndProject.getLayout = function getLayout(page: ReactElement) {
|
||||
SelectOrganizationAndProject.getLayout = function getLayout(
|
||||
page: ReactElement,
|
||||
) {
|
||||
return (
|
||||
<AuthenticatedLayout title="New Run Service">{page}</AuthenticatedLayout>
|
||||
);
|
||||
|
||||
@@ -9,13 +9,11 @@ import { EnvelopeIcon } from '@/components/ui/v2/icons/EnvelopeIcon';
|
||||
import { Input, inputClasses } from '@/components/ui/v2/Input';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
useGetOrganizationsQuery,
|
||||
type GetAllWorkspacesAndProjectsQuery,
|
||||
type GetOrganizationsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { styled } from '@mui/material';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
@@ -23,11 +21,6 @@ import { type ReactElement } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
type Workspace = Omit<
|
||||
GetAllWorkspacesAndProjectsQuery['workspaces'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
type Organization = Omit<
|
||||
GetOrganizationsQuery['organizations'][0],
|
||||
'__typename'
|
||||
@@ -35,7 +28,6 @@ type Organization = Omit<
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
organization: Yup.string().label('Organization'),
|
||||
workspace: Yup.string().label('Workspace'),
|
||||
project: Yup.string().label('Project').required(),
|
||||
services: Yup.array()
|
||||
.of(Yup.object({ label: Yup.string(), value: Yup.string() }))
|
||||
@@ -60,7 +52,7 @@ function TicketPage() {
|
||||
const form = useForm<CreateTicketFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
workspace: '',
|
||||
organization: '',
|
||||
project: '',
|
||||
services: [],
|
||||
priority: '',
|
||||
@@ -77,20 +69,15 @@ function TicketPage() {
|
||||
formState: { errors, isSubmitting },
|
||||
} = form;
|
||||
|
||||
const selectedWorkspace = watch('workspace');
|
||||
const selectedOrganization = watch('organization');
|
||||
const user = useUserData();
|
||||
|
||||
const { data } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
const { data: organizationsData } = useGetOrganizationsQuery({
|
||||
variables: {
|
||||
userId: user?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const workspaces: Workspace[] = data?.workspaces || [];
|
||||
const organizations: Organization[] = organizationsData?.organizations || [];
|
||||
|
||||
const getAvailableProjects = () => {
|
||||
@@ -99,12 +86,6 @@ function TicketPage() {
|
||||
organizations.find((org) => org.id === selectedOrganization)?.apps || []
|
||||
);
|
||||
}
|
||||
if (selectedWorkspace) {
|
||||
return (
|
||||
workspaces.find((workspace) => workspace.id === selectedWorkspace)
|
||||
?.projects || []
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -192,69 +173,32 @@ function TicketPage() {
|
||||
>
|
||||
<Text className="font-bold">Which project is affected ?</Text>
|
||||
|
||||
<Box className="grid grid-cols-[1fr,auto,1fr] items-start gap-4">
|
||||
<ControlledSelect
|
||||
id="organization"
|
||||
name="organization"
|
||||
label="Organization"
|
||||
placeholder="Organization"
|
||||
slotProps={{
|
||||
root: { className: 'grid grid-flow-col gap-1' },
|
||||
}}
|
||||
error={!!errors.organization}
|
||||
helperText={errors.organization?.message}
|
||||
disabled={!!selectedWorkspace}
|
||||
renderValue={(option) => (
|
||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||
{option?.label}
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<Option value="" label="" />
|
||||
{organizations.map((organization) => (
|
||||
<Option
|
||||
key={organization.name}
|
||||
value={organization.id}
|
||||
label={organization.name}
|
||||
>
|
||||
{organization.name}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
|
||||
<Text className="mt-[34px] text-center font-medium">
|
||||
or
|
||||
</Text>
|
||||
|
||||
<ControlledSelect
|
||||
id="workspace"
|
||||
name="workspace"
|
||||
label="Workspace"
|
||||
placeholder="Workspace"
|
||||
slotProps={{
|
||||
root: { className: 'grid grid-flow-col gap-1' },
|
||||
}}
|
||||
error={!!errors.workspace}
|
||||
helperText={errors.workspace?.message}
|
||||
disabled={!!selectedOrganization}
|
||||
renderValue={(option) => (
|
||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||
{option?.label}
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<Option value="" label="" />
|
||||
{workspaces.map((workspace) => (
|
||||
<Option
|
||||
key={workspace.name}
|
||||
value={workspace.id}
|
||||
label={workspace.name}
|
||||
>
|
||||
{workspace.name}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
</Box>
|
||||
<ControlledSelect
|
||||
id="organization"
|
||||
name="organization"
|
||||
label="Organization"
|
||||
placeholder="Organization"
|
||||
slotProps={{
|
||||
root: { className: 'grid grid-flow-col gap-1 mb-4' },
|
||||
}}
|
||||
error={!!errors.organization}
|
||||
helperText={errors.organization?.message}
|
||||
renderValue={(option) => (
|
||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||
{option?.label}
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
{organizations.map((organization) => (
|
||||
<Option
|
||||
key={organization.name}
|
||||
value={organization.id}
|
||||
label={organization.name}
|
||||
>
|
||||
{organization.name}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
|
||||
<ControlledSelect
|
||||
id="project"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Organization, Project, Workspace } from '@/types/application';
|
||||
import type { Organization, Project } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Organization_Status_Enum } from '@/utils/__generated__/graphql';
|
||||
import { faker } from '@faker-js/faker';
|
||||
@@ -19,15 +19,15 @@ export const mockMatchMediaValue = (query: any) => ({
|
||||
|
||||
export const mockRouter: NextRouter = {
|
||||
basePath: '',
|
||||
pathname: '/test-workspace/test-application',
|
||||
route: '/[workspaceSlug]/[appSlug]',
|
||||
asPath: '/test-workspace/test-application',
|
||||
pathname: '/orgs/xyz/projects/test-project',
|
||||
route: '/orgs/[orgSlug]/projects/[appSubdomain]',
|
||||
asPath: '/orgs/xyz/projects/test-project',
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
query: {
|
||||
workspaceSlug: 'test-workspace',
|
||||
appSlug: 'test-application',
|
||||
orgSlug: 'xyz',
|
||||
appSubdomain: 'test-project',
|
||||
},
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
@@ -46,10 +46,10 @@ export const mockRouter: NextRouter = {
|
||||
|
||||
export const mockApplication: Project = {
|
||||
id: '1',
|
||||
name: 'Test Application',
|
||||
slug: 'test-application',
|
||||
name: 'Test Project',
|
||||
slug: 'test-project',
|
||||
appStates: [],
|
||||
subdomain: 'subdomain',
|
||||
subdomain: 'test-project',
|
||||
region: {
|
||||
name: 'us-east-1',
|
||||
city: 'New York',
|
||||
@@ -83,15 +83,6 @@ export const mockApplication: Project = {
|
||||
},
|
||||
};
|
||||
|
||||
export const mockWorkspace: Workspace = {
|
||||
id: '1',
|
||||
name: 'Test Workspace',
|
||||
slug: 'test-workspace',
|
||||
workspaceMembers: [],
|
||||
projects: [mockApplication],
|
||||
creatorUserId: '1',
|
||||
};
|
||||
|
||||
export const mockSession: NhostSession = {
|
||||
accessToken: faker.random.alphaNumeric(),
|
||||
accessTokenExpiresIn: 900,
|
||||
@@ -130,15 +121,7 @@ export const mockOrganization: Organization = {
|
||||
__typename: 'plans',
|
||||
},
|
||||
members: [],
|
||||
apps: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Test Application',
|
||||
subdomain: '',
|
||||
slug: 'test-application',
|
||||
__typename: 'apps',
|
||||
},
|
||||
],
|
||||
apps: [mockApplication],
|
||||
__typename: 'organizations',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mockApplication, mockWorkspace } from '@/tests/mocks';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
/**
|
||||
@@ -49,18 +48,3 @@ export const getAllPlansQuery = nhostGraphQLLink.query(
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Use this handler to simulate a query that returns a workspace and a project.
|
||||
* Useful if you want to mock the currently selected project.
|
||||
*/
|
||||
export const getWorkspaceAndProjectQuery = nhostGraphQLLink.query(
|
||||
'GetWorkspaceAndProject',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
workspaces: [mockWorkspace],
|
||||
projects: [mockApplication],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -67,7 +67,6 @@ export const prefetchNewAppQuery = nhostGraphQLLink.query(
|
||||
__typename: 'plans',
|
||||
},
|
||||
],
|
||||
workspaces: [],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import type { Organization } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Organization_Status_Enum } from '@/utils/__generated__/graphql';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { NhostSession } from '@nhost/nextjs';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
export const mockMatchMediaValue = (query: any) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
});
|
||||
|
||||
export const mockRouter: NextRouter = {
|
||||
basePath: '',
|
||||
pathname: '/orgs/xyz/projects/test-project',
|
||||
route: '/orgs/[orgSlug]/projects/[appSubdomain]',
|
||||
asPath: '/orgs/xyz/projects/test-project',
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
query: {
|
||||
orgSlug: 'xyz',
|
||||
appSubdomain: 'test-project',
|
||||
},
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
reload: vi.fn(),
|
||||
back: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
beforePopState: vi.fn(),
|
||||
events: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
forward: vi.fn(),
|
||||
};
|
||||
|
||||
export const mockApplication = {
|
||||
id: '1',
|
||||
name: 'Test Project',
|
||||
slug: 'test-project',
|
||||
appStates: [],
|
||||
subdomain: 'test-project',
|
||||
region: {
|
||||
name: 'us-east-1',
|
||||
city: 'New York',
|
||||
countryCode: 'US',
|
||||
id: '1',
|
||||
domain: 'nhost.run',
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
deployments: [],
|
||||
desiredState: ApplicationStatus.Live,
|
||||
featureFlags: [],
|
||||
githubRepository: { fullName: 'test/git-project' },
|
||||
repositoryProductionBranch: null,
|
||||
nhostBaseFolder: null,
|
||||
legacyPlan: {
|
||||
id: '1',
|
||||
name: 'Starter',
|
||||
isFree: true,
|
||||
price: 0,
|
||||
featureMaxDbSize: 1,
|
||||
},
|
||||
config: {
|
||||
observability: {
|
||||
grafana: {
|
||||
adminPassword: 'admin',
|
||||
},
|
||||
},
|
||||
hasura: {
|
||||
adminSecret: 'nhost-admin-secret',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockSession: NhostSession = {
|
||||
accessToken: faker.random.alphaNumeric(),
|
||||
accessTokenExpiresIn: 900,
|
||||
refreshToken: faker.datatype.uuid(),
|
||||
user: {
|
||||
id: faker.datatype.uuid(),
|
||||
email: faker.internet.email(),
|
||||
displayName: faker.name.fullName(),
|
||||
createdAt: faker.date.past().toISOString(),
|
||||
avatarUrl: faker.image.avatar(),
|
||||
locale: 'en',
|
||||
isAnonymous: false,
|
||||
defaultRole: 'user',
|
||||
roles: ['user', 'me'],
|
||||
metadata: {},
|
||||
emailVerified: true,
|
||||
phoneNumber: faker.phone.number(),
|
||||
phoneNumberVerified: true,
|
||||
activeMfaType: 'totp',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockOrganization: Organization = {
|
||||
id: '93297df9-125e-49df-9db3-94067fa065bd',
|
||||
name: 'Test organization',
|
||||
slug: 'xyz',
|
||||
status: Organization_Status_Enum.Ok,
|
||||
plan: {
|
||||
id: 'abc',
|
||||
name: 'Pro',
|
||||
deprecated: false,
|
||||
individual: false,
|
||||
isFree: false,
|
||||
price: 25,
|
||||
featureMaxDbSize: 1,
|
||||
},
|
||||
members: [],
|
||||
apps: [mockApplication],
|
||||
};
|
||||
@@ -1,164 +0,0 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { DialogProvider } from '@/components/common/DialogProvider';
|
||||
import { UIProvider } from '@/components/common/UIProvider';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { createTheme } from '@/components/ui/v2/createTheme';
|
||||
import { mockRouter, mockSession } from '@/tests/orgs/mocks';
|
||||
import { createEmotionCache } from '@/utils/createEmotionCache';
|
||||
import { createHttpLink } from '@apollo/client';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { NhostClient, NhostProvider } from '@nhost/nextjs';
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type {
|
||||
Queries,
|
||||
queries,
|
||||
RenderOptions,
|
||||
waitForOptions,
|
||||
} from '@testing-library/react';
|
||||
import {
|
||||
render as rtlRender,
|
||||
waitForElementToBeRemoved as rtlWaitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { vi } from 'vitest';
|
||||
import nhostGraphQLLink from '../msw/mocks/graphql/nhostGraphQLLink';
|
||||
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const emotionCache = createEmotionCache();
|
||||
|
||||
process.env = {
|
||||
TEST_MODE: 'true',
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
NEXT_PUBLIC_NHOST_AUTH_URL: 'https://local.auth.local.nhost.run/v1',
|
||||
NEXT_PUBLIC_NHOST_FUNCTIONS_URL: 'https://local.functions.local.nhost.run/v1',
|
||||
NEXT_PUBLIC_NHOST_GRAPHQL_URL: 'https://local.graphql.local.nhost.run/v1',
|
||||
NEXT_PUBLIC_NHOST_STORAGE_URL: 'https://local.storage.local.nhost.run/v1',
|
||||
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL: 'https://local.hasura.local.nhost.run',
|
||||
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL:
|
||||
'https://local.hasura.local.nhost.run',
|
||||
NEXT_PUBLIC_NHOST_HASURA_API_URL: 'https://local.hasura.local.nhost.run',
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: 0,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function Providers({ children }: PropsWithChildren<{}>) {
|
||||
const nhost = new NhostClient({ subdomain: 'local' });
|
||||
const theme = createTheme('light');
|
||||
|
||||
return (
|
||||
<RouterContext.Provider value={mockRouter}>
|
||||
<RetryableErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CacheProvider value={emotionCache}>
|
||||
<NhostProvider nhost={nhost} initial={mockSession}>
|
||||
<NhostApolloProvider
|
||||
nhost={nhost}
|
||||
generateLinks={() => [
|
||||
createHttpLink({
|
||||
uri: 'https://local.graphql.local.nhost.run/v1',
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<UIProvider>
|
||||
<Toaster position="bottom-center" />
|
||||
<ThemeProvider theme={theme}>
|
||||
<DialogProvider>{children}</DialogProvider>
|
||||
</ThemeProvider>
|
||||
</UIProvider>
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</CacheProvider>
|
||||
</QueryClientProvider>
|
||||
</RetryableErrorBoundary>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function render<
|
||||
Q extends Queries = typeof queries,
|
||||
Container extends Element | DocumentFragment = HTMLElement,
|
||||
BaseElement extends Element | DocumentFragment = Container,
|
||||
>(ui: ReactElement, options?: RenderOptions<Q, Container, BaseElement>) {
|
||||
return rtlRender<Q, Container, BaseElement>(ui, {
|
||||
wrapper: Providers,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForElementToBeRemoved<T>(
|
||||
callback: T | (() => T),
|
||||
options?: waitForOptions,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await rtlWaitForElementToBeRemoved(callback, options);
|
||||
} catch {
|
||||
// We shouldn't fail if the element was to be removed but it wasn't there in
|
||||
// the first place.
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
const graphqlRequestHandlerFactory = (
|
||||
operationName: string,
|
||||
type: 'mutation' | 'query',
|
||||
responsePromise,
|
||||
) =>
|
||||
nhostGraphQLLink[type](operationName, async (_req, res, ctx) => {
|
||||
const data = await responsePromise;
|
||||
return res(ctx.data(data));
|
||||
});
|
||||
/* Helper function to pause responses to be able to test loading states */
|
||||
export const createGraphqlMockResolver = (
|
||||
operationName: string,
|
||||
type: 'mutation' | 'query',
|
||||
defaultResponse?: any,
|
||||
) => {
|
||||
let resolver;
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
return {
|
||||
handler: graphqlRequestHandlerFactory(operationName, type, responsePromise),
|
||||
resolve: (response = undefined) => resolver(response ?? defaultResponse),
|
||||
};
|
||||
};
|
||||
|
||||
export const mockPointerEvent = () => {
|
||||
// Note: Workaround based on https://github.com/radix-ui/primitives/issues/1382#issuecomment-1122069313
|
||||
class MockPointerEvent extends Event {
|
||||
button: number;
|
||||
|
||||
ctrlKey: boolean;
|
||||
|
||||
pointerType: string;
|
||||
|
||||
constructor(type: string, props: PointerEventInit) {
|
||||
super(type, props);
|
||||
this.button = props.button || 0;
|
||||
this.ctrlKey = props.ctrlKey || false;
|
||||
this.pointerType = props.pointerType || 'mouse';
|
||||
}
|
||||
}
|
||||
window.PointerEvent = MockPointerEvent as any;
|
||||
window.HTMLElement.prototype.scrollIntoView = vi.fn();
|
||||
window.HTMLElement.prototype.releasePointerCapture = vi.fn();
|
||||
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
|
||||
};
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { render, waitForElementToBeRemoved };
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { vi } from 'vitest';
|
||||
import nhostGraphQLLink from './msw/mocks/graphql/nhostGraphQLLink';
|
||||
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const emotionCache = createEmotionCache();
|
||||
@@ -110,5 +112,53 @@ async function waitForElementToBeRemoved<T>(
|
||||
}
|
||||
}
|
||||
|
||||
const graphqlRequestHandlerFactory = (
|
||||
operationName: string,
|
||||
type: 'mutation' | 'query',
|
||||
responsePromise,
|
||||
) =>
|
||||
nhostGraphQLLink[type](operationName, async (_req, res, ctx) => {
|
||||
const data = await responsePromise;
|
||||
return res(ctx.data(data));
|
||||
});
|
||||
/* Helper function to pause responses to be able to test loading states */
|
||||
export const createGraphqlMockResolver = (
|
||||
operationName: string,
|
||||
type: 'mutation' | 'query',
|
||||
defaultResponse?: any,
|
||||
) => {
|
||||
let resolver;
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
return {
|
||||
handler: graphqlRequestHandlerFactory(operationName, type, responsePromise),
|
||||
resolve: (response = undefined) => resolver(response ?? defaultResponse),
|
||||
};
|
||||
};
|
||||
|
||||
export const mockPointerEvent = () => {
|
||||
// Note: Workaround based on https://github.com/radix-ui/primitives/issues/1382#issuecomment-1122069313
|
||||
class MockPointerEvent extends Event {
|
||||
button: number;
|
||||
|
||||
ctrlKey: boolean;
|
||||
|
||||
pointerType: string;
|
||||
|
||||
constructor(type: string, props: PointerEventInit) {
|
||||
super(type, props);
|
||||
this.button = props.button || 0;
|
||||
this.ctrlKey = props.ctrlKey || false;
|
||||
this.pointerType = props.pointerType || 'mouse';
|
||||
}
|
||||
}
|
||||
window.PointerEvent = MockPointerEvent as any;
|
||||
window.HTMLElement.prototype.scrollIntoView = vi.fn();
|
||||
window.HTMLElement.prototype.releasePointerCapture = vi.fn();
|
||||
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
|
||||
};
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { render, waitForElementToBeRemoved };
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
GetProjectQuery,
|
||||
PermissionVariableFragment,
|
||||
SecretFragment,
|
||||
WorkspaceFragment,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
|
||||
/**
|
||||
@@ -39,7 +38,6 @@ export type DesiredState =
|
||||
|
||||
export type ApplicationState = AppStateHistoryFragment;
|
||||
export type Deployment = DeploymentRowFragment;
|
||||
export type Workspace = WorkspaceFragment;
|
||||
export type Organization = GetOrganizationQuery['organizations'][0];
|
||||
// export type Project = ProjectFragment;
|
||||
export type Project = GetProjectQuery['apps'][0];
|
||||
|
||||
20728
dashboard/src/utils/__generated__/graphite.graphql.ts
generated
20728
dashboard/src/utils/__generated__/graphite.graphql.ts
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user