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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </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>&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-a66cba186d2014b03f1a0e005147ae7b48e88933700fe065d235cd819a949a97">+28/-84</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>ApplicationUnknown.tsx</strong><dd><code>Refactor
ApplicationUnknown component for new structure</code>&nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-d1d7044dd66488c5bc787a89612754b283eedb404d4d6abcface2fa533d5c9d3">+17/-21</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>DeleteAccount.tsx</strong><dd><code>Update imports and
remove workspace references</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-3d84927ffa4b91d986ff6c6f601b3476503220e1c1d8cde25ebf72c8d0ed6b9e">+2/-26</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>run-one-click-install.tsx</strong><dd><code>Refactor
one-click install for organization structure</code>&nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-00e84c02bfc3c34019e15f820b23e332eeb1933a745be330c3644cb0f63c92b5">+5/-9</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>RemoveApplicationModal.tsx</strong><dd><code>Update mutation
refetch query for organization structure</code>&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-e454a42c12dcbfcfaa463ab3421037408634e3a539f460525c79d68adfc118ab">+7/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ApplicationInfo.tsx</strong><dd><code>Update mutation
refetch query in ApplicationInfo</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-7372ad22d70c3c354d8e0dd442eb7e49f70f65a386b934b6eee7f8c4b89c3a3f">+8/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useProjectRedirectWhenReady.ts</strong><dd><code>Update
refetch query for organization structure</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-a234bc908266de3091b23b5134a01fd769f96759eb52aa108d2ad4b796b0303f">+2/-6</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>

<td><strong>DatabaseMigrateVersionConfirmationDialog.tsx</strong><dd><code>Remove
workspace refetch query in migration dialog</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-89e193ec45127a72f9491ad89eed5eda5939936686f88aadb48cfac350462271">+1/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>new.tsx</strong><dd><code>Update new project page for
organization structure</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-ef97470126e3edc146dda51337aaec556387e2f8a37afa70810d1dc94958f4fd">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>BreadcrumbNav.tsx</strong><dd><code>Remove workspace
references from BreadcrumbNav</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-2a69d273b2a9e8695d46f6c73dcbb6e161d3bb85f52deb65930018b17b148b3e">+3/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>BaseDirectorySettings.tsx</strong><dd><code>Update refetch
query for organization structure</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-50bcccdf949a19ce69fa86acdd63b5291fa2beaba07191a62c87d40ea5b94e88">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DeploymentBranchSettings.tsx</strong><dd><code>Update
refetch query for organization structure</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-d8fc80cc734f593c686f873536856bf9103efb1115ca865709bbeb7bd940895e">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>DeploymentListItem.tsx</strong><dd><code>Update refetch
query for organization structure</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-2a548c457ff2ab8fc1bee326a6a3b5eae9d0d6eb18f5ae95bbdb437c3f6b0a73">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>index.tsx</strong><dd><code>Update mutations to refetch
organization data</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-b4185be97a505e25badcdefe31ea86fa9d69f72264c4bb35eae17fba936a3d47">+4/-3</a>&nbsp;
&nbsp; &nbsp; </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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-d1ef12c0f15123bb4e23a0c513fc3d9b5c16af421c71c2909fde3717e09a9d89">+10/-27</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>testUtils.tsx</strong><dd><code>Add new test utilities for
GraphQL mocking</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-78f29250407edf853a353b48242d3cee59aa5724f38a60bb23bebdfc1ea2f9b5">+50/-0</a>&nbsp;
&nbsp; </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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3241/files#diff-262687fd80c4510f966a57885b1cc42a6297fd89ab49f6ff49b0df59670027f1">+1/-3</a>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
</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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
</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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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:
David BM
2025-03-27 16:25:39 +01:00
committed by GitHub
parent 7b9cdf1f5f
commit 8ea263ec75
106 changed files with 588 additions and 23956 deletions

View File

@@ -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 }}

View File

@@ -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:

View File

@@ -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;

View File

@@ -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&apos;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&apos;re here to help, so don&apos;t hesitate to reach out!</Text>
</div>
);
}

View File

@@ -1,2 +0,0 @@
export * from './ContactUs';
export { default as ContactUs } from './ContactUs';

View File

@@ -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';

View File

@@ -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>
);
}

View File

@@ -1 +0,0 @@
export { default as InviteNotification } from './InviteNotification';

View File

@@ -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';

View File

@@ -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>

View File

@@ -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">

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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" />

View File

@@ -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();
});

View File

@@ -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

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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();
});

View File

@@ -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>
);
}

View File

@@ -1 +0,0 @@
export { default as ErrorToast } from './ErrorToast';

View File

@@ -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';

View File

@@ -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 }] =

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>
);

View File

@@ -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>
</>
);
}

View File

@@ -1 +0,0 @@
export { default as ApplicationErrored } from './ApplicationErrored';

View File

@@ -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(

View File

@@ -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 (
<>

View File

@@ -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 () => {

View File

@@ -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"

View File

@@ -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';

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();

View File

@@ -1 +0,0 @@
export { default as useNavigationVisible } from './useNavigationVisible';

View File

@@ -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;
}

View File

@@ -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,
]);

View File

@@ -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 };
}

View File

@@ -1,2 +0,0 @@
export * from './useProjectRoutes';
export { default as useProjectRoutes } from './useProjectRoutes';

View File

@@ -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,
};
}

View File

@@ -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';

View File

@@ -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 } : {}),
});

View File

@@ -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';

View File

@@ -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 } : {}),
});

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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';

View File

@@ -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(() => {

View File

@@ -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';

View File

@@ -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';

View File

@@ -1,5 +0,0 @@
query GetAllWorkspacesAndProjects {
workspaces(order_by: { name: asc }) {
...Workspace
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -1,5 +0,0 @@
query GetWorkspaceAndProject($workspaceSlug: String!, $projectSlug: String) {
workspaces(where: { slug: { _eq: $workspaceSlug } }) {
...Workspace
}
}

View File

@@ -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
}
}

View File

@@ -1,12 +0,0 @@
mutation insertApplication($app: apps_insert_input!) {
insertApp(object: $app) {
id
name
slug
workspace {
id
name
slug
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -16,10 +16,7 @@ query getOrganizations($userId: uuid!) {
featureMaxDbSize
}
apps(order_by: { name: asc }) {
id
name
subdomain
slug
...Project
}
members {
id

View File

@@ -1,5 +0,0 @@
mutation deletePaymentMethod($paymentMethodId: uuid!) {
deletePaymentMethod(id: $paymentMethodId) {
id
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -1,5 +0,0 @@
mutation deleteWorkspaceMemberInvites($id: uuid!) {
deleteWorkspaceMemberInvites(where: { id: { _eq: $id } }) {
affected_rows
}
}

View File

@@ -1,14 +0,0 @@
query getWorkspaceMemberInvitesToManage($userId: uuid!) {
workspaceMemberInvites(where: { userByEmail: { id: { _eq: $userId } } }) {
id
email
userByEmail {
id
}
workspace {
id
name
slug
}
}
}

View File

@@ -1,7 +0,0 @@
mutation insertWorkspaceMemberInvite(
$workspaceMemberInvite: workspaceMemberInvites_insert_input!
) {
insertWorkspaceMemberInvite(object: $workspaceMemberInvite) {
id
}
}

View File

@@ -1,11 +0,0 @@
mutation updateWorkspaceMemberInvite(
$id: uuid!
$workspaceMemberInvite: workspaceMemberInvites_set_input!
) {
updateWorkspaceMemberInvites(
_set: $workspaceMemberInvite
where: { id: { _eq: $id } }
) {
affected_rows
}
}

View File

@@ -1,5 +0,0 @@
mutation deleteWorkspaceMember($id: uuid!) {
deleteWorkspaceMember(id: $id) {
id
}
}

View File

@@ -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
}
}
}

View File

@@ -1,8 +0,0 @@
mutation updateWorkspaceMember(
$id: uuid!
$workspaceMember: workspaceMembers_set_input!
) {
updateWorkspaceMember(_set: $workspaceMember, pk_columns: { id: $id }) {
id
}
}

View File

@@ -1,5 +0,0 @@
mutation deleteWorkspace($id: uuid!) {
deleteWorkspace(id: $id) {
id
}
}

View File

@@ -1,6 +0,0 @@
mutation insertWorkspace($workspace: workspaces_insert_input!) {
insertWorkspace(object: $workspace) {
name
id
}
}

View File

@@ -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
}
}

View File

@@ -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>
);
};

View File

@@ -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>({

View File

@@ -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);

View File

@@ -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>
);

View File

@@ -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"

View File

@@ -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',
};

View File

@@ -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],
}),
),
);

View File

@@ -67,7 +67,6 @@ export const prefetchNewAppQuery = nhostGraphQLLink.query(
__typename: 'plans',
},
],
workspaces: [],
}),
),
);

View File

@@ -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],
};

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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];

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